3. مقدمة في برمجة الويب في PHP
3.1. برمجة PHP
تجدر الإشارة هنا إلى أن PHP هي لغة برمجة قائمة بذاتها، وأنه على الرغم من استخدامها بشكل أساسي في مجال تطوير تطبيقات الويب، إلا أنه يمكن استخدامها في سياقات أخرى. يقدم المستند «PHP من خلال الأمثلة» المتاح على الرابط http://shiva.istia.univ-angers.fr/~tahe/pub/php/php.pdf أساسيات هذه اللغة. ونفترض هنا أن هذه الأساسيات قد تم إتقانها. دعونا نوضح من خلال مثال بسيط إجراءات تشغيل برنامج PHP على نظام ويندوز. تم حفظ الكود التالي تحت الاسم coucou.php.
يتم تشغيل هذا البرنامج في نافذة DOS في نظام ويندوز:
dos>"e:\program files\easyphp\php\php.exe" coucou.php
X-Powered-By: PHP/4.3.0-dev
Content-type: text/html
coucou
تجدر الإشارة إلى أن المترجم PHP يرسل افتراضيًا:
- المترجم PHP هو php.exe ويوجد عادةً في مجلد <php> الخاص بتثبيت البرنامج.
- رؤوس HTTP X-Powered-By و Content-type:
- السطر الفارغ الذي يفصل الرؤوس HTTP عن بقية المستند
- المستند المكون هنا من النص الذي كتبته الدالة echo
3.2. ملف تكوين المترجم PHP
يتم ضبط سلوك المترجم PHP بواسطة ملف تكوين يُسمى php.ini ويُخزَّن، في نظام ويندوز، في مجلد ويندوز نفسه. وهو ملف كبير الحجم، حيث يبلغ عدد أسطره في نظام ويندوز، وبالنسبة للإصدار 4.2 من PHP، ما يقرب من 1000 سطر، ولحسن الحظ فإن ثلاثة أرباعها عبارة عن تعليقات. دعونا نلقي نظرة على بعض سمات التكوين الخاصة بـ PHP:
يسمح بتضمين التعليمات بين العلامات <? >. أما في off، فيجب تضمينها بين <?php ... > | |
في on، يسمح باستخدام صيغة <% =variable %> المستخدمة في تقنية ASP (Active Server Pages) | |
يسمح بإرسال الرأس HTTP X-Powered-By: PHP/4.3.0-dev. في off، يتم حذف هذا الرأس. | |
يحدد نطاق تتبع الأخطاء. هنا، سيتم الإبلاغ عن جميع الأخطاء (E_ALL) باستثناء التحذيرات أثناء التشغيل (~E_NOTICE) | |
في on، تضع الأخطاء في التدفق HTML المرسَل إلى العميل. وبالتالي، يتم عرضها في المتصفح. يُنصح بتعيين هذا الخيار على off. | |
سيتم تخزين الأخطاء في ملف | |
يخزن آخر خطأ حدث في المتغير $php_errormsg | |
تعيين ملف تخزين الأخطاء (إذا كان log_errors=on) | |
في on، يصبح عدد معين من المتغيرات عالمية. يُعتبر ذلك ثغرة أمنية. | |
يُنشئ افتراضيًا الرأس HTTP: Content-type: text/html | |
قائمة المجلدات التي سيتم استكشافها بحثًا عن الملفات المطلوبة بواسطة التوجيهات include أو require | |
الدليل الذي سيتم فيه حفظ الملفات التي تسجل الجلسات المختلفة الجارية. القرص المعني هو الذي تم تثبيت PHP عليه. هنا يشير /temp إلى e:\temp |
يؤثر ملف التكوين هذا على قابلية نقل البرنامج المكتوب بلغة PHP. في الواقع، إذا كان على تطبيق ويب استرداد قيمة حقل C من نموذج ويب، فيمكنه القيام بذلك بطرق مختلفة اعتمادًا على ما إذا كانت متغير التكوين register_globals له القيمة on أو off:
- off: سيتم استرداد القيمة بواسطة $HTTP_GET_VARS["C"] أو _GET["C"] أو $HTTP_POST_VARS["C"] أو $_POST["C"] وفقًا للطريقة (GET/POST) التي يستخدمها العميل لإرسال قيم النموذج
- : كما هو مذكور أعلاه بالإضافة إلى $C، لأن قيمة الحقل C أصبحت عامة في متغير يحمل نفس اسم الحقل
إذا كتب مطور برنامجًا باستخدام الترميز $C لأن خادم الويب/PHP الذي يستخدمه يعيّن المتغير register_globals إلى on، فلن يعمل هذا البرنامج بعد ذلك إذا تم نقله إلى خادم ويب/PHP حيث تكون هذه المتغير نفسه هو off. لذلك، يجب السعي لكتابة البرامج مع تجنب استخدام الميزات التي تعتمد على تكوين خادم الويب/PHP.
3.3. تكوين PHP عند التشغيل
لتحسين قابلية نقل برنامج PHP، يمكننا تعيين بعض متغيرات التكوين الخاصة بـ PHP بأنفسنا. يتم تعديل هذه المتغيرات أثناء تشغيل البرنامج ولصالحه فقط. وهناك دالتان مفيدتان في هذه العملية:
تُرجع قيمة متغير التكوين confVariable | |
تحدد قيمة متغير التكوين confVariable |
فيما يلي مثال يتم فيه تعيين قيمة متغير التكوين track_errors:
<?php
// valeur de la variable de configuration track_errors
echo "track_errors=".ini_get("track_errors")."\n";
// changement de cette valeur
ini_set("track_errors","off");
// vérification
echo "track_errors=".ini_get("track_errors")."\n";
?>
عند التنفيذ، نحصل على النتائج التالية:
E:\data\serge\web\php\poly\intro>"E:\Program Files\EasyPHP\php\php.exe" conf1.php
Content-type: text/html
track_errors=1
track_errors=off
كانت قيمة متغير التكوين track_errors في البداية 1 (~on). تم تغييرها إلى off. تجدر الإشارة إلى أنه إذا كان تطبيقنا يعتمد على قيم معينة لمتغيرات التكوين، فمن الأفضل تهيئة هذه المتغيرات داخل البرنامج نفسه.
3.4. سياق تنفيذ الأمثلة
سيتم تنفيذ الأمثلة الواردة في هذه النشرة باستخدام التكوين التالي:
- PC على نظام التشغيل Windows 2000
- خادم Apache 1.3
- PHP 4.3
يتم تكوين خادم Apache في الملف httpd.conf. تُوجِّه الأسطر التالية Apache إلى تحميل PHP كوحدة مدمجة في Apache، وإحالة أي طلب يستهدف مستندًا يحمل امتدادات معينة، بما في ذلك .php، إلى المُترجم PHP. وهذا هو اللاحقة الافتراضية التي سنستخدمها لبرامجنا المكتوبة بلغة PHP.
LoadModule php4_module "E:/Program Files/EasyPHP/php/php4apache.dll"
AddModule mod_php4.c
AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc
بالإضافة إلى ذلك، قمنا بتعريف اسم مستعار متعدد لـ Apache:
Alias "/poly/" "e:/data/serge/web/php/poly/"
<Directory "e:/data/serge/web/php/poly">
Options Indexes FollowSymLinks Includes
AllowOverride All
#Order allow,deny
Allow from all
</Directory>
لنسمي المسار <poly> باسم e:/data/serge/web/php/poly. إذا أردنا أن نطلب المستند doc.php من خادم Apache باستخدام متصفح، فسنستخدم عنوان http://localhost/poly/doc.php. سيتعرف خادم Apache في URL على الاسم المستعار poly، ومن ثم سيربط URL /poly/doc.php بالملف <poly>\doc.php.
3.5. مثال أول
لنكتب أول تطبيق ويب/PHP. يتم حفظ النص التالي في الملف heure.php:
<html>
<head>
<title>Une page php dynamique</title>
</head>
<body>
<center>
<h1>Une page PHP générée dynamiquement</h1>
<h2>
<?php
$maintenant=time();
echo date("j/m/y, h:i:s",$maintenant);
?>
</h2>
<br>
A chaque fois que vous rafraîchissez la page, l'heure change.
</body>
</html>
إذا طلبنا هذه الصفحة باستخدام متصفح، فسنحصل على النتيجة التالية:

تم إنشاء الجزء الديناميكي من الصفحة بواسطة الكود PHP:
ما الذي حدث بالضبط؟ طلب المتصفح ملف URL http://localhost/poly/intro/heure.php. تلقى خادم الويب (في المثال هذا هو Apache) هذا الطلب، ولاحظ، بسبب اللاحقة .php للملف المطلوب، أنه يجب عليه توجيه هذا الطلب إلى المترجم PHP. ثم يقوم هذا المفسر بتحليل المستند heure.php وتنفيذ جميع أجزاء الكود الموجودة بين العلامات <?php > ويستبدل كل منها بالأسطر المكتوبة بواسطة التعليمات PHP أو echo أو print. وبالتالي، سيقوم المُترجم PHP بتنفيذ جزء الكود أعلاه واستبداله بالسطر الذي كتبته التعليمات echo:
بمجرد تنفيذ جميع أجزاء الكود PHP، يصبح المستند PHP مستندًا بسيطًا HTML يتم إرساله بعد ذلك إلى العميل.
سنحاول قدر الإمكان تجنب الخلط بين الكود PHP والكود HTML. ولهذا الغرض، يمكن إعادة كتابة التطبيق السابق على النحو التالي:
<!-- code PHP -->
<?php
// on récupère l'heure du moment
$maintenant=time();
$maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- code HTML -->
<html>
<head>
<title>Une page php dynamique</title>
</head>
<body>
<center>
<h1>Une page PHP générée dynamiquement</h1>
<h2>
<?php echo $maintenant ?>
</h2>
<br>
A chaque fois que vous rafraîchissez la page, l'heure change.
</body>
</html>
والنتيجة التي تظهر في المتصفح هي نفسها:

النسخة الثانية أفضل من الأولى لأن كود PHP أقل في كود HTML. وبذلك تصبح بنية الصفحة أكثر وضوحًا. ويمكننا الذهاب إلى أبعد من ذلك بوضع الكود PHP والكود HTML في ملفين مختلفين. ويتم تخزين الكود PHP في الملف heure3.php:
<!-- code PHP -->
<?php
// on récupère l'heure du moment
$maintenant=time();
$maintenant=date("j/m/y, h:i:s",$maintenant);
// on affiche la réponse
include "heure3-page1.php";
?>
أما الرمز HTML فهو مخزّن في الملف heure3-page1.php:
<!-- code HTML -->
<html>
<head>
<title>Une page php dynamique</title>
</head>
<body>
<center>
<h1>Une page PHP générée dynamiquement</h1>
<h2>
<?php echo $maintenant ?>
</h2>
<br>
A chaque fois que vous rafraîchissez la page, l'heure change.
</body>
</html>
عندما يطلب المتصفح المستند heure3.php، سيتم تحميله وتحليله بواسطة المترجم PHP. عند الوصول إلى السطر
سيقوم المُترجم بتضمين الملف heure3-page1.php في شفرة المصدر لـ heure3.php وتنفيذه. وبذلك، يبدو الأمر كما لو كان لدينا الشفرة التالية لـ PHP:
<!-- code PHP -->
<?php
// on récupère l'heure du moment
$maintenant=time();
$maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- code HTML -->
<html>
<head>
<title>Une page php dynamique</title>
</head>
<body>
<center>
<h1>Une page PHP générée dynamiquement</h1>
<h2>
<?php echo $maintenant ?>
</h2>
<br>
A chaque fois que vous rafraîchissez la page, l'heure change.
</body>
</html>
النتيجة التي تم الحصول عليها هي نفسها كما في السابق:

سيتم اعتماد الحل المتمثل في وضع الرموز PHP و HTML في ملفات مختلفة لاحقًا. ويقدم هذا الحل مزايا متنوعة:
- لا تختفي بنية الصفحات المرسلة إلى العميل وسط كود PHP. وبالتالي يمكن صيانتها بواسطة «مصمم ويب» يتمتع بمهارات في التصميم الجرافيكي ولكنه يفتقر إلى الخبرة في PHP.
- يُستخدم كود PHP كواجهة أمامية لطلبات العملاء. ويهدف إلى حساب البيانات اللازمة للصفحة التي سيتم إرسالها كاستجابة للعميل.
إلا أن هذا الحل ينطوي على عيب: فبدلاً من أن يتطلب تحميل مستند واحد فقط، يتطلب تحميل عدة مستندات، مما قد يؤدي إلى انخفاض في الأداء.
3.6. استرداد المعلمات المرسلة من عميل الويب
3.6.1. عبر POST
لنأخذ النموذج التالي على سبيل المثال، حيث يتعين على المستخدم إدخال معلومتين: الاسم والعمر.

عندما يملأ المستخدم الحقول Nom و Age، يضغط بعد ذلك على الزر Envoyer الذي ينتمي إلى النوع submit. ثم يتم إرسال قيم النموذج إلى الخادم. ويقوم الخادم بإعادة إرسال النموذج مع جدول يضم القيم التي تلقّاها:

