Skip to content

12. استخدام SGBD MySQL

Image

سنقوم الآن بكتابة نصوص برمجية PHP باستخدام قاعدة بيانات MySQL:

Image

في البنية المذكورة أعلاه، لا يتواصل البرنامج النصي PHP (1) مباشرةً مع SGBD (نظام إدارة قواعد البيانات) (3). بل يتواصل مع وسيط يُسمى برنامج تشغيل SGBD أو برنامج تشغيل SGBD. يوفر PHP واجهة قياسية لهذه برامج التشغيل، وهي واجهة PDO (كائنات بيانات PHP). يتم تنفيذ هذه الواجهة من خلال فئات مختلفة مخصصة لكل SGBD: فئة لـ SGBD MySQL، وأخرى لـ SGBD PostgreSQL… لتغيير SGBD، يتم تغيير برنامج التشغيل:

Image

يعزل برنامج التشغيل PDO البرنامج النصي PHP (1) عن SGBD (3، 6). ونظرًا لأن هذه برامج التشغيل تنفذ واجهة قياسية، فمن المتوقع ألا يتغير البرنامج النصي PHP (1) عند الانتقال من SGBD أو MySQL (3) إلى SGBD و PostgreSQL (6). لكن في الواقع، هذا الوضع المثالي غير موجود. ففي الواقع، للتواصل مع SGBD، يرسل البرنامج النصي PHP أوامر SQL (لغة الاستعلام القياسية). وهي لغة يتم تنفيذها من قبل جميع SGBD، لكنها غير مكتملة. ولذلك أضافت SGBD إليها أوامر خاصة بها. وهذا هو السبب الأول لعدم التوافق بين SGBD. بالإضافة إلى ذلك، قد تختلف أنواع البيانات القابلة للاستخدام في قواعد البيانات من SGBD إلى آخر. وبالتالي، فإن PostgreSQL يقبل عددًا من أنواع البيانات أكبر بكثير من SGBD وMySQL. وهذا هو السبب الثاني لعدم التوافق. سبب آخر هو إدارة المفاتيح الأولية التلقائية (التي يتم إنشاؤها بواسطة SGBD): تقريبًا كل SGBD له سياسته الخاصة. إلخ... أسباب عدم التوافق عديدة.

إذا أردنا تجنب إعادة كتابة البرنامج النصي PHP (1) بالانتقال من MySQL (3) إلى PostgreSQL (6)، فإننا نضطر عادةً إلى إدراج طبقة جديدة بين البرنامج النصي PHP (1) وبرنامج التشغيل PDO (2، 5) والتي ستكون مهمتها إزالة حالات عدم التوافق بين البرنامجين النصيين SGBD. ومع ذلك، في الحالات البسيطة التي سنواجهها، لن تكون هذه الطبقة الإضافية ضرورية.

سنستخدم الآن SGBD MySQL. وهو مضمن في حزمة Laragon (انظر الفقرة الرابط).

إذا كان القارئ مبتدئًا في مجال قواعد البيانات ولغة SQL، فيمكنه قراءة الوثيقة [http://sergetahe.com/cours-tutoriels-de-programmation/cours-tutoriel-sql-avec-le-sgbd-firebird/]. يستخدم هذا المستند قاعدة بيانات Firebird SGBD وليس MySQL، ولكنه يقدم الأساسيات المتعلقة بقواعد البيانات ولغة SQL. وكما هو الحال مع MySQL، يتوفر لـ Firebird إصدار مجاني للاستخدام ولا يستهلك سوى مساحة صغيرة من الذاكرة.

12.1. إنشاء قاعدة بيانات

سنوضح الآن كيفية إنشاء قاعدة بيانات ومستخدم MySQL باستخدام أداة Laragon.

Image

  • بمجرد تشغيله، يمكن إدارة Laragon [1] من خلال قائمة [2]؛
  • في [3-5]، يتم تثبيت أداة إدارة [phpMyAdmin] لـ MySQL إذا لم تكن مثبتة بالفعل؛

Image

  • في [6]، يتم تشغيل خادم الويب Apache بالإضافة إلى SGBD وMySQL؛
  • في [7]، يتم تشغيل خادم Apache؛
  • في [8]، يتم تشغيل SBD MySQL؛

Image

  • في [8-10]، يتم إنشاء قاعدة بيانات تسمى [dbpersonnes] [11]. سنقوم بإنشاء قاعدة بيانات للأشخاص؛

Image

  • في [11]، سنقوم بإدارة قاعدة البيانات التي أنشأناها للتو؛

Image

  • تقوم العملية [Bases de données] بإرسال طلب ويب إلى URL [http://localhost/phpmyadmin]. ويقوم خادم الويب Apache الخاص بـ Laragon بالرد. URL [http://localhost/phpmyadmin] هي URL للأداة المساعدة [phpMyAdmin] التي قمنا بتثبيتها سابقًا [5]. تتيح هذه الأداة إدارة قواعد البيانات MySQL؛
  • بشكل افتراضي، بيانات تسجيل الدخول الخاصة بمسؤول قاعدة البيانات هي: root [13] بدون كلمة مرور [14]؛

Image

  • في [16]، قاعدة البيانات التي أنشأناها سابقًا؛

Image

  • لدينا حالياً قاعدة بيانات [dbpersonnes] [17] وهي فارغة [18] ؛

نقوم بإنشاء مستخدم [admpersonnes] بكلمة مرور [nobody]، والذي سيتمتع بجميع الصلاحيات على قاعدة البيانات [dbpersonnes]:

Image

  • في [19]، ننتقل إلى قاعدة البيانات [dbpersonnes]؛
  • في [20]، نختار علامة التبويب [Privileges]؛
  • في [21-22]، نلاحظ أن المستخدم [root] يتمتع بجميع الصلاحيات على قاعدة البيانات [dbpersonnes]؛
  • في [23]، يتم إنشاء مستخدم جديد؛

Image

  • في [25-26]، سيكون للمستخدم المعرف [admdbpersonnes]؛
  • في [27-29]، ستكون كلمة المرور الخاصة به هي [nobody]؛
  • في [30]، يشير phpMyAdmin إلى أن كلمة المرور ضعيفة جدًا (يسهل اختراقها). في بيئة الإنتاج، يُفضل إنشاء كلمة مرور قوية باستخدام [31]؛
  • في [32]، يُشار إلى أن المستخدم [admdbpersonnes] يجب أن يتمتع بجميع الصلاحيات على قاعدة البيانات [dbpersonnes]؛
  • في [33]، يتم التحقق من صحة المعلومات المقدمة؛

Image

  • في [35]، يشير phpMyAdmin إلى أن المستخدم قد تم إنشاؤه؛
  • في [36]، الأمر SQL الذي تم إصداره على أساس؛
  • في [37]، يتمتع المستخدم [admpersonnes] بجميع الحقوق على قاعدة البيانات [dbpersonnes]؛

والآن لدينا:

  • قاعدة بيانات MySQL و [dbpersonnes]؛
  • مستخدم [admpersonnes/nobody] الذي يمتلك جميع الحقوق على قاعدة البيانات هذه؛

سنقوم بكتابة نصوص برمجية PHP لاستغلال قاعدة البيانات. يتوفر في PHP العديد من المكتبات لإدارة قواعد البيانات. سنستخدم المكتبة PDO (PHP Data Objects) التي تقع بين الكود PHP و SGBD:

Image

تسمح المكتبة PDO للبرنامج النصي PHP بالتجريد من الطبيعة الدقيقة لـ SGBD المستخدم. وبالتالي، كما هو موضح أعلاه، يمكن استبدال SGBD و MySQL بـ SGBD و PostgreSQL مع تأثير ضئيل على كود البرنامج النصي PHP. هذه المكتبة غير متوفرة بشكل افتراضي. يمكن التحقق من توفرها بالطريقة التالية:

Image

  • في [1-4]، نتحقق من الامتدادات النشطة PDO؛
  • في [5]، نرى أن الامتداد PDO لـ SGBD MySQL نشط. أما الامتدادات الأخرى فليست نشطة. يكفي النقر عليها لتفعيلها؛

هناك طريقة أخرى لتفعيل الملحق، وهي تعديل الملف [php.ini] (الفقرة "الرابط") الذي يقوم بتكوين الملف PHP مباشرةً:

Image

  • إلى [1]، يتم تنشيط الملحق PDO الخاص بـ MySQL؛
  • في [2]، يتم تعطيل امتداد PDO لـ Firebird؛

بعد تعديل الملف [php.ini]، يجب إعادة تشغيل PHP الخاص بـ Laragon حتى يتم تطبيق التعديلات.

12.2. الاتصال بقاعدة بيانات MySQL

يتم الاتصال بـ SGBD عن طريق إنشاء كائن PDO. يقبل المنشئ معلمات مختلفة:

$dbh=new PDO(string $dsn,string $user,string $passwd,array $driver_options)

معاني المعلمات هي كما يلي:

$dsn
(Data Source Name) هي سلسلة تحدد طبيعة SGBD وموقعه على الإنترنت. تشير السلسلة "mysql:host=localhost" إلى أننا نتعامل مع SGBD MySQL يعمل على الخادم المحلي. قد تتضمن هذه السلسلة معلمات أخرى، لا سيما منفذ الاستماع لـ SGBD واسم قاعدة البيانات التي نريد الاتصال بها: «mysql:host=localhost:port=3306:dbname=dbpersonnes»؛
$user
معرف المستخدم الذي يقوم بالاتصال؛
$passwd
كلمة مروره؛
$driver_options
مصفوفة خيارات لبرنامج تشغيل SGBD؛

المعلمة الأولى هي الوحيدة الإلزامية. وسيكون الكائن الذي تم إنشاؤه بهذه الطريقة هو الداعم لجميع العمليات التي يتم إجراؤها على قاعدة البيانات التي تم الاتصال بها. إذا تعذر إنشاء الكائن PDO، يتم إصدار استثناء من النوع PDOException.

فيما يلي مثال على اتصال [mysql-01.php]:


<?php

// الاتصال بقاعدة بيانات محلية MySql
// هوية المستخدم هي (admpersonnes، nobody)
const ID = "admpersonnes";
const PWD = "nobody";
const HOTE = "localhost";

try {
  // اتصال
  $dbh = new PDO("mysql:host=".HOTE, ID, PWD);
  print "Connexion réussie\n";
  // إغلاق الاتصال
  $dbh = NULL;
} catch (PDOException $e) {
  print "Erreur : " . $e->getMessage() . "\n";
  exit();
}

النتائج:

Connexion réussie

تعليقات

  • السطر 11: يتم الاتصال بـ SGBD عن طريق إنشاء كائن PDO. يتم استخدام المنشئ هنا مع المعلمات التالية:
    • سلسلة تحدد طبيعة كائن SGBD وموقعه على الإنترنت. تشير السلسلة "mysql:host=localhost" إلى أننا نتعامل مع كائن SGBD MySQL يعمل على الخادم المحلي. لم يتم تحديد المنفذ. لذا يتم استخدام المنفذ 3306 افتراضيًا. كما لم يتم تحديد اسم قاعدة البيانات أيضًا. سيتم عندئذٍ الاتصال بـ SGBD MySQL، على أن يتم اختيار قاعدة بيانات محددة لاحقًا؛
    • معرف المستخدم؛
    • وكلمة مروره؛
  • السطر 14: يتم إغلاق الاتصال عن طريق حذف الكائن PDO الذي تم إنشاؤه في البداية؛
  • السطر 15: قد يفشل الاتصال بـ SGBD. في هذه الحالة، يتم إطلاق استثناء من النوع PDOException. وهذا الاستثناء مشتق من الاستثناء PHP [RuntimeException]؛
  • السطر 16: يتم عرض رسالة الخطأ الخاصة بالاستثناء؛

لنُعيد تشغيل البرنامج النصي بعد إدخال كلمة مرور خاطئة في السطر 6. وتكون النتيجة كما يلي:


Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes'@'localhost' (using password: YES)

12.3. إنشاء جدول

يُظهر البرنامج النصي [mysql-02.php] إنشاء جدول في قاعدة بيانات:


<?php

// هوية قاعدة البيانات
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// بيانات اعتماد المستخدم
const ID = "admpersonnes";
const PWD = "nobody";

try {
  // الاتصال بقاعدة البيانات MySql
  $connexion = new PDO(DSN, ID, PWD);
  // حذف جدول «الأشخاص» إن وجد
  $sql = "drop table personnes";
  $connexion->exec($sql);
  // إنشاء جدول «الأشخاص»
  $sql = "create table personnes (prenom varchar(30) NOT NULL, nom varchar(30) NOT NULL, age integer NOT NULL, primary key(nom,prenom))";
  $connexion->exec($sql);
} catch (PDOException $ex) {
  // عرض الخطأ
  print "Erreur : " . $ex->getMessage() . "\n";
} finally {
  // يتم قطع الاتصال إذا لزم الأمر
  $connexion = NULL;
}
// النهاية
print "Terminé\n";
exit;

تعليقات

  • السطر 11: الاتصال بقاعدة البيانات. هذا هو دائمًا أول ما يجب القيام به. نتيجة الاتصال هي كائن [PDO] الذي ستتم من خلاله العمليات على قاعدة البيانات؛
  • السطر 13: الأمر SQL [drop table personnes] سيحذف الجدول [personnes] من قاعدة البيانات [dbpersonnes]. إذا لم تكن الجدولة [personnes] موجودة، فلن يحدث خطأ؛
  • السطر 14: تنفيذ الأمر SQL السابق على قاعدة البيانات [dbpersonnes]. قد يؤدي هذا التنفيذ إلى تشغيل أمر [PDOException] الذي سيتم اعتراضه في السطر 18؛
  • السطر 16: هذا الأمر SQL ينشئ جدولًا باسم [personnes]. يحتوي الجدول على صفوف وأعمدة. تشكل الأعمدة ما يُعرف ببنية الجدول. أما الصفوف فتشكل محتوى الجدول. يمكن أن تحتوي قاعدة البيانات على جدول واحد أو أكثر. سيحتوي الجدول [personnes] هنا على ثلاثة أعمدة:
    • الاسم الأول: الاسم الأول للشخص على شكل سلسلة لا تتجاوز 30 حرفًا؛
    • اللقب: لقب الشخص نفسه على شكل سلسلة لا تتجاوز 30 حرفًا؛
    • العمر: عمر الشخص على شكل عدد صحيح؛
    • يفرض السمة NOT NULL على أحد الأعمدة أن يحتوي العمود على قيمة. وعدم تعيين قيمة له يؤدي إلى حدوث خطأ [PDOException
    • يحدد [primary key(nom,prenom)] مفتاحًا أساسيًا للجدول [personnes]. يتمتع المفتاح الأساسي بقيمة فريدة لكل صف في الجدول. هنا، سيتم الحصول على المفتاح الأساسي عن طريق ربط العمودين [nom] و [prenom] في الصف. هذا القيد يعني أنه لا يمكن أن يوجد في الجدول شخصان يحملان نفس الاسم واللقب، أي شخصان متماثلان في الاسم. يؤدي إنشاء شخص يحمل نفس الاسم في الجدول إلى حدوث خطأ [PDOException]؛
  • السطر 17: تنفيذ الأمر SQL على قاعدة البيانات [dbpersonnes]؛
  • السطر 20: في حالة حدوث خطأ [PDOException]، يتم عرض رسالة الخطأ المرتبطة به؛
  • الأسطر 21-24: يتم الانتقال إلى الجملة [finally] في جميع الحالات، سواء حدث استثناء أم لا، لإغلاق الاتصال بقاعدة البيانات (السطر 23)؛

النتائج:

إذا تم تنفيذ البرنامج النصي دون أخطاء، يمكن ملاحظة وجود الجدول في phpMyAdmin:

Image

Image

  • في [3] قاعدة البيانات؛
  • في [4]، الجدول المعروض؛
  • في [5]، يتم عرض بنية الجداول في علامة التبويب [Structure]؛
  • في [6-8]، الأعمدة الثلاثة للجدول؛
  • في [9]، لا يمكن أن يكون أي من الأعمدة الثلاثة فارغًا؛

Image

  • في [10]، قائمة فهارس الجدول. يتيح الفهرس العثور في الجدول على الصفوف التي تحمل فهرسًا معينًا، بشكل أسرع مما لو تم تصفح صفوف الجدول بالتسلسل. المفتاح الأساسي هو دائمًا جزء من الفهارس، لكن الفهرس قد لا يكون مفتاحًا أساسيًا؛
  • في [11]، الفهرس هنا هو المفتاح الأساسي؛
  • في [12]، يتكون الفهرس من الأعمدة [nom, prenom] لكل سطر؛

الآن، لنرى ما يحدث إذا قمنا بإنشاء أخطاء، على التوالي في اسم قاعدة البيانات، واسم المستخدم، وكلمة المرور الخاصة به:

إذا أدخلنا اسم قاعدة بيانات غير موجودة:


Erreur : SQLSTATE[HY000] [1044] Access denied for user 'admpersonnes'@'%' to database 'dbpersonnes2'

إذا أدخلنا اسم مستخدم غير موجود:


Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes2'@'localhost' (using password: YES)

إذا أدخلنا كلمة مرور خاطئة:


Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes'@'localhost' (using password: YES)

12.4. ملء جدول

سنقوم بكتابة برنامج نصي PHP يقوم بتنفيذ الأوامر SQL الموجودة في الملف النصي [creation.txt] التالي:

drop table if exists personnes
SET NAMES 'utf8'
create table personnes (prenom varchar(30) not null, nom varchar(30) not null, age integer not null, primary key (nom,prenom))
insert into personnes (prenom, nom, age) values('Paul','Langevin',48)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70)
insert into personnes (prenom, nom, age) values ('Pierre','Nicazou',35)
insert into personnes (prenom, nom, age) values ('Géraldine','Colou',26)
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56)
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56)