يطلب المتصفح النموذج من التطبيق التالي nomage.php:
<?php
// a-t-on les paramètres attendus ?
$post=isset($_POST["txtNom"]) && isset($_POST["txtAge"]);
if($post){
// on récupère les paramètres txtNom et txtAge "postés" par le client
$nom=$_POST["txtNom"];
$age=$_POST["txtAge"];
} else {
$nom="";
$age="";
}//if
// affichage de la page
include "nomage-p1.php";
?>
بعض التوضيحات:
- يمكن إرسال حقل نموذج HTML يُسمى «field» إلى الخادم باستخدام الطريقة GET أو الطريقة POST. وإذا تم إرساله باستخدام الطريقة GET، يمكن للخادم استرداده من المتغير $_GET["champ"] ومن المتغير $_POST["champ"] إذاإذا تم إرساله عبر الطريقة POST.
- يمكن اختبار وجود البيانات باستخدام الدالة isset(البيانات) التي تُرجع true إذا كانت البيانات موجودة، و false في حالة عدم وجودها.
- يقوم التطبيق nomage.php بإنشاء ثلاث متغيرات: $nom لاسم النموذج، و$age للعمر، و$post للإشارة إلى ما إذا كانت هناك قيم «مرسلة» أم لا. يتم إرسال هذه المتغيرات الثلاثة إلى الصفحة nomage-p1.php. وتجدر الإشارة إلى أنه على الرغم من مشاركة هذه الصفحة في إعداد الرد المرسَل إلى العميل، إلا أن العميل لا يعلم بذلك. فمن وجهة نظره، فإن التطبيق nomage.php هو الذي يرد عليه.
- في المرة الأولى التي يطلب فيها العميل التطبيق nomage.php، نحصل على $post كقيمة خاطئة. في الواقع، خلال هذا الاستدعاء الأول، لا يتم إرسال أي قيم من النموذج إلى الخادم.
الصفحة nomage-p1.php هي كما يلي:
<html>
<head>
<title>Formulaire web</title>
</head>
<body>
<center>
<h3>Un formulaire Web</h3>
<h4>Récupération des valeurs des champs d'un formulaire</h4>
<hr>
<form name="frmPersonne" method="post">
<table>
<tr>
<td>Nom</td>
<td><input type="text" value="<?php echo $nom ?>" name="txtNom" size="20"></td>
<td>Age</td>
<td><input type="text" value="<?php echo $age ?>" name="txtAge" size="3"></td>
<tr>
</table>
<input type="submit" name="cmdEffacer" value="Envoyer">
</form>
</center>
<hr>
<?php
// y-at-il eu des valeurs postées ?
if ($post) {
?>
<h4>Valeurs récupérées</h4>
<table border="1">
<tr>
<td>Nom</td><td><?php echo $nom ?></td>
<td width="10"></td>
<td>Age</td><td><?php echo $age ?></td>
<tr>
</table>
<?php } ?>
</body>
</html>
يعرض التطبيق nomage-p1.php النموذج frmPersonne. ويتم تعريف هذا النموذج بواسطة العلامة:
ونظرًا لعدم تعريف السمة action الخاصة بالعلامة، فإن المتصفح سيرسل بيانات النموذج إلى URL الذي استدعاه للحصول عليها، وهو c.a.d. التطبيق nomage.php.
لنميز بين حالتين لاستدعاء التطبيق nomage.php:
- هذه هي المرة الأولى التي يستدعي فيها المستخدم التطبيق. لذا، يستدعي التطبيق nomage.php التطبيق nomage-p1.php مزودًا إياه بالقيم ($nom,$age,$post)=("","",false). ثم يعرض التطبيق nomage-p1.php نموذجًا فارغًا.
- يقوم المستخدم بملء النموذج ويستخدم الزر Envoyer (من النوع submit). ثم يتم «إرسال» قيم النموذج (txtNom، txtAge) (method="post" في <form>) إلى التطبيق nomage.php (السمة action غير محددة في <form>). يقوم التطبيق nomage.php بحساب ($nom، $age،$post) = (txtNom، txtAge، صحيح) وترسلها إلى التطبيق nomage-p1.php الذي يعرض بعد ذلك نموذجًا مملوءًا مسبقًا بالإضافة إلى جدول القيم المستردة.
3.6.2. بواسطة GET
في حالة إرسال قيم النموذج إلى الخادم بواسطة GET، يتحول التطبيق nomage.php إلى التطبيق التالي nomage2.php:
<?php
// a-t-on les paramètres attendus ?
$get=isset($_GET["txtNom"]) && isset($_GET["txtAge"]);
if($get){
// on récupère les paramètres txtNom et txtAge "GETTés" par le client
$nom=$_GET["txtNom"];
$age=$_GET["txtAge"];
} else {
$nom="";
$age="";
}//if
// affichage de la page
include "nomage-p2.php";
?>
التطبيق nomage-p2.php مطابق للتطبيق nomage-p1.php باستثناء التفاصيل التالية:
- تم تعديل العلامة form:
- يسترد التطبيق الآن متغير $get بدلاً من $post:
عند التنفيذ، عندما يتم إدخال القيم في النموذج وإرسالها إلى الخادم، يعكس المتصفح في حقل URL حقيقة أن القيم قد تم إرسالها بواسطة الطريقة GET:
![]()
3.7. استرداد الرؤوس HTTP المرسلة من قبل عميل ويب
عندما يرسل متصفح طلبًا إلى خادم ويب، فإنه يرسل إليه عددًا معينًا من الرؤوس HTTP. قد يكون من المفيد أحيانًا الوصول إلى هذه الرؤوس. يمكننا في البداية الاستعانة بالجدول الترابطي $_SERVER. يحتوي هذا المصفوف على معلومات متنوعة يزوده بها خادم الويب، ومن بينها رؤوس HTTP التي يقدمها العميل. لننظر إلى البرنامج التالي الذي يعرض جميع قيم المصفوفة $_SERVER:
<?php
// affiche les variables liées au serveur web
// on envoie du texte simple
header("Content-type: text/plain");
// parcours du tableau associatif $_SERVER
reset($_SERVER);
while (list($clé,$valeur)=each($_SERVER)){
echo "$clé : $valeur\n";
}//while
?>
لنحفظ هذا الرمز في headers.php ونطلب هذا URL باستخدام متصفح:

نحصل على عدد من المعلومات بما في ذلك رؤوس HTTP المرسلة من المتصفح. هذه هي القيم المرتبطة بالمفاتيح التي تبدأ بـ HTTP. لنفصل بعض المعلومات التي تم الحصول عليها أعلاه:
أنواع المستندات التي يقبلها عميل الويب | |
أنواع الأحرف المقبولة في المستندات | |
أنواع الترميزات المقبولة للمستندات | |
أنواع اللغات المقبولة في المستندات | |
نوع الاتصال بالخادم. Keep-Alive: يجب على الخادم الحفاظ على الاتصال مفتوحًا بعد إرسال الرد | |
? المدة القصوى لفتح الاتصال | |
الجهاز المضيف الذي يستفسر عنه العميل | |
هوية العميل | |
عنوان IP للعميل | |
منفذ الاتصال الذي يستخدمه العميل | |
بروتوكول HTTP الذي يستخدمه الخادم | |
طريقة الاستعلام المستخدمة من قبل العميل (GET أو POST) | |
الطلب ?param1=val1¶m2=val2&... الموجود خلف الطلب URL (طريقة GET) |
من خلال إجراء تعديل طفيف على كود البرنامج السابق، يمكننا استرداد رؤوس HTTP فقط:
<?php
// affiche les variables liées au serveur web
// on envoie du texte simple
header("Content-type: text/plain");
// parcours du tableau associatif $_SERVER
reset($_SERVER);
while (list($clé,$valeur)=each($_SERVER)){
// entête HTTP ?
if(strtolower(substr($clé,0,4))=="http")
echo substr($clé,5)." : $valeur\n";
}//while
?>
والنتيجة التي تظهر في المتصفح هي كما يلي:

إذا أردنا عنوانًا محددًا مثل HTTP، فسنكتب على سبيل المثال $_SERVER["HTTP_ACCEPT"].
3.8. استرداد معلومات البيئة
يعمل خادم الويب/PHP في بيئة يمكننا التعرف عليها من خلال الجدول $_ENV الذي يخزن خصائص متنوعة لبيئة التشغيل. لنفترض التطبيق التالي: env1.php:
<?php
// affiche les variables liées au serveur web
// on envoie du texte simple
header("Content-type: text/plain");
// parcours du tableau associatif $_ENV
reset($_ENV);
while (list($clé,$valeur)=each($_ENV)){
echo "$clé : $valeur\n";
}//while
?>
وتعطي النتيجة التالية في المتصفح (عرض جزئي):

ونرى في المثال أعلاه أن خادم الويب/PHP يعمل تحت نظام التشغيل Windows.
3.9. أمثلة
3.9.1. إنشاء نموذج ديناميكي - 1
نأخذ كمثال إنشاء نموذج يحتوي على عنصر تحكم واحد فقط: قائمة منسدلة. يتم إنشاء محتوى هذه القائمة المنسدلة ديناميكيًا باستخدام قيم مأخوذة من مصفوفة. في الواقع، غالبًا ما يتم أخذها من قاعدة بيانات. النموذج هو كما يلي:

إذا استخدمنا في المثال أعلاه Envoyer، فسنحصل على النتيجة التالية:

الرمز HTML للنموذج الأولي، بمجرد إنشائه، هو كما يلي:
<html>
<head>
<title>Génération de formulaire</title>
</head>
<body>
<h2>Choisissez un nombre</h2>
<hr>
<form name="frmvaleurs" method="post" action="valeurs.php">
<select name="cmbValeurs" size="1">
<option>un</option>
<option>deux</option>
<option>trois</option>
<option>quatre</option>
<option>cinq</option>
<option>six</option>
<option>sept</option>
<option>huit</option>
<option>neuf</option>
<option>dix</option>
</select>
<input type="submit" value="Envoyer" name="cmdEnvoyer">
</form>
</body>
</html>
يتكون التطبيق PHP من صفحة رئيسية valeurs.php يتم استدعاؤها سواء للحصول على النموذج الأولي (قائمة القيم) أو لمعالجة قيمه وتقديم الإجابة (القيمة المختارة). يقوم التطبيق بإنشاء صفحتين مختلفتين:
- صفحة النموذج الأولي التي سيتم إنشاؤها بواسطة البرنامج valeurs-p1.php
- صفحة الإجابة المقدمة للمستخدم، والتي سيتم إنشاؤها بواسطة البرنامج valeurs-p2.php
التطبيق valeurs.php هو التالي:
<?php
// le tableau des valeurs
$valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");
// a-t-on les paramètres attendus
$requêteVide=! isset($_POST["cmbValeurs"]);
// on récupère le choix de l'utilisateur
if ($requêteVide){
// requête initiale
include "valeurs-p1.php";
}else{
// réponse à un POST
$choix=$_POST["cmbValeurs"];
include "valeurs-p2.php";
}
?>
وهي تحدد جدول القيم وتستدعي valeurs-p1.php لإنشاء النموذج الأولي إذا كانت استعلام العميل فارغًا، أو valeurs-p2.php لإنشاء الرد في حالة وجود استعلام صالح. وفيما يلي برنامج valeurs1-php:
<html>
<head>
<title>Génération de formulaire</title>
</head>
<body>
<h2>Choisissez un nombre</h2>
<hr>
<form name="frmvaleurs" method="post" action="valeurs.php">
<select name="cmbValeurs" size="1">
<?php
for($i=0;$i<count($valeurs);$i++){
echo "<option>$valeurs[$i]</option>\n";
}//for
?>
</select>
<input type="submit" value="Envoyer" name="cmdEnvoyer">
</form>
</body>
</html>
يتم إنشاء قائمة قيم القائمة المنسدلة ديناميكيًا من الجدول $valeurs الذي تم إرساله بواسطة valeurs.php. يقوم البرنامج valeurs-p2.php بإنشاء الاستجابة:
<html>
<head>
<title>réponse</title>
</head>
<body>
<h2>Vous avez choisi le nombre <?php echo $choix ?></h2>
</body>
</html>
هنا، نكتفي بعرض قيمة المتغير $choix الذي تم إرساله أيضًا بواسطة valeurs.php.
3.9.2. التوليد الديناميكي للنموذج - 2
نستأنف المثال السابق مع تعديله على النحو التالي. النموذج المقترح لا يزال هو نفسه:

هل تختلف الاستجابة؟