تعليقات

  • لغة SQL (لغة الاستعلام الهيكلية) لا تميز بين الأحرف الكبيرة والصغيرة في الأوامر SQL؛
  • السطر 1: يتم حذف الجدول [personnes] إذا كان موجودًا؛
  • السطر 2: يُشار إلى الخادم MySQL بأنه سيتم إرسال أحرف مُشفَّرة بتنسيق UTF-8 إليه. هذا الأمر SQL الخاص بـ MySQL ضروري هنا، على سبيل المثال، للحصول على السطر 7، حرف «é» في اسم جيرالدين في قاعدة البيانات. إذا لم يتم إدراج السطر 2، فسيتم ترجمة حرف «é» إلى سلسلة من حرفين غريبين. العميل هو البرنامج النصي PHP المكتوب باستخدام Netbeans. ويقوم هذا البرنامج بترميز الملفات إلى UTF-8 و[1-4] الموضحة أدناه:

Image

  • السطر 3: إنشاء الجدول [personnes] بثلاثة أعمدة (الاسم الأول، الاسم الأخير، العمر) والمفتاح الأساسي (الاسم الأخير، الاسم الأول)؛
  • الأسطر 4-10: إدراج 7 أسطر في الجدول [personnes]؛
  • السطر 6: من المفترض أن يفشل أمر الإدراج هذا لأنه يحاول إجراء نفس الإدراج الموجود في السطر 5. من المفترض أن تمنع قيود المفتاح الأساسي هذا الإدراج: لا يمكن أن يكون هناك شخصان يحملان نفس الاسم واللقب؛
  • السطر 10: من المفترض أن تفشل عملية الإدراج هذه لأنها تحاول إجراء نفس عملية الإدراج الموجودة في السطر 9؛

النص البرمجي PHP المكلف بتنفيذ الأوامر SQL الواردة في هذا الملف النصي هو التالي [mysql-03.php]:


<?php

// هوية قاعدة البيانات
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// بيانات اعتماد المستخدم
const ID = "admpersonnes";
const PWD = "nobody";
// معرف ملف الأوامر النصي SQL المطلوب تنفيذه
const SQL_COMMANDS_FILENAME = "creation.txt";

// فتح اتصال بقاعدة البيانات MySql
try {
  $connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
  // عرض الخطأ
  print "Erreur : " . $ex->getMessage() . "\n";
  exit;
}
// نريد أن يتم إطلاق استثناء عند كل خطأ في SGBD
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// تنفيذ ملف الأوامر SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE, FALSE);
// إغلاق الاتصال
$connexion = NULL;
//عرض عدد الأخطاء
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
  print "$erreurs[$i]\n";
}

// انتهى
print "Terminé\n";
exit;

// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE, bool $arrêt = TRUE): array {
// استخدام الاتصال $connexion
// تنفيذ الأوامر SQL الموجودة في الملف النصي SQLFileName
// هذا الملف هو ملف أوامر SQL يتم تنفيذها بمعدل أمر واحد في كل سطر
// إذا كان $suivi=1، فإن كل عملية تنفيذ لأمر SQL تُعرض مع إشارة إلى نجاحها أو فشلها
// إذا كانت قيمة $arrêt تساوي 1، تتوقف الدالة عند أول خطأ تصادفه، وإلا فإنها تنفذ جميع أوامر SQL
// تُرجع الدالة مصفوفة (عدد الأخطاء، الخطأ 1، الخطأ 2...)
// يتم التحقق من وجود الملف SQLFileName

  if (!file_exists($SQLFileName)) {
    return ["Le fichier [$SQLFileName] n'existe pas"];
  }

  // تنفيذ الاستعلامات SQL الموجودة في SQLFileName
  // يتم وضعها في مصفوفة
  $requêtes = file($SQLFileName);
  // خطأ؟
  if ($requêtes === FALSE) {
    return ["Erreur lors de l'exploitation du fichier SQL [$SQLFileName]"];
  }
  // يتم تنفيذ الاستعلامات واحدًا تلو الآخر - في البداية لا توجد أخطاء
  $erreurs = [];
  $i = 0;
  $fini = FALSE;
  while ($i < count($requêtes) && !$fini) {
    // نسترد نص الاستعلام
    // ستقوم وظيفة trim بإزالة علامة نهاية السطر
    $requête = trim($requêtes[$i]);
    // استعلام فارغ؟
    if (strlen($requête) == 0) {
      // يتم تجاهل الاستعلام والانتقال إلى الاستعلام التالي
      $i++;
      continue;
    }
    try {
      // تنفيذ الاستعلام - قد يتم إلقاء استثناء
      $connexion->exec($requête);
      // هل يتم عرض الشاشة أم لا؟
      if ($suivi) {
        print "$requête : Exécution réussie\n";
      }
    } catch (PDOException $ex) {
      // حدث خطأ
      addError($erreurs, $requête, $ex->getMessage(), $suivi);
      // هل نتوقف؟
      $fini = $arrêt;
    }
    // الاستعلام التالي
    $i++;
  }
  // النتيجة
  return $erreurs;
}

function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
  // يتم إضافة رسالة خطأ
  $msg = "$requête : Erreur (" . $msg . ")";
  $erreurs[] = $msg;
  // متابعة الشاشة أم لا؟
  if ($suivi) {
    print "$msg\n";
  }
}

تعليقات

  • تتولى الدالة [exécuterCommandes] (الأسطر 36-89) تنفيذ الأوامر SQL التي تجدها في الملف النصي [$SQLFileName] (المعلمة 2). ولتنفيذها، تستخدم الدالة الاتصال المفتوح [$connexion] (المعلمة 1) مع الخادم MySQL. أما المعلمة الثالثة [$suivi] فهي قيمة منطقية تتحكم في العروض على الشاشة: في TRUE، يتم عرض الأمر SQL الذي تم تنفيذه على الشاشة مع نجاحه أو فشله، وإلا فإن تنفيذ الأمر SQL يكون صامتًا. المعلمة الرابعة [$arrêt] تتحكم في الإجراء الذي يجب اتخاذه عند فشل الأمر SQL: في TRUE، يشير إلى أنه يجب إيقاف تنفيذ الأوامر SQL، وإلا فإن التنفيذ يستمر. تعيد الدالة [exécuterCommandes] مصفوفة من رسائل الخطأ، تكون فارغة في حالة عدم وجود أخطاء؛
  • الأسطر 11-18: يتم فتح الاتصال بقاعدة البيانات MySQL [dbpersonnes]. إذا فشل الفتح، يتم عرض رسالة خطأ ويتم إيقاف العملية (الأسطر 14-18)؛
  • السطر 22: يتم تمرير الاتصال المفتوح إلى الدالة [exécuterCommandes]. وسيتم إغلاقه عند عودة الدالة (السطر 24)؛
  • السطر 20: قبل تمريرها إلى الدالة [exécuterCommandes]، يتم تكوين الاتصال. في حالة حدوث خطأ، قد تُرجع العمليات SQL التي تستخدم كائن [PDO] إما القيمة المنطقية FALSE (القيمة الافتراضية)، أو تُطلق استثناءً. السطر 20 يختار الحالة الثانية. في الواقع، من السهل «نسيان» التحقق من النتيجة المنطقية لتنفيذ أمر SQL. سيؤدي ذلك إلى حدوث خطأ لاحقًا ولكن في مكان آخر من الكود، مما يجعل تحديد مصدره الأصلي أكثر صعوبة. في حالة حدوث استثناء غير مُعالج (عدم وجود catch)، سيتتبع الاستثناء مساره صعودًا في الكود حتى يصادف catch أو حتى يصل إلى المُفسر PHP الذي سيقوم باعتراض الاستثناء. في هذه الحالة، يتم عرض طبيعة الاستثناء ومصدره في الكود؛
  • السطر 22: يتم استدعاء الدالة [exécuterCommandes] لتنفيذ ملف الأوامر SQL [$SQLFileName]؛
  • الأسطر 45-47: يتم التحقق من وجود ملف الأوامر SQL. إذا لم يكن موجودًا، يتم تسجيل الخطأ وإرجاع هذه النتيجة؛
  • السطر 51: نضع الأوامر SQL في مصفوفة [$requêtes]. الأسطر 53-55، إذا فشلت العملية، نُرجع مصفوفة أخطاء مع رسالة واحدة؛
  • السطر 57: سنقوم بتجميع الأخطاء في المصفوفة [$erreurs]؛
  • السطر 58: رقم الطلب؛
  • السطر 59: المتغير المنطقي [$fini] يتحكم في تنفيذ الأوامر SQL من المصفوفة [$requêtes]. عند الوصول إلى TRUE، يتوقف التنفيذ؛
  • السطر 60: يتم تكرار العملية على جميع الطلبات؛
  • السطر 63: يتم استخراج نص الأمر SQL رقم i. ستقوم الدالة [trim] بإزالة المسافات التي تسبق وتلي نص الأمر SQL. ويُقصد بـ «المسافات» الحرف الأبيض \b، وعلامة إرجاع العربة \r، وعلامة نهاية السطر \n، وعلامة تغيير الصفحة \f، وعلامة الجدولة \t… وما يهمنا هنا هو أن علامة نهاية السطر في النص SQL ستختفي؛
  • الأسطر 65-69: إذا كان النص SQL فارغًا، يتم تجاهل الطلب والانتقال إلى الطلب التالي؛
  • السطر 72: نرسل الأمر SQL إلى الخادم MySQL. ستطلق الطريقة [PDO::exec] استثناءً إذا فشل التنفيذ. نذكر أن هذا السلوك ناتج عن التكوين الذي تم في السطر 20؛
  • السطر 79: تُضاف رسالة الخطأ إلى مصفوفة الأخطاء؛
  • السطر 81: يتم تعيين القيمة المنطقية [$fini] التي تتحكم في الحلقة. إذا كانت قيمة المعلمة [$arrêt] (السطر 36) تساوي TRUE، فيجب إيقاف الحلقة؛
  • الأسطر 74-76: إذا نجح تنفيذ الأمر SQL، يتم عرضه على الشاشة إذا كانت قيمة المعلمة [$suivi] (السطر 36) هي TRUE؛
  • السطر 87: بمجرد تنفيذ جميع الأوامر SQL، يتم إرجاع مصفوفة الأخطاء [$erreurs

تسمح الدالة [adError] في الأسطر 90-97 بإضافة خطأ إلى مصفوفة الأخطاء [$erreurs]:

  • السطر 90: تتلقى الدالة 4 معلمات:
    • يتم تمرير المعلمة [$erreurs] كمرجع. فنحن نريد في الواقع العمل على المصفوفة التي تم تمريرها كمعلمة وليس على نسخة منها؛
    • المعلمة [$requête] هي النص SQL للأمر الذي فشل؛
    • المعلمة [$msg] هي رسالة الخطأ المرتبطة بالأمر الذي فشل؛
    • تشير القيمة المنطقية [$suivi] إلى ما إذا كان يجب عرض رسالة الخطأ ($suivi=TRUE) أم لا ($suivi=FALSE) على وحدة التحكم؛

يتم استدعاء الدالة [exécuterCommandes] بواسطة البرنامج النصي في الأسطر 3-33:

  • الأسطر 11-18: يتم إجراء اتصال بقاعدة البيانات MySQL [dbpersonnes]؛
  • السطر 20: يتم تكوين الاتصال؛
  • السطر 22: يتم بعد ذلك تنفيذ ملف الأوامر SQL؛
  • السطر 24: يتم إغلاق الاتصال؛
  • الأسطر 26-29: يتم عرض الأخطاء التي أرجعتها الدالة [exécuterCommandes]؛

نتائج الشاشة:


drop table if exists personnes : Exécution réussie
SET NAMES 'utf8' : Exécution réussie
create table personnes (prenom varchar(30) not null, nom varchar(30) not null, age integer not null, primary key (nom,prenom)) : Exécution réussie
insert into personnes (prenom, nom, age) values('Paul','Langevin',48) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Lefur-Sylvie' for key 'PRIMARY')
insert into personnes (prenom, nom, age) values ('Pierre','Nicazou',35) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Géraldine','Colou',26) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Girond-Paulette' for key 'PRIMARY')