في الرد، يتم إرجاع النموذج، مع الإشارة إلى الرقم الذي اختاره المستخدم أسفله. علاوة على ذلك، هذا الرقم هو الذي يظهر على أنه محدد في القائمة المعروضة في الرد.
الرمز الخاص بـ valeurs.php هو التالي:
<?php
// configuration
ini_set("register_globals","off");
// le tableau des valeurs
$valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");
// on récupère l'éventuel choix de l'utilisateur
$choix=$_POST["cmbValeurs"];
// on affiche la réponse
include "valeurs-p1.php";
?>
يُلاحظ هنا أننا حرصنا على تكوين PHP بحيث لا توجد متغيرات عامة. وهذا إجراء احترازي سليم بشكل عام لأن المتغيرات العامة تسبب مشاكل أمنية. وهناك بديل آخر يتمثل في تهيئة جميع المتغيرات المستخدمة. وسيؤدي ذلك إلى «تجاوز» أي متغير عام محتمل يحمل نفس الاسم.
يتم عرض صفحة النموذج بواسطة valeurs-p1.php:
<html>
<head>
<title>Génération de formulaire</title>
</head>
<body>
<h2>Choisissez un nombre</h2>
<hr>
<form name="frmvaleurs" method="post" action="valeurs.php">
<select name="cmbValeurs" size="1">
<?php
for($i=0;$i<count($valeurs);$i++){
// si option courante est égale au choix, on la sélectionne
if (isset($choix) && $choix==$valeurs[$i])
echo "<option selected>$valeurs[$i]</option>\n";
else echo "<option>$valeurs[$i]</option>\n";
}//for
?>
</select>
<input type="submit" value="Envoyer" name="cmdEnvoyer">
</form>
<?php
// suite page
if(isset($choix)){
echo "<hr>\n";
echo "<h3>Vous avez choisi le nombre $choix</h3>\n";
}
?>
</body>
</html>
يعتمد البرنامج المولد للصفحة على المتغير $choix الذي تم تمريره بواسطة البرنامج valeurs.php. تجدر الإشارة هنا إلى أن بنية الصفحة HTML بدأت تتعرض لـ«تلوث» خطير بسبب كود PHP. يمكن للواجهة الأمامية valeurs.php أن تؤدي المزيد من المهام كما يوضح الإصدار الجديد التالي:
<?php
// configuration
ini_set("register_globals","off");
// le tableau des valeurs
$valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");
// on récupère l'éventuel choix de l'utilisateur
$choix=$_POST["cmbValeurs"];
// on calcule la liste des valeurs à afficher
$HTMLvaleurs="";
for($i=0;$i<count($valeurs);$i++){
// si option courante est égale au choix, on la sélectionne
if (isset($choix) && $choix==$valeurs[$i])
$HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
}//for
// on calcule la seconde partie de la page
$HTMLpart2="";
if(isset($choix)){
$HTMLpart2="<hr>\n";
$HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
}//if
// on affiche la réponse
include "valeurs-p2.php";
?>
يتم الآن إنشاء الصفحة بواسطة البرنامج valeurs-p2.php التالي:
<html>
<head>
<title>Génération de formulaire</title>
</head>
<body>
<h2>Choisissez un nombre</h2>
<hr>
<form name="frmvaleurs" method="post" action="valeurs.php">
<select name="cmbValeurs" size="1">
<?php
// affichage liste de valeurs
echo $HTMLvaleurs;
?>
</select>
<input type="submit" value="Envoyer" name="cmdEnvoyer">
</form>
<?php
// affichage partie 2
echo $HTMLpart2;
?>
</body>
</html>
تم الآن حذف جزء كبير من الرمز PHP من الرمز HTML. ولكن دعونا نذكر الغرض من التقسيم إلى برنامج واجهة يحلل ويعالج طلب العميل وبرامج مكلفة ببساطة بعرض الصفحات التي تم تكوينها بواسطة البيانات المرسلة من الواجهة: وهو فصل عمل المطور PHP عن عمل مصمم الجرافيك. يعمل المطور PHP على الواجهة الأمامية، بينما يعمل مصمم الجرافيك على صفحات الويب. في إصدارنا الجديد، لم يعد بإمكان مصمم الجرافيك، على سبيل المثال، العمل على الجزء 2 من الصفحة لأنه لم يعد لديه حق الوصول إلى كود HTML الخاص بها. في الإصدار الأول، كان بإمكانه ذلك. لذلك، لا تعتبر أي من الطريقتين مثالية.
3.9.3. التوليد الديناميكي للنماذج - 3
نعود إلى نفس المشكلة السابقة، لكن هذه المرة يتم استخراج القيم من قاعدة بيانات. وفي مثالنا هذا، قاعدة البيانات هي MySQL:
- اسم قاعدة البيانات هو dbValeurs
- ومالكها هو admDbValeurs وكلمة المرور هي mdpDbValeurs
- تحتوي قاعدة البيانات على جدول واحد يسمى tvaleurs
- يحتوي هذا الجدول على حقل واحد من النوع «عدد صحيح» يُسمى «قيمة»
dos> mysql --database=dbValeurs --user=admDbValeurs --password=mdpDbValeurs
mysql> show tables;
+---------------------+
| Tables_in_dbValeurs |
+---------------------+
| tvaleurs |
+---------------------+
1 row in set (0.00 sec)
mysql> describe tvaleurs;
+--------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+-------+
| valeur | int(11) | | | 0 | |
+--------+---------+------+-----+---------+-------+
mysql> select * from tvaleurs;
+--------+
| valeur |
+--------+
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 6 |
| 5 |
| 7 |
| 8 |
| 9 |
+--------+
10 rows in set (0.00 sec)
في أي تطبيق يستخدم قاعدة بيانات، عادةً ما تتضمن العملية الخطوات التالية:
- الاتصال بـ SGBD
- إرسال استعلامات SQL إلى قاعدة بيانات SGBD
- معالجة نتائج هذه الاستعلامات
- إغلاق الاتصال بـ SGBD
يتم تنفيذ الخطوتين 2 و3 بشكل متكرر، ولا يتم إغلاق الاتصال إلا عند انتهاء عملية تشغيل قاعدة البيانات. وهذا نمط تقليدي نسبيًا لأي شخص سبق له تشغيل قاعدة بيانات بطريقة تفاعلية. يقدم الجدول التالي التعليمات PHP لتنفيذ هذه العمليات المختلفة باستخدام SGBD وMySQL:
$connexion=mysql_pconnect($hote,$user,$pwd) $connexion=mysql_connect($hote،$user،$pwd) $hote: الاسم على الإنترنت للجهاز الذي يعمل عليه SGBD وMySQL. في الواقع، من الممكن العمل مع برامج SGBD وMySQL عن بُعد. $user: اسم مستخدم معروف لـ SGBD و MySQL $pwd: كلمة مروره $connexion: الاتصال الذي تم إنشاؤه يقوم mysql_pconnect بإنشاء اتصال دائم مع SGBD و MySQL. لا يتم إغلاق هذا الاتصال عند انتهاء البرنامج النصي. بل يظل مفتوحًا. وبالتالي، عندما يتعين فتح اتصال جديد مع SGBD، سيبحث PHP عن اتصال موجود تابع لنفس المستخدم. وإذا عثر عليه، فسيستخدمه. وهذا يوفر الوقت. يقوم mysql_connect بإنشاء اتصال غير دائم، وبالتالي يتم إغلاقه عند انتهاء العمل مع SGBD وMySQL. | |
$résultats=mysql_db_query($base,$requête,$connexion) $base: قاعدة MySQL التي سنعمل عليها $requête: استعلام SQL (إدراج، حذف، تحديث، اختيار، ...) $connexion: الاتصال بـ SGBD MySQL $résultats: نتائج الاستعلام - تختلف حسب ما إذا كان الاستعلام عبارة عن «select» أو عملية تحديث (إدراج، تحديث، حذف، ...) | |
$résultats=mysql_db_query($base، "select ..."، $connexion) نتيجة عملية الاختيار (select) هي جدول، أي مجموعة من الصفوف والأعمدة. يمكن الوصول إلى هذا الجدول عبر $résultats. تقوم $ligne=mysql_fetch_row($résultats) بقراءة سطر من الجدول ووضعه في $ligne في شكل مصفوفة. وبالتالي، فإن $ligne[i] يمثل العمود i من السطر الذي تم استرداده. يمكن استدعاء الدالة mysql_fetch_row بشكل متكرر. وفي كل مرة، تقرأ الدالة صفًا جديدًا من الجدول $résultats. وعند الوصول إلى نهاية الجدول، تُرجع الدالة القيمة false. وبذلك يمكن استغلال الجدول $résultats بالطريقة التالية: while($line=mysql_fetch_row($results)){ // معالجة الصف الحالي $ligne }//while | |
$résultats=mysql_db_query($base، "insert ..."، $connexion) تكون قيمة $résultats صحيحة أو خاطئة حسب نجاح العملية أو فشلها. في حالة النجاح، تتيح الدالة mysql_affected_rows معرفة عدد الأسطر التي تم تعديلها بواسطة عملية التحديث. | |
mysql_close($connexion) $connexion: اتصال بـ SGBD MySQL |
يصبح كود الواجهة الأمامية valeurs.php كما يلي:
<?php
// configuration
ini_set("register_globals","off");
ini_set("display_errors","off");
ini_set("track_errors","on");
// le tableau des valeurs
list($erreur,$valeurs)=getValeurs();
// y-at-il eu erreur ?
if($erreur){
// affichage page d'erreur
include "valeurs-err.php";
// fin
return;
}//if
// on récupère l'éventuel choix de l'utilisateur
$choix=$_POST["cmbValeurs"];
// on calcule la liste des valeurs à afficher
$HTMLvaleurs="";
for($i=0;$i<count($valeurs);$i++){
// si option courante est égale au choix, on la sélectionne
if (isset($choix) && $choix==$valeurs[$i])
$HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
}//for
// on calcule la seconde partie de la page
$HTMLpart2="";
if(isset($choix)){
$HTMLpart2="<hr>\n";
$HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
}//if
// on affiche la réponse
include "valeurs-p1.php";
// fin
return;
// ------------------------------------------------------------------------
function getValeurs(){
// récupère les valeurs dans une base MySQL
$user="admDbValeurs";
$pwd="mdpDbValeurs";
$db="dbValeurs";
$hote="localhost";
$table="tvaleurs";
$champ="valeur";
// ouverture d'une connexion persistante au serveur MySQL
// ou sinon d'une connexion normale
($connexion=mysql_pconnect($hote,$user,$pwd))
|| ($connexion=mysql_connect($hote,$user,$pwd));
if(! $connexion)
return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");
// obtention des valeurs
$selectValeurs=mysql_db_query($db,"select $champ from $table",$connexion);
if(! $selectValeurs)
return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");
// les valeurs sont mises dans un tableau
$valeurs=array();
while($ligne=mysql_fetch_row($selectValeurs)){
$valeurs[]=$ligne[0];
}//while
// fermeture de la connexion (si elle est persistante, elle ne sera en fait pas fermée)
mysql_close($connexion);
// retour du résultat
return array("",$valeurs);
}//getValeurs
?>
هذه المرة، لا يتم توفير القيم التي يجب إدخالها في القائمة المنسدلة من خلال جدول، بل من خلال الدالة getValeurs(). هذه الدالة:
- تفتح اتصالاً مستمراً (mysql_pconnect) مع الخادم mySQL عن طريق تمرير اسم مستخدم مسجل وكلمة المرور الخاصة به.
- بمجرد الحصول على الاتصال، يتم إرسال استعلام select لاسترداد القيم الموجودة في الجدول tvaleurs من قاعدة البيانات dbValeurs.
- يتم وضع نتيجة select في الجدول $valeurs الذي يتم إرجاعه إلى البرنامج المستدعي.
- تُرجع الدالة في الواقع مصفوفة تحتوي على نتيجتين ($erreur، $valeurs)، حيث يمثل العنصر الأول رسالة خطأ إن وجدت، أو سلسلة فارغة في حالة عدم وجود خطأ.
- يقوم البرنامج المستدعي بفحص ما إذا كان قد حدث خطأ أم لا، وإذا كان الأمر كذلك، فإنه يعرض الصفحة valeurs-err.php. وهذه الصفحة هي كما يلي:
<html>
<head>
<title>Erreur</title>
</head>
<body>
<h3>L'erreur suivante s'est produite</h3>
<font color="red">
<h4><?php echo $erreur ?></h4>
</font>
</body>
</html>
- إذا لم تحدث أي أخطاء، فإن البرنامج المستدعي يتوفر لديه القيم الموجودة في المصفوفة $valeurs. وبالتالي، نعود إلى المشكلة السابقة.
فيما يلي مثالان على التنفيذ:
- مع وجود خطأ

- بدون خطأ

3.9.4. التوليد الديناميكي للنماذج - 4
في المثال السابق، ماذا سيحدث إذا قمنا بتغيير SGBD؟ إذا انتقلنا من MySQL إلى Oracle أو SQL Server على سبيل المثال؟ سيكون من الضروري إعادة كتابة الدالة getValeurs() التي توفر القيم. وتكمن فائدة تجميع الكود اللازم لاسترداد القيم المراد عرضها في القائمة ضمن دالة واحدة في أن الكود المطلوب تعديله يكون محددًا بدقة ولا يكون مبعثرًا في جميع أنحاء البرنامج. يمكن إعادة كتابة الدالة getValeurs() بحيث تكون مستقلة عن الدالة SGBD المستخدمة. يكفي أن تعمل مع برنامج التشغيل ODBC الخاص بـ SGBD بدلاً من العمل مباشرة مع SGBD.
توجد العديد من قواعد البيانات في السوق. ومن أجل توحيد الوصول إلى قواعد البيانات في نظام التشغيل MS Windows، طورت Microsoft واجهة تُسمى ODBC (Open DataBase Connectivity). تخفي هذه الطبقة الخصائص المميزة لكل قاعدة بيانات تحت واجهة قياسية. توجد في نظام التشغيل MS Windows العديد من برامج التشغيل ODBC التي تسهل الوصول إلى قواعد البيانات. وفيما يلي، على سبيل المثال، قائمة ببرامج التشغيل ODBC المثبتة على جهاز يعمل بنظام Windows:

كما أن SGBD وMySQL لهما أيضًا برنامج تشغيل ODBC. ويمكن لأي تطبيق يعتمد على برامج تشغيل ODBC استخدام أي من قواعد البيانات المذكورة أعلاه دون الحاجة إلى إعادة البرمجة.
![]() |
لنجعل قاعدة البيانات MySQL وdbValeurs متاحة عبر برنامج تشغيل ODBC. الإجراء التالي مخصص لنظام التشغيل Windows 2000. أما بالنسبة لأنظمة Win9x، فإن الإجراء مشابه جدًّا. نقوم بتنشيط مدير الموارد ODBC:

استخدم الزر [Add] لإضافة مصدر بيانات جديد ODBC:

نختار برنامج التشغيل ODBC MySQL، ثم نضغط على [Terminer]، ثم نحدد خصائص مصدر البيانات:

الاسم المخصص لمصدر البيانات ODBC (odbc-قيم) | |
اسم الجهاز الذي يستضيف SGBD MySQL الذي يدير مصدر البيانات (localhost) | |
اسم قاعدة البيانات MySQL التي تمثل مصدر البيانات (dbValeurs) | |
مستخدم يتمتع بحقوق وصول كافية إلى قاعدة البيانات MySQL المراد إدارتها (admDbValeurs) | |
كلمة المرور الخاصة به (mdpDbValeurs) |
PHP قادر على العمل مع برامج التشغيل ODBC. يوضح الجدول التالي الوظائف المفيدة التي يجب معرفتها:
$connexion=odbc_pconnect($dsn,$user,$pwd) $connexion=odbc_connect($dsn,$user,$pwd) $dsn: اسم DSN (اسم مصدر البيانات) للجهاز الذي يعمل عليه SGBD $user: اسم مستخدم معروف لـ SGBD $pwd: كلمة مروره $connexion: الاتصال الذي تم إنشاؤه odbc_pconnect ينشئ اتصالاً مستمراً مع SGBD. لا يتم إغلاق هذا الاتصال عند انتهاء البرنامج النصي. بل يظل مفتوحًا. وبالتالي، عندما يتعين فتح اتصال جديد مع SGBD، سيبحث PHP عن اتصال موجود تابع لنفس المستخدم. وإذا عثر عليه، فسيستخدمه. وهذا يوفر الوقت. يقوم odbc_connect بإنشاء اتصال غير دائم، وبالتالي يتم إغلاقه عند انتهاء العمل مع SGBD. | |
$requêtePréparée=odbc_prepare($connexion,$requête) $requête: طلب SQL (إدراج، حذف، تحديث، اختيار، ...) $connexion: الاتصال بـ SGBD تحليل الاستعلام $requête والتحضير لتنفيذه. يتم الإشارة إلى الاستعلام «المُعد» بهذه الطريقة من خلال النتيجة $requêtePréparée. إن تحضير الاستعلام لتنفيذه ليس إلزاميًا، ولكنه يحسّن الأداء لأن تحليل الاستعلام يتم مرة واحدة فقط. ثم يُطلب تنفيذ الاستعلام المُعد. إذا طُلب تنفيذ استعلام غير مُعد بشكل متكرر، يتم تحليله في كل مرة، وهو أمر غير ضروري. وبمجرد إعداد الاستعلام، يتم تنفيذه بواسطة $res=odbc_execute($requêtePréparée) الذي يُرجع قيمة «صحيح» أو «خطأ» حسب نجاح تنفيذ الاستعلام من عدمه | |
نتيجة استعلام SELECT هي جدول، أي مجموعة من الصفوف والأعمدة. يمكن الوصول إلى هذا الجدول عبر $requêtePréparée. $res=odbc_fetch_row($requêtePréparée) يقرأ صفًا واحدًا من الجدول الناتج عن select. ويعطي القيمة «صحيح» أو «خطأ» حسب نجاح تنفيذ الاستعلام أو فشله. تتوفر عناصر السطر الذي تم استرداده عبر الدالة odbc_result: $val=odbc_result($requêtePréparée,i): العمود i من السطر الذي تمت قراءته للتو $val=odbc_result($requêtePréparée,"nomColonne") : العمود nomColonne من السطر الذي تمت قراءته للتو يمكن استدعاء الدالة odbc_fetch_row بشكل متكرر. وفي كل مرة، تقرأ سطراً جديداً من جدول النتائج. وعند الوصول إلى نهاية الجدول، تُرجع الدالة القيمة false. وبذلك يمكن استخدام جدول النتائج بالطريقة التالية: | |
odbc_close($اتصال) $connexion: اتصال بـ SGBD MySQL |
الدالة getValeurs() المسؤولة عن استرداد القيم من قاعدة البيانات ODBC هي كما يلي:
// ------------------------------------------------------------------------
function getValeurs(){
// récupère les valeurs dans une base MySQL
$user="admDbValeurs";
$pwd="mdpDbValeurs";
$db="dbValeurs";
$dsn="odbc-valeurs";
$table="tvaleurs";
$champ="valeur";
// ouverture d'une connexion persistante au serveur MySQL
// ou sinon d'une connexion normale
($connexion=odbc_pconnect($dsn,$user,$pwd))
|| ($connexion=odbc_connect($dsn,$user,$pwd));
if(! $connexion)
return array("1 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");
// obtention des valeurs
$selectValeurs=odbc_prepare($connexion,"select $champ from $table");
if(! odbc_execute($selectValeurs))
return array("2 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");
// les valeurs sont mises dans un tableau
$valeurs=array();
while(odbc_fetch_row($selectValeurs)){
$valeurs[]=odbc_result($selectValeurs,$champ);
}//while
// fermeture de la connexion (si elle est persistante, elle ne sera en fait pas fermée)
odbc_close($connexion);
// retour du résultat
return array("",$valeurs);
}//getValeurs
?>
إذا تم تشغيل التطبيق الجديد دون تفعيل قاعدة البيانات odbc-valeurs، فسيتم الحصول على النتيجة التالية:

يُلاحظ أن رمز الخطأ الذي يرجعه برنامج تشغيل ODBC (odbc_error()=S1000) غير واضح تمامًا. إذا تم توفير قاعدة البيانات odbc-valeurs، فسنحصل على نفس النتائج التي حصلنا عليها سابقًا.
في الختام، يمكن القول إن هذا الحل جيد لصيانة التطبيق. ففي الواقع، إذا تطلب الأمر تغيير قاعدة البيانات، فلن يتعين تغيير التطبيق. سيقوم مسؤول النظام ببساطة بإنشاء مصدر بيانات جديد ODBC لقاعدة البيانات الجديدة. ومن أجل الصيانة أيضًا، سيكون من الجيد تعيين معلمات الوصول إلى قاعدة البيانات ($dsn، $user، $pwd) في ملف منفصل يقوم التطبيق بتحميله عند بدء التشغيل (include).
3.9.5. استرداد قيم نموذج
لقد قمنا مرارًا وتكرارًا باسترداد قيم نموذج أرسلها عميل ويب. يوضح المثال التالي نموذجًا يجمع بين مكونات HTML الأكثر شيوعًا ويهدف إلى استرداد المعلمات المرسلة من متصفح العميل. النموذج هو كما يلي:

يصل النموذج مملوءًا مسبقًا. بعد ذلك، يمكن للمستخدم تعديله:

إذا ضغط على الزر [Envoyer]، فإن الخادم يرسل إليه قائمة بقيم النموذج:

النموذج عبارة عن صفحة ثابتة HTML balises.html:
<html>
<head>
<title>balises</title>
<script language="JavaScript">
function effacer(){
alert("Vous avez cliqué sur le bouton Effacer");
}//effacer
</script>
</head>
<body background="/images/standard.jpg">
<form method="POST" action="parameters.php">
<table border="0">
<tr>
<td>Etes-vous marié(e)</td>
<td>
<input type="radio" value="oui" name="R1">Oui
<input type="radio" name="R1" value="non" checked>Non
</td>
</tr>
<tr>
<td>Cases à cocher</td>
<td>
<input type="checkbox" name="C1" value="un">1
<input type="checkbox" name="C2" value="deux" checked>2
<input type="checkbox" name="C3" value="trois">3
</td>
</tr>
<tr>
<td>Champ de saisie</td>
<td>
<input type="text" name="txtSaisie" size="20" value="qqs mots">
</td>
</tr>
<tr>
<td>Mot de passe</td>
<td>
<input type="password" name="txtMdp" size="20" value="unMotDePasse">
</td>
</tr>
<tr>
<td>Boîte de saisie</td>
<td>
<textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>
</td>
</tr>
<tr>
<td>combo</td>
<td>
<select size="1" name="cmbValeurs">
<option>choix1</option>
<option selected>choix2</option>
<option>choix3</option>
</select>
</td>
</tr>
<tr>
<td>liste à choix simple</td>
<td>
<select size="3" name="lst1">
<option selected>liste1</option>
<option>liste2</option>
<option>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>
</td>
</tr>
<tr>
<td>liste à choix multiple</td>
<td>
<select size="3" name="lst2[]" multiple>
<option selected>liste1</option>
<option>liste2</option>
<option selected>liste3</option>
<option>liste4</option>
<option>liste5</option>
</select>
</td>
</tr>
<tr>
<td>bouton</td>
<td>
<input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">
</td>
</tr>
<tr>
<td>envoyer</td>
<td>
<input type="submit" value="Envoyer" name="cmdRenvoyer">
</td>
</tr>
<tr>
<td>rétablir</td>
<td>
<input type="reset" value="Rétablir" name="cmdRétablir">
</td>
</tr>
</table>
<input type="hidden" name="secret" value="uneValeur">
</form>
</body>
</html>
يوضح الجدول أدناه دور العلامات المختلفة في هذا المستند والقيمة التي يتم استردادها بواسطة PHP لأنواع المكونات المختلفة في النموذج. يمكن إرسال قيمة حقل باسم HTML C بواسطة POST أو GET. في الحالة الأولى، سيتم استردادها في المتغير $_GET["C"]، وفي الحالة الثانية في المتغير $_POST["C"]. يفترض الجدول التالي استخدام متغير POST.
التحكم | علامة HTML | القيمة المسترجعة بواسطة PHP |
<form method="POST" > | ||
<input type="text" name="txtSaisie" size="20" value="بضع كلمات"> | $_POST["txtSaisie"]: القيمة الموجودة في الحقل txtSaisie في النموذج | |
<input type="password" name="txtMdp" size="20" value="unMotDePasse"> | $_POST["txtmdp"]: القيمة الموجودة في الحقل txtMdp في النموذج | |
<textarea rows="2" name="areaSaisie" cols="20"> السطر 1 السطر 2 السطر 3 </textarea> | $_POST["areaSaisie"]: الأسطر الموجودة في الحقل areaSaisie في شكل سلسلة أحرف واحدة: "ligne1\r\nligne2\r\nligne3". يتم فصل الأسطر عن بعضها البعض بواسطة التسلسل "\r\n". | |
<input type="radio" value="نعم" name="R1">نعم <input type="radio" name="R1" value="لا" checked>لا | $_POST["R1"]: قيمة (=value) زر الاختيار المحدد بـ «نعم» أو «لا» حسب الحالة. | |
<input type="checkbox" name="C1" value="واحد">1 <input type="checkbox" name="C2" value="اثنان" checked>2 <input type="checkbox" name="C3" value="ثلاثة">3 | $_POST["C1"]: قيمة (=value) خانة الاختيار إذا تم تحديدها، وإلا فإن المتغير غير موجود. وبالتالي، إذا تم تحديد خانة الاختيار C1، فإن $_POST["C1"] تساوي "واحد"، وإلا فإن $_POST["C1"] غير موجودة. | |
<select size="1" name="cmbValeurs"> <option>الخيار 1</option> <option selected>الخيار 2</option> <option>الخيار 3</option> </select> | $_POST["cmbValeurs"] : الخيار المحدد في القائمة، على سبيل المثال "الخيار 3". | |
<select size="3" name="lst1"> <option selected>القائمة 1</option> <option>القائمة 2</option> <option>القائمة 3</option> <option>القائمة 4</option> <option>القائمة 5</option> </select> | $_POST["lst1"]: الخيار المحدد في القائمة، على سبيل المثال "liste5". | |
<select size="3" name="lst2[]" multiple> <option>liste1</option> <option>القائمة 2</option> <option selected>القائمة 3</option> <option>القائمة 4</option> <option>القائمة 5</option> </select> | $_POST["lst2"]: جدول الخيارات المحددة في القائمة، على سبيل المثال ["liste3,"liste5"]. تجدر الإشارة إلى الصيغة الخاصة لعلامة HTML في هذه الحالة بالذات: lst2[]. | |
<input type="hidden" name="secret" value="uneValeur"> | $_POST["secret"]: قيمة (=value) الحقل، وهي هنا "uneValeur". |
في مثالنا هذا، يتم توجيه قيم النموذج إلى البرنامج parameters.php:
ورمز هذا البرنامج هو التالي:
<?php
// configuration
ini_set("register_globals","off");
ini_set("display_errors","off");
// méthode d'appel
$méthode=$_SERVER["REQUEST_METHOD"];
// récupération des paramètres
// elle dépend de la méthode d'envoi de ceux-ci
if($méthode=="GET")
$param=$_GET;
else $param=$_POST;
$R1=$param["R1"];
$C1=$param["C1"];
$C2=$param["C2"];
$C3=$param["C3"];
$txtSaisie=$param["txtSaisie"];
$txtMdp=$param["txtMdp"];
$areaSaisie=implode("<br>",explode("\r\n",$param["areaSaisie"]));
$cmbValeurs=$param["cmbValeurs"];
$lst1=$param["lst1"];
$lst2=implode("<br>",$param["lst2"]);
$secret=$param["secret"];
// requête valide ?
$requêteValide=isset($R1) && (isset($C1) || isset($C2) || isset($C3))
&& isset($txtSaisie) && isset($txtMdp) && isset($areaSaisie)
&& isset($cmbValeurs) && isset($lst1) && isset($lst2)
&& isset($secret);
// affichage page
if ($requêteValide)
include "parameters-p1.php";
else include "balises.html";
?>
دعونا نستعرض بعض النقاط في هذا البرنامج:
- يتجاوز التطبيق طريقة نقل قيم النموذج إلى الخادم. في الحالتين المحتملتين (GET و POST)، تتم الإشارة إلى قاموس القيم المنقولة بواسطة $param.
- من محتوى الحقل areaSaisie "السطر1\r\nالسطر2\r\n..." يتم إنشاء مصفوفة من السلاسل باستخدام explode("\r\n",$param["areaSaisie"]). وبذلك نحصل على المصفوفة [ligne1,ligne2,...]. ومن هذه المصفوفة، يتم إنشاء السلسلة "ligne1<br>ligne2<br>..." باستخدام الدالة implode.
- قيمة قائمة الاختيار المتعدد lst2 هي مصفوفة، على سبيل المثال ["option3","option5"]. انطلاقًا من هذه المصفوفة، يتم إنشاء سلسلة أحرف "option3<br>option5" باستخدام الدالة implode.
- يتحقق التطبيق من أن جميع المعلمات قد تم تعيينها. يجب أن نتذكر هنا أن أي دالة من نوع URL يمكن استدعاؤها يدويًّا أو برمجيًّا، وأن المعلمات المتوقعة قد لا تكون متوفرة بالضرورة. في حالة نقص المعلمات، يتم عرض الصفحة balises.html، وإلا يتم عرض الصفحة parameters-p1.php. تعرض هذه الصفحة القيم التي تم استردادها وحسابها في parameters.php في جدول:
<html>
<head>
<title>Récupération des paramètres d'un formulaire</title>
</head>
<body>
<table border="1">
<tr>
<td>R1</td>
<td><?php echo $R1 ?></td>
</tr>
<tr>
<td>C1</td>
<td><?php echo $C1 ?></td>
</tr>
<tr>
<td>C2</td>
<td><?php echo $C2 ?></td>
</tr>
<tr>
<td>C3</td>
<td><?php echo $C3 ?></td>
</tr>
<tr>
<td>txtSaisie</td>
<td><?php echo $txtSaisie ?></td>
</tr>
<tr>
<td>txtMdp</td>
<td><?php echo $txtMdp ?></td>
</tr>
<tr>
<td>areaSaisie</td>
<td><?php echo $areaSaisie ?></td>
</tr>
<tr>
<td>cmbValeurs</td>
<td><?php echo $cmbValeurs ?></td>
</tr>
<tr>
<td>lst1</td>
<td><?php echo $lst1 ?></td>
</tr>
<tr>
<td>lst2</td>
<td><?php echo $lst2 ?></td>
</tr>
<tr>
<td>secret</td>
<td><?php echo $secret ?></td>
</tr>
</table>
</body>
</html>
3.10. تتبع الجلسة
3.10.1. المشكلة
قد يتألف تطبيق الويب من عدة عمليات تبادل للنماذج بين الخادم والعميل. وعندئذ يكون سير العمل كما يلي:
الخطوة 1
- يقوم العميل C1 بفتح اتصال مع الخادم وإرسال طلبه الأولي.
- يرسل الخادم النموذج F1 إلى العميل C1 ويغلق الاتصال الذي تم فتحه في الخطوة 1.
الخطوة 2
- يقوم العميل C1 بملء النموذج وإرساله إلى الخادم. للقيام بذلك، يفتح المتصفح اتصالاً جديداً بالخادم.
- يقوم الخادم بمعالجة بيانات النموذج 1، ويحسب المعلومات I1 بناءً عليها، ويرسل النموذج F2 إلى العميل C1 ويغلق الاتصال المفتوح في الخطوة 3.
الخطوة 3
- يتكرر دورة الخطوتين 3 و4 في الخطوتين 5 و6. عند انتهاء الخطوة 6، سيكون الخادم قد تلقى نموذجين هما F1 و F2، وسيقوم بناءً عليهما بحساب المعلومات I1 و I2.
المشكلة المطروحة هي: كيف يقوم الخادم بالاحتفاظ بالمعلومات I1 و I2 المرتبطة بالعميل C1؟ يُطلق على هذه المشكلة اسم «تتبع جلسة عمل العميل C1». لفهم جذور هذه المشكلة، دعونا نلقي نظرة على مخطط تطبيق خادم TCP-IP الذي يخدم عدة عملاء في وقت واحد:
![]() |
في تطبيق عميل-خادم TCP-IP التقليدي:
- يقوم العميل بإنشاء اتصال مع الخادم
- ويقوم عبر هذا الاتصال بتبادل البيانات مع الخادم
- يتم إغلاق الاتصال من قبل أحد الطرفين
النقطتان المهمتان في هذه الآلية هما:
- يتم إنشاء اتصال واحد لكل عميل
- يُستخدم هذا الاتصال طوال مدة الحوار بين الخادم وعميله
ما يسمح للخادم بمعرفة العميل الذي يعمل معه في لحظة معينة هو الاتصال أو بعبارة أخرى "الأنبوب" الذي يربطه بعميله. وبما أن هذا الأنبوب مخصص لعميل معين، فإن كل ما يصل عبر هذا الأنبوب يأتي من هذا العميل وكل ما يتم إرساله عبر هذا الأنبوب يصل إلى هذا العميل نفسه.
تتبع آلية العميل-الخادم HTTP المخطط السابق، مع ميزة تتمثل في أن التفاعل بين العميل والخادم يقتصر على تبادل واحد فقط بينهما:
- يقوم العميل بفتح اتصال بالخادم وتقديم طلبه
- يقوم الخادم بالرد ويغلق الاتصال
إذا قام العميل C، في الوقت T1، بتوجيه طلب إلى الخادم، فسيحصل على اتصال C1 الذي سيُستخدم في التبادل الوحيد للطلب والاستجابة. إذا قام هذا العميل نفسه، في الوقت T2، بتقديم طلب ثانٍ إلى الخادم، فسيحصل على اتصال C2 يختلف عن الاتصال C1. بالنسبة للخادم، لا يوجد أي فرق بين هذا الطلب الثاني للمستخدم C وطلبه الأولي: في كلتا الحالتين، يعتبر الخادم العميل عميلاً جديداً. ولكي يكون هناك ارتباط بين الاتصالات المختلفة للعميل C بالخادم، يجب أن «يتعرف» الخادم على العميل C باعتباره «عميلاً معتادًا» وأن يسترد الخادم المعلومات التي لديه عن هذا العميل المعتاد.
لنتخيل نظام إدارة يعمل بالطريقة التالية:
- هناك قائمة انتظار واحدة
- هناك عدة شبابيك. وبالتالي يمكن خدمة عدة عملاء في وقت واحد. عندما يتوفر شباك، يغادر عميل قائمة الانتظار ليتم خدمته عند هذا الشباك
- إذا كانت هذه هي المرة الأولى التي يحضر فيها العميل، فإن الموظف عند الشباك يعطيه قسيمة تحمل رقمًا. لا يمكن للعميل طرح سوى سؤال واحد. وعندما يحصل على إجابته، يجب عليه مغادرة الشباك والانتقال إلى نهاية طابور الانتظار. يقوم موظف الشباك بتدوين معلومات هذا العميل في ملف يحمل رقمه.
- وعندما يحين دوره مرة أخرى، قد يتم خدمة العميل من قبل موظف شباك آخر غير الذي خدمه في المرة السابقة. يطلب منه هذا الموظف الرقاقة ويستلم الملف الذي يحمل رقم الرقاقة. مرة أخرى، يقدم العميل طلبًا، ويحصل على إجابة، وتُضاف المعلومات إلى ملفه.
- وهكذا دواليك... وبمرور الوقت، سيحصل العميل على إجابة لجميع طلباته. ويتم تتبع الطلبات المختلفة بفضل الرقم التعريفي والملف المرتبط به.
آلية تتبع الجلسة في تطبيق الويب من نوع «العميل-الخادم» مشابهة للطريقة السابقة:
- عند تقديم الطلب الأول، يحصل العميل على رمز من خادم الويب
- وسيقدم هذا الرمز في كل طلب لاحق له للتعريف بنفسه
يمكن أن يتخذ الرمز أشكالًا مختلفة:
- حقل مخفي في نموذج
- يقوم العميل بإرسال طلبه الأول (يتعرف عليه الخادم من خلال عدم وجود رمز لدى العميل)
- يقوم الخادم بإرسال الرد (نموذج) ويضع الرمز المميز في حقل مخفي ضمنه. في هذه اللحظة، يتم إغلاق الاتصال (يغادر العميل النافذة حاملاً الرمز المميز الخاص به). وقد يكون الخادم قد حرص على ربط معلومات بهذا الرمز المميز.
- يقوم العميل بإرسال طلبه الثاني عن طريق إعادة إرسال النموذج. يسترد الخادم الرمز من النموذج. وبذلك يمكنه معالجة الطلب الثاني للعميل من خلال الوصول، بفضل الرمز، إلى المعلومات التي تم حسابها أثناء الطلب الأول. تُضاف معلومات جديدة إلى الملف المرتبط بالرمز، ويتم إرسال استجابة ثانية إلى العميل، ثم يُغلق الاتصال للمرة الثانية. وقد أُعيد إدراج الرمز في نموذج الاستجابة حتى يتمكن المستخدم من تقديمه عند طلبه التالي.
- وهكذا دواليك...
العيب الرئيسي لهذه التقنية هو أن الرمز المميز يجب إدراجه في نموذج. إذا لم تكن استجابة الخادم عبارة عن نموذج، فإن طريقة الحقل المخفي تصبح غير قابلة للاستخدام.
- طريقة ملف تعريف الارتباط
- يقوم العميل بإرسال طلبه الأول (يتعرف عليه الخادم من خلال عدم وجود رمز لدى العميل)
- يقوم الخادم بالرد بإضافة ملف تعريف ارتباط في رؤوس HTTP الخاصة بالرد. ويتم ذلك باستخدام الأمر HTTP Set-Cookie:
Set-Cookie: param1=القيمة1;param2=القيمة2;....
حيث تمثل parami أسماء المعلمات و valeursi قيمها. ومن بين هذه المعلمات، سيكون هناك الرمز المميز. وفي كثير من الأحيان، لا يحتوي ملف تعريف الارتباط إلا على الرمز المميز، بينما يتم تسجيل المعلومات الأخرى بواسطة الخادم في المجلد المرتبط بالرمز المميز. ويقوم المتصفح الذي يتلقى ملف تعريف الارتباط بتخزينه في ملف على القرص. وبعد استجابة الخادم، يتم إغلاق الاتصال (يغادر العميل النافذة مع رمزه المميز).
- (تابع)
- يقوم العميل بتوجيه طلبه الثاني إلى الخادم. في كل مرة يتم فيها توجيه طلب إلى الخادم، يبحث المتصفح بين جميع ملفات تعريف الارتباط الموجودة لديه، لمعرفة ما إذا كان لديه ملف تعريف ارتباط قادم من الخادم المطلوب. إذا كان الأمر كذلك، فإنه يرسلها إلى الخادم دائمًا في شكل أمر HTTP، وهو أمر ملف تعريف الارتباط الذي له صيغة مشابهة لتلك الخاصة بأمر Set-Cookie المستخدم من قبل الخادم:
Cookie: param1=value1;param2=value2;....
من بين رؤوس HTTP المرسلة من المتصفح، سيجد الخادم الرمز الذي يسمح له بالتعرف على العميل واسترجاع المعلومات المرتبطة به.
وهذا هو الشكل الأكثر استخدامًا للرمز المميز. لكنه ينطوي على عيب واحد: يمكن للمستخدم تهيئة متصفحه بحيث لا يقبل ملفات تعريف الارتباط. وبالتالي، لا يستطيع هذا النوع من المستخدمين الوصول إلى تطبيقات الويب التي تستخدم ملفات تعريف الارتباط.
- إعادة كتابة URL
- يقوم العميل بإرسال طلبه الأول (يتعرف عليه الخادم من خلال عدم وجود رمز لدى العميل)
- يرسل الخادم ردّه. يحتوي هذا الرد على روابط يجب على المستخدم استخدامها لمتابعة التطبيق. في عنوان URL لكل رابط من هذه الروابط، يضيف الخادم الرمز على الشكل URL;رمز=قيمة.
- عندما ينقر المستخدم على أحد الروابط لمتابعة التطبيق، يرسل المتصفح طلبه إلى خادم الويب عن طريق إرسال الرمز HTTP في رؤوس الطلبات، مع الرموز URL وURL؛token=القيمة المطلوبة. وبذلك يصبح الخادم قادرًا على استرداد الرمز المميز.
3.10.2. API و PHP لتتبع الجلسة
نقدم الآن الطرق الرئيسية المفيدة لتتبع الجلسة:
يبدأ الجلسة التي تنتمي إليها الطلب الحالي. إذا لم يكن الطلب جزءًا من جلسة بعد، يتم إنشاء الجلسة. | |
معرف الجلسة الحالية | |
قاموس يخزن بيانات الجلسة. يمكن الوصول إليه للقراءة والكتابة | |
يحذف البيانات الموجودة في الجلسة الحالية. تظل هذه البيانات متاحة للتبادل الجاري بين العميل والخادم، لكنها لن تكون متاحة عند التبادل التالي. |
3.10.3. المثال 1
نقدم مثالاً مستوحى من كتاب «البرمجة باستخدام J2EE» الصادر عن دار نشر Wrox والموزع من قبل Eyrolles. يتيح هذا المثال فهم كيفية عمل جلسة PHP. الصفحة الرئيسية هي التالية:

تحتوي على العناصر التالية:
- معرف الجلسة ID الذي يتم الحصول عليه بواسطة الدالة session_id(). يتم إرسال معرّف الجلسة ID الذي تم إنشاؤه بواسطة المتصفح إلى العميل عبر ملف تعريف ارتباط يرسله المتصفح عند طلب معرّف جلسة URL من نفس الشجرة. وهذا هو ما يحافظ على استمرار الجلسة.
- عداد يتم زيادته مع كل طلب من المتصفح، ويُظهر أن الجلسة لا تزال قائمة.
- رابط يسمح بحذف البيانات المرتبطة بالجلسة الحالية. ويتم ذلك بواسطة الدالة session_destroy()
- رابط لإعادة تحميل الصفحة
فيما يلي كود التطبيق cycledevie.php:
<?php
//cycledevie.php
// configuration
ini_set("register_globals","off");
ini_set("display_errors","off");
// on démarre une session
session_start();
// faut-il l'invalider ?
$action=$_GET["action"];
if($action=="invalider"){
// fin session
session_destroy();
}//if
// gestion du compteur
if(! isset($_SESSION["compteur"]))
// le compteur n'existe pas - on le crée
$_SESSION["compteur"]=0;
// le compteur existe - on l'incrémente
else $_SESSION["compteur"]++;
// on récupère l'ID de la session courante
$idSession=session_id();
// on récupère le compteur
$compteur=$_SESSION["compteur"];
// on passe la main à la page de visualisation
include "cycledevie-p1.php";
?>
تجدر الإشارة إلى النقاط التالية:
- منذ بدء تشغيل التطبيق، يتم بدء جلسة عمل. إذا أرسل العميل رمز جلسة عمل، يتم إعادة تشغيل الجلسة التي تحمل هذا المعرف، وتوضع جميع البيانات المرتبطة بها في القاموس $_SESSION. وإلا، يتم إنشاء رمز جلسة عمل جديد.
- إذا أرسل العميل معلمة action ذات القيمة "invalider"، يتم وضع علامة على بيانات الجلسة على أنها «مراد حذفها» في التبادل التالي. ولن يتم حفظها على الخادم في نهاية التبادل، على عكس التبادل العادي.
- يتم استرداد عداد مرتبط بالجلسة من القاموس $_SESSION، بالإضافة إلى معرّف الجلسة ID (session_id()).
- يتم إنشاء الصفحة المراد إرسالها إلى العميل بواسطة البرنامج cycledevie-p1.php
تعرض الصفحة cycledevie-p1.php الصفحة التي تم إرسالها إلى العميل:
<html>
<head>
<title>Gestion de sessions</title>
</head>
<body>
<h3>Cycle de vie d'une session PHP</h3>
<hr>
<br>ID session : <?php echo $idSession ?>
<br>compteur : <?php echo $compteur ?>
<br><a href="cycledevie.php?action=invalider">Invalider la session</a>
<br><a href="cycledevie.php">Recharger la page</a>
</body>
</html>
يُلاحظ وجود الرمز URL المرفق بكل من الرابطين:
- cycledevie.php لإعادة تحميل الصفحة
- cycledevie.php?action=invalider لإلغاء صلاحية الجلسة. في هذه الحالة، يتم إرفاق المعلمة action=invalider بـ URL. وسيتم استرداده بواسطة برنامج الخادم cycledevie.php من خلال الأمر $action=$_GET["action"].
لنقوم بإعادة تحميل الصفحة مرتين متتاليتين:

تم زيادة العداد بالفعل. لم يتغير معرّف الجلسة ID. لنقوم الآن بإبطال صلاحية الجلسة:

نلاحظ أننا فقدنا معرّف الجلسة ID، لكن العداد قد زاد مرة أخرى. لنقم بإعادة تحميل الصفحة:

نلاحظ أننا نبدأ من جديد بنفس معرف الجلسة ID كما في السابق. أما العداد فيعود إلى الصفر. وبالتالي، فإن الدالة session_destroy() لا يكون لها تأثير فوري. يتم حذف بيانات الجلسة الحالية فقط من أجل التبادل بين العميل والخادم الذي يلي عملية الحذف. لم تتغير جلسة العمل ID، مما يشير إلى أن الدالة session_destroy() لا تبدأ جلسة عمل جديدة مع إنشاء رقم جلسة عمل جديد ID. تم إعادة إرسال ملف تعريف الارتباط الخاص برمز الجلسة من متصفح العميل، وقامت PHP باسترداد الجلسة من هذا الرمز، وهي جلسة لم تعد تحتوي على أي بيانات.
أُجريت الاختبارات السابقة باستخدام متصفح Netscape المُهيأ لاستخدام ملفات تعريف الارتباط. لنقوم الآن بتهيئته لعدم استخدامها. وهذا يعني أنه لن يقوم بتخزين أو إعادة إرسال ملفات تعريف الارتباط المرسلة من الخادم. ومن المتوقع عندئذٍ أن تتوقف الجلسات عن العمل. لنجرب ذلك من خلال تبادل أولي:

لدينا معرّف جلسة ID، وهو الذي تم إنشاؤه بواسطة session_start(). لنقم بإعادة تحميل الصفحة باستخدام الرابط:

بشكل مفاجئ، تُظهر النتائج أعلاه أن لدينا جلسة عمل جارية وأن العداد يُدار بشكل صحيح. كيف يمكن ذلك، بما أن ملفات تعريف الارتباط قد تم تعطيلها، ولم يعد هناك تبادل للرموز بين الخادم والمتصفح؟ يمنحنا الرمز URL الموجود في لقطة الشاشة أعلاه الإجابة:
هذا هو الرمز URL الخاص برابط «إعادة تحميل الصفحة». دعونا نتحقق من كود المصدر للصفحة المعروضة بواسطة المتصفح:
<a href="cycledevie.php?action=invalider&PHPSESSID=587ce26f943a288d8f41212e30fed13c">Invalider la session</a>
<a href="cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c">Recharger la page</a>
لنتذكر أن الكود الأصلي للرابطين في الصفحة cycledevie-p1.php هو التالي:
<a href="cycledevie.php?action=invalider">Invalider la session</a>
<a href="cycledevie.php">Recharger la page</a>
وبالتالي، قام المُترجم PHP من تلقاء نفسه بإعادة كتابة URL للرابطين بإضافة رمز الجلسة إليهما. وبذلك يتم إرسال هذا الرمز بالفعل بواسطة المتصفح عند تنشيط الرابطين. وهذا ما يفسر أن الجلسة تظل تُدار بشكل صحيح حتى في حالة عدم تفعيل ملفات تعريف الارتباط.
3.10.4. المثال 3
نقترح كتابة تطبيق PHP يكون عميلاً للتطبيق compteur السابق. وسيقوم هذا التطبيق باستدعاءه N مرات متتالية، حيث يتم تمرير القيمة N كمعلمة. هدفنا هو عرض عميل ويب مبرمج والطريقة التي تُدار بها علامة الجلسة. وستكون نقطة انطلاقنا عميل ويب عام يُستدعى على النحو التالي:
clientweb URL GET/HEAD
- URL: عنوان URL المطلوب
- GET/HEAD: GET لطلب الرمز HTML من الصفحة، وHEAD للاقتصار على الرؤوس فقط HTTP
فيما يلي مثال باستخدام URL http://localhost/poly/sessions/2/cycledevie.php. هذا البرنامج هو نفسه الذي تم وصفه سابقًا مع اختلاف بسيط:
// on fixe le chemin du cookie
session_set_cookie_params(0,"/poly/sessions/2");
// on démarre une session
session_start();
تسمح الدالة session_set_cookie_params بتحديد بعض معلمات ملف تعريف الارتباط الذي سيحتوي على رمز الجلسة. المعلمة الأولى هي مدة صلاحية ملف تعريف الارتباط. تعني مدة الصلاحية الصفرية أن ملف تعريف الارتباط يتم حذفه بواسطة المتصفح الذي استلمه عند إغلاقه. المعلمة الثانية هي مسار URL الذي يجب على المتصفح إعادة ملف تعريف الارتباط إليه. في المثال أعلاه، إذا تلقى المتصفح ملف تعريف الارتباط من الجهاز localhost، فسيرسل ملف تعريف الارتباط إلى أي جهاز URL موجود في شجرة http://localhost/poly/sessions/2/.
dos>e:\php43\php.exe clientweb.php http://localhost/poly/sessions/2/cycledevie.php GET
HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 13:58:16 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: text/html
<html>
<head>
<title>Gestion de sessions</title>
</head>
<body>
<h3>Cycle de vie d'une session PHP</h3>
<hr>
<br>ID session : 48d5aaa0e99850b17c33a6e22d38e5c4 <br>compteur : 0
<br><a href="cycledevie.php?action=invalider&PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Invalid
er la session</a>
<br><a href="cycledevie.php?PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Recharger la page</a>
</body>
</html>
يعرض البرنامج clientweb كل ما يتلقاه من الخادم. نرى أعلاه الأمر HTTP Set-cookie الذي يستخدمه الخادم لإرسال ملف تعريف ارتباط إلى عميله. هنا يحتوي ملف تعريف الارتباط على معلومتين:
- PHPSESSID وهو رمز الجلسة
- path الذي يحدد URL التي تنتمي إليها ملف تعريف الارتباط. يُشير path=/poly/sessions/2 إلى المتصفح بأنه سيتعين عليه إعادة إرسال ملف تعريف الارتباط إلى الخادم في كل مرة يطلب فيها URL تبدأ بـ /poly/sessions/2 من الجهاز الذي أرسل إليه ملف تعريف الارتباط.
- يمكن لملف تعريف الارتباط أيضًا تحديد مدة صلاحية. في هذه الحالة، هذه المعلومة غير موجودة. وبالتالي، سيتم حذف ملف تعريف الارتباط عند إغلاق المتصفح. يمكن أن تبلغ مدة صلاحية ملف تعريف الارتباط N يومًا على سبيل المثال. وطالما كان ملف تعريف الارتباط ساري المفعول، سيقوم المتصفح بإعادة إرساله في كل مرة يتم فيها الوصول إلى أي من ملفات URL التابعة لنطاقه (Path). لنأخذ مثالاً على موقع بيع عبر الإنترنت لـ CD. يمكن لهذا الموقع تتبع مسار العميل في الكتالوج الخاص به وتحديد تفضيلاته تدريجيًا: الموسيقى الكلاسيكية على سبيل المثال. يمكن تخزين هذه التفضيلات في ملف تعريف ارتباط (cookie) مدته 3 أشهر. إذا عاد هذا العميل نفسه إلى الموقع بعد شهر، فسيرسل المتصفح ملف تعريف الارتباط إلى تطبيق الخادم. ووفقًا للمعلومات الموجودة في ملف تعريف الارتباط، سيتمكن التطبيق من تكييف الصفحات التي يتم إنشاؤها مع تفضيلات العميل.
فيما يلي كود عميل الويب.
<?php
// configuration
dl("php_curl.dll"); // bibliothèque CURL
// syntaxe : $0 URL GET
// il faut trois arguments
if($argc != 3){
// msg d'erreur
fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
// arrêt
exit(1);
}//if
// le troisième argument doit être GET ou HEAD
$header=strtolower($argv[2]);
if($header!="get" && $header!="head"){
// msg d'erreur
fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
// arrêt
exit(2);
}//if
// le premier argument est une URL
$URL=strtolower($argv[1]);
// préparation de la connexion
$connexion=curl_init($URL);
// paramétrage de la connexion
curl_setopt($connexion,CURLOPT_HEADER,1);
if($header=="head") curl_setopt($connexion,CURLOPT_NOBODY,1);
// exécution de la connexion
curl_exec($connexion);
// fermeture de la connexion
curl_close($connexion);
// fin
exit(0);
?>
يستخدم البرنامج السابق المكتبة CURL:
يقوم بتهيئة كائن CURL مع URL المطلوب الوصول إليه | |
يحدد قيمة بعض خيارات الاتصال. ومن بين الخيارات المستخدمة في البرنامج: CURLOPT_HEADER=1: يسمح بالحصول على رؤوس HTTP المرسلة من الخادم CURLOPT_NOBODY=1: يسمح بتجاهل المستند المرسل من الخادم خلف الرؤوس HTTP | |
يقوم بإجراء الاتصال بـ $URL باستخدام الخيارات المطلوبة. ويعرض على الشاشة كل ما يرسله الخادم | |
يغلق الاتصال |
البرنامج السابق بسيط إلى حد ما. ومع ذلك، لا تسمح مكتبة CURL بمعالجة استجابة الخادم بدقة، على سبيل المثال تحليلها سطراً سطراً. يقوم البرنامج التالي بنفس ما يقوم به البرنامج السابق ولكن باستخدام وظائف الشبكة الأساسية لـ PHP. وسيكون نقطة انطلاق لكتابة عميل لتطبيقنا cycledevie.php.
<?php
// syntaxe : $0 URL GET/HEAD
// il faut trois arguments
if($argc != 3){
// msg d'erreur
fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
// arrêt
exit(1);
}//if
// connexion et affichage du résultat
$résultats=getURL($argv[1],$argv[2]);
if(isset($résultats->erreur)){
// erreur
echo "L'erreur suivante s'est produite : $résultats->erreur\n";
}else{
// affichage réponse du serveur
echo $résultats->réponse;
}//if
// fin
exit(0);
//-----------------------------------------------------------------------
function getURL($URL,$header){
// se connecte à $URL
// fait un GET ou un HEAD selon la valeur de header
// la réponse du serveur forme le résultat de la fonction
// analyse de l'URL
$url=parse_url($URL);
// le protocole
if(strtolower($url["scheme"])!="http"){
$résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
return $résultats;
}//if
// la machine
$hote=$url["host"];
if(! isset($hote)){
$résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
return $résultats;
}//if
// le port
$port=$url["port"];
if(! isset($port)) $port=80;
// le chemin
$chemin=$url["path"];
// la requête
if(isset($url["query"])){
$résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
return $résultats;
}//if
// analyse de $header
$header=strtoupper($header);
if($header!="GET" && $header!="HEAD"){
// msg d'erreur
$résultats->erreur="méthode [$header] doit être GET ou HEAD";
// arrêt
return $résultats;
}//if
// ouverture d'une connexion sur le port $port de $hote
$connexion=fsockopen($hote,$port,&$errno,&$erreur);
// retour si erreur
if(! $connexion){
$résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
return $résultats;
}//if
// $connexion représente un flux de communication bidirectionnel
// entre le client (ce programme) et le serveur web contacté
// ce canal est utilisé pour les échanges de commandes et d'informations
// le protocole de dialogue est HTTP
// le client envoie la commande get pour demander l'URL /
// syntaxe get URL HTTP/1.0
// les entêtes (headers) du protocole HTTP doivent se terminer par une ligne vide
fputs($connexion, "$header $chemin HTTP/1.0\n\n");
// le serveur va maintenant répondre sur le canal $connexion. Il va envoyer toutes
// ces données puis fermer le canal. Le client lit donc tout ce qui arrive de $connexion
// jusqu'à la fermeture du canal
$résultats->réponse="";
while($ligne=fgets($connexion,10000))
$résultats->réponse.=$ligne;
// le client ferme la connexion à son tour
fclose($connexion);
// retour
return $résultats;
}//getURL
?>
دعونا نعلق على بعض النقاط في هذا البرنامج:
- يقبل البرنامج معلمتين:
- رابط http من نوع URL نريد عرض محتواه على الشاشة.
- طريقة GET أو HEAD تُستخدم حسب ما إذا كنا نريد فقط الرؤوس HTTP (HEAD) أم نص المستند المرتبط بـ URL (GET).
- يتم تمرير المعلمتين إلى الدالة getURL. وتُرجع هذه الدالة كائنًا $résultats. ويحتوي هذا الكائن على حقل erreur في حالة وجود خطأ، وحقل réponse في حالة عدم وجود خطأ. يُستخدم الحقل erreur لتخزين أي رسالة خطأ محتملة. أما الحقل réponse فهو رد الخادم الويب الذي تم الاتصال به.
- تقوم الدالة getURL بتحليل URL و$URL باستخدام الدالة parse_url. ستقوم التعليمات $url=parse_url($URL) بإنشاء المصفوفة الترابطية $url مع المفاتيح المحتملة التالية:
- scheme: بروتوكول URL (http، ftp، ...)
- host: جهاز URL
- port: المنفذ الخاص بـ URL
- path: مسار URL
- سلسلة الاستعلام: معلمات URL
سيكون عنوان URL صحيحًا إذا كان بالصيغة http://machine[:port][/chemin].
- يتم أيضًا التحقق من المعلمة $header
- بمجرد التحقق من صحة المعلمات، يتم إنشاء اتصال TCP على الجهاز ($hote، $port)، ثم يتم إرسال الأمر HTTP أو GET أو HEAD وفقًا للمعلمة $header.
- ثم نقوم بقراءة استجابة الخادم ونضعها في $résultats->réponse.
يؤدي تشغيل البرنامج إلى النتائج التالية:
dos>"e:\php43\php.exe" geturl.php http://localhost/poly/sessions/2/cycledevie.php get
HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 14:56:55 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=ea0d2673811ed069e7289d86933a4c0a; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html
<html>
<head>
<title>Gestion de sessions</title>
</head>
<body>
<h3>Cycle de vie d'une session PHP</h3>
<hr>
<br>ID session : ea0d2673811ed069e7289d86933a4c0a <br>compteur : 0
<br><a href="cycledevie.php?action=invalider&PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Invalid
er la session</a>
<br><a href="cycledevie.php?PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Recharger la page</a>
</body>
</html>
سيلاحظ القارئ اليقظ أن استجابة الخادم تختلف باختلاف برنامج العميل المستخدم. في الحالة الأولى، أرسل الخادم رأسًا HTTP: Transfer-Encoding: chunked، وهو رأس لم يتم إرساله في الحالة الثانية. ويعود ذلك إلى أن العميل الثاني أرسل الرأس HTTP: get URL HTTP/1.0 الذي يطلب URL ويشير إلى أنه يعمل باستخدام بروتوكول HTTP الإصدار 1.0، مما يُجبر الخادم على الرد عليه باستخدام نفس البروتوكول. إلا أن رأس الرسالة HTTP Transfer-Encoding: chunked ينتمي إلى بروتوكول HTTP الإصدار 1.1. ولذلك لم يستخدمه الخادم في رده. وهذا يوضح لنا أن العميل الأول قد أرسل طلبه مشيرًا إلى أنه يعمل باستخدام بروتوكول HTTP الإصدار 1.1.
نقوم الآن بإنشاء البرنامج clientCompteur الذي يُستدعى على النحو التالي:
clientCompteur URL N [JSESSIONID]
- URL: عنوان URL لتطبيق cycledevie
- N: عدد المرات التي سيتم فيها استدعاء هذا التطبيق
- PHPSESSID: معلمة اختيارية - رمز جلسة العمل
الهدف من البرنامج هو استدعاء التطبيق cycledevie.php N مرة مع إدارة ملف تعريف ارتباط الجلسة وعرض قيمة العداد التي يرسلها الخادم في كل مرة. في نهاية N استدعاءات، يجب أن تكون قيمة العداد هي N-1. فيما يلي مثال أول للتنفيذ:
dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 3
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--
[Le compteur est égal à 0]
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--
[Le compteur est égal à 1]
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--
[Le compteur est égal à 2]
يعرض البرنامج:
- رؤوس القوائم HTTP التي يرسلها إلى الخادم في صيغة --> entêteEnvoyé
- رؤوس HTTP التي يتلقاها في شكل <-- entêteReçu
- قيمة العداد بعد كل استدعاء
ونلاحظ أنه عند الاستدعاء الأول:
- لا يرسل العميل ملف تعريف ارتباط
- يرسل الخادم ملف تعريف ارتباط (Set-Cookie:)
بالنسبة للطلبات التالية:
- يقوم العميل بإعادة إرسال ملف تعريف الارتباط الذي تلقّاه من الخادم خلال الزيارة الأولى بشكل منهجي. وهذا ما سيسمح للخادم بالتعرف عليه وزيادة عداده.
- أما الخادم، فلا يعيد إرسال ملف تعريف الارتباط
نُعيد تشغيل البرنامج السابق مع تمرير الرمز أعلاه كمعلمة ثالثة:
dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 1 2425e00d1d65c2bdcbafc1ce6244f7ea
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:32:03 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--
[Le compteur est égal à 3]
نلاحظ هنا أنه منذ أول اتصال من العميل، يتلقى الخادم ملف تعريف ارتباط جلسة صالحًا. وقد يشير هذا إلى ثغرة أمنية محتملة. فإذا تمكنت من اعتراض رمز جلسة على الشبكة، فسأتمكن من انتحال صفة الشخص الذي بدأ الجلسة. في مثالنا، يمثل الطلب الأول (بدون رمز جلسة) الشخص الذي بدأ الجلسة (ربما باستخدام اسم مستخدم وكلمة مرور يمنحانه الحق في الحصول على رمز الجلسة)، بينما يمثل الطلب الثاني (مع رمز الجلسة) الشخص الذي «اخترق» رمز جلسة الطلب الأول. إذا كانت العملية الجارية عملية مصرفية، فقد يصبح الأمر مزعجًا...
كود العميل هو كما يلي:
<?php
// syntaxe : $0 URL N [PHPSESSID]
// il faut trois arguments
if($argc!=3 && $argc!=4){
// msg d'erreur
fputs(STDERR,"Syntaxe : $argv[0] URL N [PHPSESSID]");
// arrêt
exit(1);
}//if
// récupération des paramètres
$URL=$argv[1];
$N=$argv[2];
$PHPSESSID=$argv[3];
// connexion et affichage du résultat
$résultats=getURL($URL,$N,$PHPSESSID);
if(isset($résultats->erreur)){
// erreur
echo "L'erreur suivante s'est produite : $résultats->erreur\n";
}
// fin
exit(0);
//-----------------------------------------------------------------------
function getURL($URL,$N,$PHPSESSID){
// se connecte à URL
// fait un GET ou un HEAD selon la valeur de header
// la réponse du serveur forme le résultat de la fonction
// analyse de l'URL
$url=parse_url($URL);
// le protocole
if(strtolower($url["scheme"])!="http"){
$résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
return $résultats;
}//if
// la machine
$hote=$url["host"];
if(! isset($hote)){
$résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
return $résultats;
}//if
// le port
$port=$url["port"];
if(! isset($port)) $port=80;
// le chemin
$chemin=$url["path"];
// la requête
if(isset($url["query"])){
$résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
return $résultats;
}//if
// vérification de $N
if (! preg_match("/^\d+$/",$N)){
// erreur
$résultats->erreur="nombre [$N] erroné";
// fin
return $résultats;
}//if
// on fait les $N appels à $URL
for($i=0;$i<$N;$i++){
// ouverture d'une connexion sur le port $port de $hote
$connexion=fsockopen($hote,$port,&$errno,&$erreur);
// retour si erreur
if(! $connexion){
$résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
return $résultats;
}//if
// $connexion représente un flux de communication bidirectionnel
// entre le client (ce programme) et le serveur web contacté
// ce canal est utilisé pour les échanges de commandes et d'informations
// le protocole de dialogue est HTTP
// le client envoie les entêtes HTTP
// get URL HTTP/1.1
envoie($connexion, "GET $chemin HTTP/1.1\n");
// host: hote:port
envoie($connexion, "Host: $hote:$port\n");
// Connection: close
envoie($connexion, "Connection: close\n");
// Cookie: $PHPSESSID
if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
// ligne vide
envoie($connexion,"\n");
// le serveur va maintenant répondre sur le canal $connexion. Il va envoyer toutes
// ses données puis fermer le canal.
// Le client commence par lire les entêtes HTTP terminés par une ligne vide
$CHUNKED=0;
while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
// écho ligne
echo "<-- $ligne\n";
// recherche du jeton si on ne l'a pas encore trouvé
if(! $PHPSESSID){
// recherche de la ligne set-cookie
if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
// on a trouvé le jeton - on le mémorise
$PHPSESSID=$champs[1];
}//if
}//if
// recherche du mode de transfert du document
if(! $CHUNKED){
// recherche de la ligne Transfer-Encoding: chunked
if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
// transfert par morceaux
$CHUNKED=1;
}//if
}//if
}//ligne suivante
// écho ligne
echo "<-- $ligne\n";
// la lecture du document dépend de la façon dont il a été envoyé
if($CHUNKED) $document=getChunkedDoc($connexion);
else $document=getDoc($connexion);
// recherche du compteur dans le document
if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
// on a trouvé le compteur - on l'affiche
echo "\n[Le compteur est égal à $champs[1]]\n\n";
}//if
// le client ferme la connexion
fclose($connexion);
}//for i
}//getURL
//--------------------------
function getDoc($connexion){
// lecture du document sur $connexion
$doc="";
while($ligne=fread($connexion,10000))
$doc.=$ligne;
// fin
return $doc;
}//getDoc
//--------------------------
function getChunkedDoc($connexion){
// lecture du document sur $connexion
// ce document est envoyé par morceaux sous la forme
// nombre de caractères du morceau en hexadécimal
// suite du morceau
// ligne vide
// on lit la taille du morceau dans la 1ère ligne
$taille=hexdec(rtrim(fgets($connexion,10000)));
// on lit le document qui suit
$doc="";
while($taille!=0){
// lecture morceau de $taille caractères
$doc.=fread($connexion,$taille);
// ligne vide
fgets($connexion,10000);
// morceau suivant
// on lit la taille du morceau
$taille=hexdec(rtrim(fgets($connexion,10000)));
}//while
// c'est fini
return $doc;
}// getChunkedDoc
//--------------------------
function envoie($flux,$msg){
// envoie $msg sur $flux
fwrite($flux,$msg);
// echo écran
echo "--> $msg";
}//envoie
?>
دعونا نحلل النقاط المهمة في هذا البرنامج:
- يجب إجراء N عملية تبادل بين العميل والخادم. ولهذا السبب يتم تنفيذها ضمن حلقة
- في كل عملية تبادل، يفتح العميل اتصالاً TCP-IP مع الخادم. وبمجرد الحصول على هذا الاتصال، يرسل إلى الخادم رؤوس HTTP لطلبه:
<?php
...
// le client envoie les entêtes HTTP
// get URL HTTP/1.1
envoie($connexion, "GET $chemin HTTP/1.1\n");
// host: hote:port
envoie($connexion, "Host: $hote:$port\n");
// Connection: close
envoie($connexion, "Connection: close\n");
// Cookie: $PHPSESSID
if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
// ligne vide
envoie($connexion,"\n");
إذا كان الرمز المميز PHPSESSID متاحًا، يتم إرساله في شكل ملف تعريف ارتباط، وإلا فإنه لا يتم إرساله. تجدر الإشارة إلى أن العميل أشار إلى أنه يعمل باستخدام بروتوكول HTTP/1.1. وهذا ما يفسر أن الخادم سيرسل إليه لاحقًا الرأس HTTP: Transfer-Encoding: chunked الذي ينتمي إلى البروتوكول HTTP/1.1 ولكنه لا ينتمي إلى البروتوكول HTTP/1.0.
- بمجرد إرسال طلبه، ينتظر العميل رد الخادم. ويبدأ باستغلال رؤوس HTTP في هذا الرد. ويبحث فيه عن سطرين:
السطر Cookie: هو الرأس HTTP الذي يحتوي على رمز الجلسة PHPSESSID. يجب على العميل استردادها لإعادة إرسالها إلى الخادم عند التبادل التالي. أما السطر Transfer-Encoding: chunked، إذا كان موجودًا، فيشير إلى أن الخادم سيرسل المستند على شكل أجزاء. ثم يتم إرسال كل جزء إلى العميل بالشكل التالي:
إذا لم تكن السطر «Transfer-Encoding: chunked» موجودة، يتم إرسال المستند دفعة واحدة بعد السطر الفارغ من الرؤوس «HTTP». وبالتالي، فإن طريقة استلام المستند ستختلف بناءً على وجود هذا السطر أو عدم وجوده. رمز معالجة الرؤوس «HTTP» هو كما يلي:
<?php
...
// Le client commence par lire les entêtes HTTP terminés par une ligne vide
$CHUNKED=0;
while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
// écho ligne
echo "<-- $ligne\n";
// recherche du jeton si on ne l'a pas encore trouvé
if(! $PHPSESSID){
// recherche de la ligne set-cookie
if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
// on a trouvé le jeton - on le mémorise
$PHPSESSID=$champs[1];
}//if
}//if
// recherche du mode de transfert du document
if(! $CHUNKED){
// recherche de la ligne Transfer-Encoding: chunked
if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
// transfert par morceaux
$CHUNKED=1;
}//if
}//if
}//ligne suivante
- عندما يتم العثور على الرمز لأول مرة، لن يتم البحث عنه مرة أخرى في الاستدعاءات التالية للخادم. عندما يتم معالجة رؤوس HTTP في الرد، ننتقل إلى المستند الذي يتبع رؤوس HTTP. ويتم قراءة هذا المستند بشكل مختلف حسب طريقة نقله:
<?php
...
// la lecture du document dépend de la façon dont il a été envoyé
if($CHUNKED) $document=getChunkedDoc($connexion);
else $document=getDoc($connexion);
- في المستند $document الذي تم استلامه، نبحث عن السطر الذي يعطي قيمة العداد. ويتم هذا البحث هنا أيضًا باستخدام تعبير منتظم:
<?php
...
// recherche du compteur dans le document
if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
// on a trouvé le compteur - on l'affiche
echo "\n[Le compteur est égal à $champs[1]]\n\n";
}//if
- في حالة إرسال الخادم للوثيقة دفعة واحدة، يكون استلامها بسيطًا:
<?php
...
//--------------------------
function getDoc($connexion){
// lecture du document sur $connexion
$doc="";
while($ligne=fgets($connexion,10000))
$doc.=$ligne;
// fin
return $doc;
}//getDoc
- في حالة قيام الخادم بإرسال المستند على عدة أجزاء، تصبح قراءته أكثر تعقيدًا:
<?php
...
function getChunkedDoc($connexion){
// lecture du document sur $connexion
// ce document est envoyé par morceaux sous la forme
// nombre de caractères du morceau en hexadécimal
// suite du morceau
// on lit la taille du morceau dans la 1ère ligne
$taille=hexdec(rtrim(fgets($connexion,10000)));
// on lit le document qui suit
$doc="";
while($taille!=0){
// lecture morceau de $taille caractères
$doc.=fread($connexion,$taille);
// ligne vide
fgets($connexion,10000);
// morceau suivant
// on lit la taille du morceau
$taille=hexdec(rtrim(fgets($connexion,10000)));
}//while
// c'est fini
return $doc;
}// getChunkedDoc
تجدر الإشارة إلى أن جزءًا من المستند يُرسل بالصيغة التالية
لذلك نبدأ بقراءة حجم المستند. وبمجرد معرفة الحجم، نطلب من الدالة fread قراءة $taille حرفًا من التدفق $connexion ثم السطر الفارغ الذي يليه. نكرر هذه العملية حتى يرسل الخادم معلومة تفيد بأنه سيرسل مستندًا بحجم 0.
3.10.5. المثال 4
في المثال السابق، يعيد عميل الويب الرمز المميز في شكل ملف تعريف ارتباط. وقد رأينا أنه يمكنه أيضًا إعادته ضمن الطلب URL نفسه في الصيغة URL;PHPSESSID=xxx. دعونا نتحقق من ذلك. يتم تحويل البرنامج clientCompteur.php إلى clientCompteur2.php وتعديله على النحو التالي:
<?php
...
....
// le client envoie les entêtes HTTP
// get URL HTTP/1.1
if($PHPSESSID)
envoie($connexion, "GET $chemin?PHPSESSID=$PHPSESSID HTTP/1.1\n");
else envoie($connexion, "GET $chemin HTTP/1.1\n");
// host: hote:port
envoie($connexion, "Host: $hote:$port\n");
// Connection: close
envoie($connexion, "Connection: close\n");
// ligne vide
envoie($connexion,"\n");
....
وبالتالي، يطلب العميل URL من العداد عبر GET URL؛PHPSESSID=xx HTTP/1.1 ولم يعد يرسل ملف تعريف الارتباط. هذا هو التعديل الوحيد. فيما يلي نتائج الاستدعاء الأول:
dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 2
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=573212ba82303d7903caf8944ee7a86f; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--
[Le compteur est égal à 0]
--> GET /poly/sessions/2/cycledevie.php?PHPSESSID=573212ba82303d7903caf8944ee7a86f HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--
[Le compteur est égal à 1]
عند الطلب الأول، يطلب العميل URL دون رمز جلسة. فيرد عليه الخادم بإرسال رمز الجلسة إليه. ثم يعيد العميل استعلام نفس URL مرفقاً رمز الجلسة الذي تلقّاه بها. ونلاحظ أن العداد قد زاد بالفعل، مما يدل على أن الخادم قد تعرف بالفعل على أن هذه هي الجلسة نفسها.
3.10.6. المثال 5
يوضح هذا المثال تطبيقًا يتألف من ثلاث صفحات سنسميها page1 وpage2 وpage3. يجب على المستخدم الوصول إليها بهذا الترتيب:
- الصفحة 1 عبارة عن نموذج يطلب معلومة: الاسم
- الصفحة 2 عبارة عن نموذج يتم الحصول عليه استجابةً لإرسال نموذج الصفحة 1. ويطلب معلومة ثانية: العمر
- الصفحة 3 هي مستند HTML يعرض الاسم الذي تم الحصول عليه من الصفحة 1 والعمر الذي تم الحصول عليه من الصفحة 2.
هناك ثلاث عمليات تبادل بين العميل والخادم:
- في التبادل الأول، يطلب العميل نموذج الصفحة 1 ويرسله الخادم
- في عملية التبادل الثانية، يرسل العميل نموذج الصفحة 1 (الاسم) إلى الخادم. ويتلقى في المقابل نموذج الصفحة 2 أو نموذج الصفحة 1 مرة أخرى إذا كان هناك خطأ فيه.
- في التبادل الثالث، يرسل العميل نموذج page2 (العمر) إلى الخادم. ويتلقى في المقابل نموذج page3 أو نموذج page2 مرة أخرى إذا كان به خطأ. يعرض المستند page3 الاسم والعمر. تم الحصول على الاسم من قبل الخادم في التبادل الثاني، ثم «نُسي» منذ ذلك الحين. نستخدم جلسة عمل لتسجيل الاسم في التبادل الثاني حتى يكون متاحًا عند التبادل الثالث.
الصفحة page1 التي تم الحصول عليها في التبادل الأول هي كما يلي:

نملأ حقل الاسم:

نستخدم الزر [Suite] فنحصل عندئذٍ على الصفحة page2 التالية:

نملأ حقل العمر:

نستخدم الزر [Suite]، فنحصل على الصفحة التالية page3:

عند إرسال الصفحة page1 إلى الخادم، قد يعيد الخادم إرسالها مع رمز خطأ إذا كان الحقل فارغًا:

عند إرسال الصفحة page2 إلى الخادم، قد يعيدها الخادم مع رمز خطأ إذا كان العمر غير صالح:

يتكون التطبيق من ستة برامج:
يستدعي page1.php | |
يعرض الصفحة 1. يتم معالجة نموذج الصفحة 1 بواسطة etape2.php. | |
تعالج قيم نموذج الصفحة 1. في حالة وجود أخطاء، يتم إعادة عرض الصفحة 1 بواسطة page1.php، وإلا يتم عرض الصفحة 2 بواسطة page2.php. | |
تعرض الصفحة 2. تتم معالجة نموذج الصفحة 2 بواسطة etape3.php. | |
يعالج قيم نموذج الصفحة 2. في حالة وجود أخطاء، يتم إعادة عرض الصفحة 2 بواسطة page2.php وإلا يتم عرض الصفحة 3 بواسطة page3.php. | |
تعرض الصفحة 3. |
يتم معالجة الخطوة 1 من التطبيق بواسطة البرنامج etape1.php التالي:
<?php
// etape1.php
// configuration
ini_set("register_globals","off");
ini_set("display_errors","off");
// démarrage session
session_start();
$_SESSION["session"]=""; //raz variable session
// préparation page1
$requête->nom="";
$requête->erreurs=array();
// affichage page1
include "page1.php";
// fin
exit(0);
?>
تجدر الإشارة إلى النقاط التالية:
- يحتاج التطبيق إلى تتبع الجلسة. ولذلك، تبدأ كل خطوة من خطوات الجلسة جلسة جديدة.
- سيتم حفظ معلومات الجلسة في كائن $session.
- سيتم وضع المعلومات اللازمة لعرض الصفحات المختلفة (الثلاث) للتطبيق في كائن $requête.
يعرض البرنامج page1.php المعلومات الموجودة في $requête:
<? // page1.php ?>
<html>
<head>
<title>page 1</title>
</head>
<body>
<h3>Page 1/3</h3>
<form name="frmNom" method="POST" action="etape2.php">
<table>
<tr>
<td>Votre nom</td>
<td><input type="text" name="nom" value="<? echo $requête->nom ?>"></td>
</tr>
</table>
<input type="submit" value="Suite">
</form>
<? // erreurs ?
if (count($requête->erreurs)!=0){
?>
<hr>
<font color="red">
Les erreurs suivantes se sont produites
<ul>
<? for($i=0;$i<count($requête->erreurs);$i++){ ?>
<li><? echo $requête->erreurs[$i] ?>
<? }//for ?>
</ul>
<? }//if ?>
</body>
</html>
- تستقبل الصفحة كائنًا $requête يحتوي على حقلين: nom و erreurs. وتعرض الصفحة قيمة هذين الحقلين.
- كما تعرض نموذجًا. يتم إرسال قيم هذا النموذج (الاسم) عبر الطريقة POST إلى البرنامج etape2.php:
يتولى التطبيق etape2.php معالجة قيم نموذج الصفحة 1 وإعادة عرض الصفحة 1 في حالة وجود أخطاء (اسم خاطئ)، وإلا فإنه يعرض الصفحة 2 للحصول على العمر.
<?php
// etape2.php
// configuration
ini_set("register_globals","off");
ini_set("display_errors","off");
// démarrage session
session_start();
// normalement, on doit avoir un paramètre nom
// enregistré dans la requête
$requête->nom=$_POST["nom"];
// si pas de paramètres, on envoie page1 sans erreurs
if (! isset($requête->nom)){
$requête->nom="";
$requête->erreurs=array();
include "page1.php";
exit(0);
}//if
// si le paramètre nom est bien là - on vérifie sa validité
$page=calculerPage($requête);
// y-a-t-il eu des erreurs ?
if(count($page->erreurs)!=0){
// page 1 avec erreurs
$requête->erreurs=$page->erreurs;
include "page1.php";
exit(0);
}//if
// pas d'erreur - on mémorise le nom dans la session
unset($session);
$session->nom=$requête->nom;
$_SESSION["session"]=$session;
// affichage page 2
$requête->age="";
$requête->erreurs=array();
include "page2.php";
// fin
exit(0);
// ---------calculerPage
function calculerPage($requête){
// vérifie la validité de la requête $requête
// rend un tableau d'erreurs dans $page->erreurs
// au départ, pas d'erreurs
$page->erreurs=array();
// le nom ne doit pas être vide
if (preg_match("/^\s*$/",$requête->nom)){
$page->erreurs[]="Vous n'avez pas indiqué de nom";
}
// retour page
return $page;
}//calculerPage
?>
- يبدأ etape2 بالتحقق من وجود المعلمة nom المتوقعة. إذا لم يكن الأمر كذلك، فإنه يعيد عرض صفحة1 فارغة. قد يحدث هذا في حالة استدعاء etape2 مباشرةً من قبل عميل لا يمرر له أي معلمات.
- إذا كان المعلمة nom موجودة، يتم التحقق من صحتها. ويتم ذلك من خلال إجراء يُسمى calculerPage، وتتمثل مهمته في إنشاء كائن $page يحتوي على حقل erreurs، وهو عبارة عن مصفوفة للأخطاء. من الممكن وجود خطأ واحد فقط، لكننا أردنا إظهار أنه يمكننا إدارة قائمة بالأخطاء.
- في حالة وجود أخطاء، يتم إعادة عرض الصفحة page1 مصحوبة بقائمة الأخطاء.
- إذا لم تكن هناك أخطاء، يتم تخزين الاسم في الكائن $session الذي يخزن البيانات المتعلقة بالجلسة الحالية. ثم يتم عرض الصفحة page2.
يعرض البرنامج page2.php الصفحة 2:
<? // page2.php ?>
<html>
<head>
<title>page 2</title>
</head>
<body>
<h3>Page 2/3</h3>
<form name="frmAge" method="POST" action="etape3.php">
<table>
<tr>
<td>Nom</td>
<td><font color="green"><? echo $requête->nom ?></font></td>
</tr>
<tr>
<td>Votre âge</td>
<td><input type="text" name="age" size="3" value="<? echo $requête->age ?>"></td>
</tr>
</table>
<input type="submit" value="Suite">
</form>
<? // erreurs ?
if (count($requête->erreurs)!=0){
?>
<hr>
<font color="red">
Les erreurs suivantes se sont produites
<ul>
<? for($i=0;$i<count($requête->erreurs);$i++){
echo "<li>".$requête->erreurs[$i];
}//for
?>
</ul>
</font>
<? } ?>
</body>
</html>
مبدأ عمل هذه الصفحة مشابه جدًا لمبدأ عمل page2.php. وهي تعرض محتوى كائن $requête الذي يحتوي على الحقول nom و age و erreurs. وهي تعرض نموذجًا ستتم معالجة قيمه بواسطة etape3.php.
وبالتالي، فإن البرنامج etape3.php يعالج قيم نموذج الصفحة 2، والتي تم اختزالها هنا إلى العمر:
<?php
// etape3.php
// configuration
ini_set("register_globals","off");
ini_set("display_errors","off");
// démarrage session
session_start();
// on récupère les paramètres nom et age
$requête->age=$_POST["age"];
$session=$_SESSION["session"];
$requête->nom=$session->nom;
// normalement, on doit avoir un nom et un âge
if (! isset($requête->age) || ! isset($requête->nom)){
// si appel incorrect, on envoie page1
$_SESSION["session"]=""; // par précaution
$requête->nom="";
$requête->erreurs=array();
include "page1.php";
exit(0);
}//if
// le paramètre age est bien là - on vérifie sa validité
$page=calculerPage($requête);
// y-a-t-il eu des erreurs ?
if(count($page->erreurs)!=0){
// page 2 avec erreurs
$requête->erreurs=$page->erreurs;
include "page2.php";
exit(0);
}//if
// pas d'erreur - mémorisation de l'âge dans la session
$session->age=$requête->age;
$_SESSION["session"]=$session;
// affichage page 3
include "page3.php";
// fin
exit(0);
// ---------calculerPage
function calculerPage($requête){
// vérifie la validité de la requête $requête
// rend un tableau d'erreurs dans $page->erreurs
// au départ, pas d'erreurs
$page->erreurs=array();
// l'âge doit avoir un format valide
if (! preg_match("/^\s*\d{1,3}\s*$/",$requête->age)){
$page->erreurs[]="âge incorrect";
}
// retour page
return $page;
}//calculerPage
?>
- يبدأ البرنامج باسترداد الاسم من الجلسة (المستمد من الصفحة 1) والعمر من نموذج الصفحة 2. إذا كانت إحدى هاتين المعلومتين مفقودة، يتم عرض الصفحة 1.
- ثم يتم التحقق من صحة العمر. إذا كان العمر غير صحيح، يتم إعادة عرض الصفحة 2 مع قائمة بالأخطاء. إذا كان العمر صحيحًا، يتم عرض الصفحة 3. وتكتفي هذه الصفحة بعرض القيمتين (الاسم والعمر) اللتين تم الحصول عليهما من النموذجين (الصفحة 1 والصفحة 2).