-----------------------
Il y a eu 2 erreur(s)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Lefur-Sylvie' for key 'PRIMARY')
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Girond-Paulette' for key 'PRIMARY')
Terminé

يمكن رؤية الإدخالات التي تم إجراؤها باستخدام phpMyAdmin:

Image

12.5. تنفيذ أي أوامر SQL

يُظهر البرنامج النصي التالي تنفيذ الأوامر SQL من الملف النصي [sql.txt] التالي:

1
2
3
4
5
6
7
8
9
select * from personnes
select nom,prenom from personnes order by nom asc, prenom desc
select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
insert into personnes values('Josette','Bruneau',46)
update personnes set age=47 where nom='Bruneau'
select * from personnes where nom='Bruneau'
delete from personnes where nom='Bruneau'
select * from personnes where nom='Bruneau'
xselect * from personnes where nom='Bruneau'

من بين هذه الأوامر SQL، هناك الأمر select الذي يعرض نتائج من قاعدة البيانات، والأوامر insert و update و delete التي تعدل قاعدة البيانات دون عرض نتائج، وأخيرًا الأوامر الخاطئة مثل الأمر الأخير (xselect). النص البرمجي [mysql-04.php] هو التالي:


<?php

// هوية قاعدة البيانات
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// بيانات اعتماد المستخدم
const ID = "admpersonnes";
const PWD = "nobody";
// هوية ملف نصي الأوامر SQL المطلوب تنفيذها
const SQL_COMMANDS_FILENAME = "sql.txt";

try {
  // الاتصال بقاعدة البيانات MySql
  $connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
  // عرض الخطأ
  print "Erreur : " . $ex->getMessage() . "\n";
  exit;
}
// نريد أن يتم إطلاق استثناء عند كل خطأ في SGBD
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// تنفيذ ملف الأوامر SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE, FALSE);
// إغلاق الاتصال
$connexion = NULL;
//عرض عدد الأخطاء
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
  print "$erreurs[$i]\n";
}

// انتهى الأمر
print "Terminé\n";
exit;

// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE, bool $arrêt = TRUE): array {
………………………………………………………….
  // يتم تنفيذ الاستعلامات واحدًا تلو الآخر - في البداية لا توجد أخطاء
  $erreurs = [];
  $i = 0;
  $fini = FALSE;
  while ($i < count($requêtes) && !$fini) {
    // استرداد نص الطلب
    // ستقوم وظيفة trim بإزالة علامة نهاية السطر
    $requête = trim($requêtes[$i]);
    // استعلام فارغ؟
    if (strlen($requête) == 0) {
      // يتم تجاهل الاستعلام والانتقال إلى الاستعلام التالي
      $i++;
      continue;
    }
    // تنفيذ الطلب
    // استرداد الاسم
    $commande = "";
    if (preg_match("/^\s*(\S+)/", $requête, $champs)) {
      $commande = strtolower($champs[0]);
    }
    try {
      // هل هذا أمر SELECT؟
      if ($commande === "select") {
        $résultat = $connexion->query($requête);
      } else {
        $résultat = $connexion->exec($requête);
      }
      // هل يتم تتبع الشاشة أم لا؟
      if ($suivi) {
        print "[$requête] : Exécution réussie\n";
      }
      // عرض نتيجة التنفيذ
      afficherInfos($commande, $résultat);
    } catch (PDOException $ex) {
      // حدث خطأ
      addError($erreurs, $requête, $ex->getMessage(), $suivi);
      // هل نتوقف؟
      $fini = $arrêt;
    }
    // الطلب التالي
    $i++;
  }
  // النتيجة
  return $erreurs;
}

function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
  
}

// ---------------------------------------------------------------------------------
function afficherInfos(string $commande, $résultat): void {
  // يعرض النتيجة $résultat لطلب sql
  // هل كان هذا استعلام SELECT؟
  switch ($commande) {
    case "select" :
      // يتم عرض أسماء الحقول
      $titre = "";
      $nbColonnes = $résultat->columnCount();
      for ($i = 0; $i < $nbColonnes; $i++) {
        $infos = $résultat->getColumnMeta($i);
        $titre .= $infos['name'] . ",";
      }
      // يتم حذف الحرف الأخير ،
      $titre = substr($titre, 0, strlen($titre) - 1);
      // عرض قائمة الحقول
      print "$titre\n";
      // خط فاصل
      $séparateurs = "";
      for ($i = 0; $i < strlen($titre); $i++) {
        $séparateurs .= "-";
      }
      print "$séparateurs\n";
      // البيانات
      foreach ($résultat as $ligne) {
        $data = "";
        for ($i = 0; $i < $nbColonnes; $i++) {
          $data .= $ligne[$i] . ",";
        }
        // إزالة الحرف الأخير ،
        $data = substr($data, 0, strlen($data) - 1);
        // يتم عرض
        print "$data\n";
      }
      break;
    case "update":
    case "insert":
    case "delete";
      print " $résultat lignes(s) a (ont) été modifiée(s)\n";
      break;
  }
}

تعليقات

  • الأسطر 36-83: تم تعديل الدالة [exécuterCommandes] بشكل طفيف: لا يتم تنفيذ الأمر SQL [select] بنفس الطريقة التي يتم بها تنفيذ الأوامر الأخرى SQL. هذا الأمر هو الوحيد الذي يُرجع جدولًا كنتيجة، أي مجموعة من الصفوف والأعمدة من قاعدة البيانات؛
  • السطور 55-57: يتم عزل الكلمة الأولى من الأمر SQL باستخدام تعبير منتظم؛
  • الأسطر 60-64: إذا كان الأمر SQL هو [select]، يتم استخدام الطريقة [PDO::queryوإلا يتم استخدام الطريقة [PDO::exec] لتنفيذ الأمر SQL. في كلتا الحالتين، إذا فشل التنفيذ، سيتم إطلاق استثناء واعتراضه في الأسطر 71-77. إذا نجح التنفيذ، تعرض السطر 70 نتيجته؛
  • الأسطر 90-130: تعرض الدالة afficherInfos معلومات حول نتيجة تنفيذ أمر SQL؛
  • السطر 94: يتم معالجة حالة الأمر [select]. ونتيجته هي كائن من النوع [PDOStatement
  • السطر 96: تُرجع الطريقة [PDOStatement::getColumnCount()] عدد أعمدة الجدول الناتج عن الأمر select؛
  • السطران 98-99: تُرجع الطريقة [PDOStatement::getMeta(i)] قاموسًا يحتوي على معلومات عن العمود رقم i في الجدول الناتج عن select.. في هذا القاموس، القيمة المرتبطة بالمفتاح 'name' هي اسم العمود؛
  • الأسطر 97-102: يتم ربط أسماء أعمدة جدول النتائج الخاص بـ select في سلسلة أحرف واحدة؛
  • الأسطر 105-110: يتم إنشاء سطر فاصل بنفس طول سلسلة الأحرف التي تم إنشاؤها سابقًا؛
  • الأسطر 112-121: يمكن تصفح كائن من النوع PDOStatement باستخدام حلقة foreach. في كل تكرار، يكون العنصر الناتج عبارة عن سطر من جدول نتائج select في شكل مصفوفة من القيم تمثل قيم الأعمدة المختلفة في السطر. يتم عرض جميع هذه القيم باستخدام حلقة for (الأسطر 114-116)؛
  • الأسطر 123-127: نتيجة تنفيذ الأمر insert، update، delete هي عدد الأسطر التي تم تعديلها بواسطة الأمر؛

نتائج الشاشة:


[set names 'utf8'] : Exécution réussie
[select * from personnes] : Exécution réussie
prenom,nom,age
--------------
Géraldine,Colou,26
Paulette,Girond,56
Paul,Langevin,48
Sylvie,Lefur,70
Pierre,Nicazou,35
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom,prenom
----------
Colou,Géraldine
Girond,Paulette
Langevin,Paul
Lefur,Sylvie
Nicazou,Pierre
[select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
prenom,nom,age
--------------
Pierre,Nicazou,35
Géraldine,Colou,26
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,47
[delete from personnes where nom='Bruneau'] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)

-----------------------
Il y a eu 1 erreur(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
Terminé

12.6. استخدام الأوامر المعدة مسبقًا SQL

12.6.1. مثال 1

لنستعرض البرنامج النصي التالي [mysql-05.php]:


<?php

// معرف قاعدة البيانات
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// معرفات المستخدم
const ID = "admpersonnes";
const PWD = "nobody";

try {
  // الاتصال بقاعدة البيانات MySql
  $connexion = new PDO(DSN, ID, PWD);
  // نريد أن يتم إطلاق استثناء عند كل خطأ في SGBD
  $connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  // يتم إفراغ جدول الأشخاص
  $connexion->exec("delete from personnes");
  // قائمة بالأشخاص
  $personnes = [];
  $personnes[] = ["nom" => "Langevin", "prenom" => "Paul", "age" => 47];
  $personnes[] = ["nom" => "Lefur", "prenom" => "Sylvie", "age" => 28];
  // سنقوم بإدراج هؤلاء الأشخاص في قاعدة البيانات
  $statement = $connexion->prepare("insert into personnes (nom, prenom, age) values (:nom, :prenom, :age)");
  for ($i = 0; $i < count($personnes); $i++) {
    $statement->execute($personnes[$i]);
  }
} catch (PDOException $ex) {
  // عرض الخطأ
  print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// إغلاق الاتصال
  $connexion = NULL;
}

// انتهى الأمر
print "Terminé\n";
exit;

تعليقات

نركز هنا على الأسطر من 16 إلى 24 التي تضيف شخصين إلى جدول الأشخاص في قاعدة البيانات [dbpersonnes].

  • السطر 21: يتم «إعداد» أمر SQL مع تحديد المعلمات. وتسبق المعلمات الرموز التالية: :اللقب، :الاسم الأول، :العمر. لـ«إعداد» أمر SQL، نستخدم الطريقة [PDO::prepare]. والنتيجة هي نوع [PDOStatement]. «الإعداد» ليس تنفيذًا: لا يتم تنفيذ أي شيء؛
  • السطر 23: تنفيذ الأمر «المُعد» باستخدام الطريقة [PDOStatement::execute]. للقيام بذلك، يجب تعيين قيم للمعلمات :الاسم، :الاسم الأول و:العمر. هناك عدة طرق للقيام بذلك. نستخدم هنا قاموسًا يحتوي على مفاتيح هي معلمات الأمر المُعد الذي نمرره إلى الطريقة [PDOStatement::execute]. هناك طريقة أخرى للقيام بذلك وهي إعطاء المعلمات قيمة باستخدام الطريقة [PDOStatement::bindValue($paramètre,$valeur)]. على سبيل المثال هنا:

$statement→bindValue(“nom”,”Langevin”);
$statement→bindValue(“prenom”,”Paul”);
$statement→bindValue(“age”,47);
$statement→execute();

العيب هو أنه يتعين تكرار هذه التعليمات لكل معلمة. لذا قد تكون طريقة القاموس أكثر عملية. الطريقة [PDOStatement::execute] تُنتج FALSE في حالة فشل التنفيذ؛

  • الطريقة المستخدمة هنا لإجراء الإدخالات:
    • طريقة مُعدة مسبقًا؛
    • n عمليات تنفيذ للأمر المُعد مسبقًا؛

تكون أكثر توفيرًا لوقت التنفيذ مقارنة بتنفيذ n أوامر SQL مختلفة. لذا يُفضل استخدام هذه الطريقة. وهي قابلة للاستخدام مع أوامر SQL select و update و delete و insert. في حالة الأمر SQL select، بعد تنفيذه باستخدام [PDOStatement::execute]، يتم استرداد صفوف النتيجة باستخدام الطريقة [PDOStatement::fetchAll

12.6.2. مثال 2

يوضح البرنامج النصي التالي [mysql-06.php] استخدام أمر مُعد مسبقًا لعملية الاختيار SQL، بالإضافة إلى طرق متنوعة لاسترداد السطور التي تُرجعها هذه العملية:


<?php

// هوية قاعدة البيانات
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// بيانات اعتماد المستخدم
const ID = "admpersonnes";
const PWD = "nobody";

try {
  // الاتصال بقاعدة البيانات MySql
  $connexion = new PDO(DSN, ID, PWD);
  // نريد أن يتم إطلاق استثناء عند كل خطأ في SGBD
  $connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  // نقوم بإفراغ جدول الأشخاص
  $connexion->exec("delete from personnes");
  // سنقوم بإدراج هؤلاء الأشخاص في قاعدة البيانات
  $statement = $connexion->prepare("insert into personnes (nom, prenom, age) values (:nom, :prenom, :age)");
  for ($i = 0; $i < 10; $i++) {
    $statement->execute(["nom" => "nom" . $i, "prenom" => "prenom" . $i, "age" => $i * 10]);
  }
  // نستعلم عن قاعدة البيانات
  $statement = $connexion->prepare("select nom, prenom, age from personnes");
  $statement->execute();
  // السطر الأول
  $ligne = $statement->fetch();
  var_dump($ligne);
  // السطر الثاني
  $ligne = $statement->fetch(PDO::FETCH_ASSOC);
  var_dump($ligne);
  // السطر الثالث
  $ligne = $statement->fetch(PDO::FETCH_OBJ);
  var_dump($ligne);
  // السطر الرابع
  $statement->setFetchMode(PDO::FETCH_CLASS, "Person");
  $ligne = $statement->fetch();
  var_dump($ligne);
  // قراءة متسلسلة لجميع الأسطر
  $statement = $connexion->prepare("select nom, prenom, age from personnes");
  $statement->execute();
  $statement->setFetchMode(PDO::FETCH_CLASS, "Person");
  while ($personne = $statement->fetch()) {
    print "$personne\n";
  }
} catch (PDOException $ex) {
  // عرض الخطأ
  print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// إغلاق الاتصال
  $connexion = NULL;
}

// انتهى
print "Terminé\n";
exit;

class Person {
  private $nom;
  private $prenom;
  private $age;

  public function __toString() {
    return "Personne[$this->nom,$this->prenom,$this->age]";
  }

}

تعليقات

  • الأسطر 17-20: يتم إدراج 10 أسطر في الجدول [personnes] التابع لقاعدة البيانات [admpersonnes]:

Image

  • السطر 22: يتم «إعداد» أمر SQL [select] الذي يتم تنفيذه في السطر 23؛
  • السطر 25: يتم استرداد سطر من نتيجة العملية SQL [select] التي تم تنفيذها باستخدام الطريقة [PDOStatement::fetch]. يمكن للطريقة [PDOStatement::fetch] استرداد الأسطر الناتجة عن عملية SQL [select] المعدة مسبقًا بطرق متنوعة. يعرض البرنامج النصي بعضًا منها. تعرض الطريقة [PDOStatement::fetch] بدون معلمات السطر الحالي من العملية [select] في شكل قاموس مفهرس حسب أرقام الأعمدة وأسمائها؛
  • السطر 26: يعرض النتيجة التالية:

array(6) {
  ["nom"]=>
  string(4) "nom0"
  [0]=>
  string(4) "nom0"
  ["prenom"]=>
  string(7) "prenom0"
  [1]=>
  string(7) "prenom0"
  ["age"]=>
  string(1) "0"
  [2]=>
  string(1) "0"
}
  • السطران 28-29: المعلمة [PDO::FETCH_ASSOC] تجعل السطر الذي يتم إرجاعه عبارة عن قاموس مفهرس بأسماء أعمدة الجدول:
1
2
3
4
5
6
7
8
array(3) {
  ["nom"]=>
  string(4) "nom1"
  ["prenom"]=>
  string(7) "prenom1"
  ["age"]=>
  string(2) "10"
}
  • السطران 31-32: المعلمة [PDO::FETCH_OBJ] تجعل السطر الذي يتم إرجاعه كائنًا من النوع [stdclass]، حيث تكون سماته هي أسماء أعمدة الجدول:
1
2
3
4
5
6
7
8
object(stdClass)#2 (3) {
  ["nom"]=>
  string(4) "nom2"
  ["prenom"]=>
  string(7) "prenom2"
  ["age"]=>
  string(2) "20"
}
  • السطر 34: يتم تعيين وضع البحث الخاص بالطريقة [fetch] باستخدام الطريقة [PDOStatement::setFetchMode]. ويصبح هذا الوضع هو الوضع الافتراضي ما لم يتم تغييره إما عن طريق عملية أخرى [PDOStatement::setFetchMode] أو عن طريق تمرير الوضع كمعلمة إلى الدالة [PDOStatement::fetch] كما تم سابقًا. تشير العملية [setFetchMode(PDO::FETCH_CLASS, "Person")] إلى أن السطر الذي تمت قراءته يجب وضعه في كائن من النوع [Person]. يجب أن تحتوي هذه الفئة، ضمن سماتها، على سمات تحمل أسماء أعمدة السطر الذي تمت قراءته. وهذا هو الحال بالنسبة للفئة [Person] المُعرَّفة في الأسطر 56-63؛
  • يعرض السطر 36 النتيجة التالية:
1
2
3
4
5
6
7
8
object(Person)#4 (3) {
  ["nom":"Person":private]=>
  string(4) "nom3"
  ["prenom":"Person":private]=>
  string(7) "prenom3"
  ["age":"Person":private]=>
  string(2) "30"
}
  • الأسطر 38-43: توضح كيفية استغلال نتائج [select] بالتسلسل؛
  • السطر 42: سيستخدم عرض [$personne] الطريقة [__toString] الخاصة بالفئة [Person]؛

12.7. استخدام المعاملات

تسمح المعاملة بتجميع تسلسل من الأوامر SQL في وحدة تنفيذ واحدة: إما أن تنجح جميع الأوامر أو تفشل إحداها، وعندها يتم إلغاء جميع الأوامر SQL التي سبقتها. بعبارة أخرى، عند استخدام معاملة لتنفيذ الأوامر SQL، تصبح قاعدة البيانات بعد تنفيذ المعاملة في حالة مستقرة:

  • إما في حالة جديدة ناتجة عن التنفيذ الناجح لجميع الأوامر SQL الخاصة بالمعاملة؛
  • إما في الحالة التي كانت عليها قبل بدء تنفيذ المعاملة؛

سنستأنف مثال تنفيذ الأوامر SQL الواردة في ملف نصي تمت دراسته في الفقرة «الرابط». سنقوم بتضمين هذا التنفيذ في معاملة. وستكون الأوامر SQL مضمنة في الملف [sql2.txt] التالي:


set names 'utf8'
select * from personnes
select nom,prenom from personnes order by nom asc, prenom desc
select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
insert into personnes values('Josette','Bruneau',46)
update personnes set age=47 where nom='Bruneau'
select * from personnes where nom='Bruneau'
delete from personnes where nom='Bruneau'
select * from personnes where nom='Bruneau'
insert into personnes values('Josette','Bruneau',46)
select * from personnes where nom='Bruneau'
xselect * from personnes where nom='Bruneau'

سيؤدي الأمر الخاطئ في السطر 12 إلى فشل المعاملة بأكملها. لذلك، يجب أن نجد قاعدة البيانات كما كانت قبل المعاملة. في المثال أعلاه، لا ينبغي أن نرى في الجدول السطر الذي تم إدراجه بواسطة السطر 10 أعلاه. لم يتغير البرنامج النصي كثيرًا. ومع ذلك، نعيد عرض كامل كود [mysql-07.php]:


<?php

// هوية قاعدة البيانات
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// بيانات تعريف المستخدم
const ID = "admpersonnes";
const PWD = "nobody";
// هوية ملف نصي الأوامر SQL المطلوب تنفيذه
const SQL_COMMANDS_FILENAME = "sql2.txt";

try {
  // الاتصال بقاعدة البيانات MySql
  $connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
  // عرض الخطأ
  print "Erreur : " . $ex->getMessage() . "\n";
  exit;
}
// نريد أن يتم إطلاق استثناء عند كل خطأ في SGBD
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// تنفيذ ملف الأوامر SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE);
// إغلاق الاتصال
$connexion = NULL;
//عرض عدد الأخطاء
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
  print "$erreurs[$i]\n";
}

// انتهى
print "Terminé\n";
exit;

// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE): array {
// استخدام الاتصال $connexion
// تنفيذ الأوامر SQL الموجودة في الملف النصي SQLFileName
// هذا الملف هو ملف أوامر SQL يتم تنفيذها بمعدل أمر واحد في كل سطر
// يتم تنفيذ الأوامر SQL ضمن معاملة واحدة
// إذا فشل أحد الأوامر، يتم إلغاء المعاملة وتعود قاعدة البيانات إلى الحالة التي كانت عليها قبل المعاملة
// إذا كان $suivi=1، فإن كل عملية تنفيذ لأمر SQL تُعرض مع إشارة إلى نجاحها أو فشلها
// تُرجع الدالة مصفوفة (عدد الأخطاء، الخطأ 1، الخطأ 2…)
//
// يتم التحقق من وجود الملف SQLFileName
  if (!file_exists($SQLFileName)) {
    return ["Le fichier [$SQLFileName] n'existe pas"];
  }
  // تنفيذ الاستعلامات SQL الموجودة في SQLFileName
  // نضعها في جدول
  $requêtes = file($SQLFileName);
  // هل هناك خطأ؟
  if ($requêtes === FALSE) {
    return ["Erreur lors de l'exploitation du fichier SQL [$SQLFileName]"];
  }
  // سيتم وضع الاستعلامات في معاملة
  $connexion->beginTransaction();
  // يتم تنفيذ الاستعلامات واحدًا تلو الآخر - في البداية لا توجد أخطاء
  $erreurs = [];
  $i = 0;
  $fini = FALSE;
  while ($i < count($requêtes) && !$fini) {
    // نسترد نص الاستعلام
    // ستقوم وظيفة trim بإزالة علامة نهاية السطر
    $requête = trim($requêtes[$i]);
    // استعلام فارغ؟
    if (strlen($requête) == 0) {
      // يتم تجاهل الاستعلام والانتقال إلى الاستعلام التالي
      $i++;
      continue;
    }
    // تنفيذ الاستعلام
    // استرداد الاسم
    $commande = "";
    if (preg_match("/^\s*(\S+)/", $requête, $champs)) {
      $commande = strtolower($champs[0]);
    }
    try {
      // هل هذا أمر SELECT؟
      if ($commande === "select") {
        $résultat = $connexion->query($requête);
      } else {
        $résultat = $connexion->exec($requête);
      }
      // هل يتم تتبع الشاشة أم لا؟
      if ($suivi) {
        print "[$requête] : Exécution réussie\n";
      }
      // عرض نتيجة التنفيذ
      afficherInfos($commande, $résultat);
    } catch (PDOException $ex) {
      // حدث خطأ
      addError($erreurs, $requête, $ex->getMessage(), $suivi);
      // التوقف عند الدورة التالية
      $fini = TRUE;
    }
    // الطلب التالي
    $i++;
  }
  // نهاية المعاملة
  if (!$fini) {
    // لم تحدث أخطاء: يتم المصادقة على المعاملة
    $connexion->commit();
  } else {
    // حدثت أخطاء: يتم إلغاء المعاملة
    $connexion->rollBack();
    // إضافة خطأ
    addError($erreurs, "", "Transaction annulée", $suivi);
  }
  // النتيجة
  return $erreurs;
}

function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
  
}

// ---------------------------------------------------------------------------------
function afficherInfos(string $commande, $résultat): void {
  
}

تعليقات

لقد قمنا بتمييز التعديلات التي أُجريت على البرنامج النصي الأصلي [mysql-04.php].

  • السطران 22 و36: فقدت الدالة [exécuterCommandes] معلمتها الرابعة [$arrêt=TRUE]. وبالفعل، نظرًا لأن الأوامر SQL تُنفَّذ ضمن معاملة، فإن أي خطأ سيؤدي إلى إيقاف المعاملة؛
  • السطران 40-41: استدعاء وظيفة المعاملة؛
  • السطر 57: نبدأ معاملة. من الآن فصاعدًا، يتم تنفيذ أي أمر SQL في حلقة الأسطر 62-99 ضمن هذه المعاملة؛
  • السطور 101-109: تكون القيمة المنطقية [$fini] هي TRUE في حالة حدوث خطأ (السطر 95). وعندما تكون قيمتها FALSE، فهذا يعني عدم وجود أخطاء، وبالتالي يتم المصادقة على المعاملة (السطر 103). وعندما تكون القيمة هي TRUE، فهذا يعني حدوث أخطاء، وعندها يتم إلغاء المعاملة (السطر 106) وإضافة خطأ المعاملة إلى قائمة الأخطاء (السطر 108)؛

النتائج

قبل تنفيذ البرنامج النصي، تكون قاعدة البيانات [admpersonnes] في الحالة التالية:

Image

يتم تنفيذ البرنامج النصي [mysql-07.php]. وعندئذٍ تظهر الشاشات التالية:


[set names 'utf8'] : Exécution réussie
[select * from personnes] : Exécution réussie
prenom,nom,age
--------------
prenom0,nom0,0
prenom1,nom1,10
prenom2,nom2,20
prenom3,nom3,30
prenom4,nom4,40
prenom5,nom5,50
prenom6,nom6,60
prenom7,nom7,70
prenom8,nom8,80
prenom9,nom9,90
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom,prenom
----------
nom0,prenom0
nom1,prenom1
nom2,prenom2
nom3,prenom3
nom4,prenom4
nom5,prenom5
nom6,prenom6
nom7,prenom7
nom8,prenom8
nom9,prenom9
[select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
prenom,nom,age
--------------
prenom4,nom4,40
prenom3,nom3,30
prenom2,nom2,20
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,47
[delete from personnes where nom='Bruneau'] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
 1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,46
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
[] : Erreur (Transaction annulée)

-----------------------
Il y a eu 2 erreur(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
[] : Erreur (Transaction annulée)
Terminé
  • السطر 53: يحدث خطأ في الأمر [xselect]؛
  • السطر 54: يتم عندئذٍ إلغاء المعاملة؛

إذا تحققنا من حالة قاعدة البيانات، نجدها في نفس الحالة التي كانت عليها قبل تنفيذ البرنامج النصي. ولا نرى على وجه الخصوص السطر [Josette, Bruneau, 46] من السطر 52 من النتائج أعلاه.

Image

ملخص

  • تبدأ المعاملة باستخدام الطريقة [PDO::beginTransaction]؛
  • ويتم إنهاء المعاملة بنجاح باستخدام الطريقة [PDO::commit
  • يتم إنهاؤها بفشل باستخدام الطريقة [PDO::rollback]؛

عند استخدام قاعدة بيانات، من الأفضل وضع أي عملية SQL ضمن معاملة لعزلها عن مستخدمي قاعدة البيانات الآخرين (وهذا هو دورها أيضًا). يجب أن تكون المعاملة أقصر ما يمكن. لذلك، لا تنسَ إنهاءها بـ [commit] أو [rollback] حسب الحالة.