Skip to content

17. خدمات الويب

ملاحظة: يُقصد بخدمة الويب هنا أي تطبيق ويب يقدم بيانات خام يستهلكها عميل، وهو عبارة عن برنامج نصي للوحدة في الأمثلة التالية. لا نهتم بتكنولوجيا معينة، مثل REST (REpresentational State Transfer) أو SOAP (Simple Object Access Protocol) على سبيل المثال، التي تقدم بيانات خام إلى حد ما بتنسيق محدد جيدًا. تقدم REST تنسيق jSON، بينما تقدم SOAP تنسيق XML. تصف كل من هذه التقنيات بدقة الطريقة التي يجب أن يستعلم بها العميل عن الخادم والشكل الذي يجب أن تتخذه استجابة الخادم. في هذه الدورة، سنكون أكثر مرونة فيما يتعلق بطبيعة طلب العميل واستجابة الخادم. ومع ذلك، فإن البرامج النصية المكتوبة والأدوات المستخدمة قريبة من تلك الخاصة بتقنية REST.

17.1. مقدمة

نظرًا لأن برامج PHP يمكن تنفيذها بواسطة خادم WEB، فإن مثل هذا البرنامج يصبح برنامج خادم قادرًا على خدمة عدة عملاء. من وجهة نظر العميل، فإن استدعاء خدمة ويب يعادل طلب URL لتلك الخدمة. يمكن كتابة العميل بأي لغة برمجة، ولا سيما بلغة PHP. في هذه الحالة الأخيرة، يتم استخدام وظائف الشبكة التي رأيناها للتو. كما يتعين علينا أن نعرف كيف "نتواصل" مع خدمة ويب، أي أن نفهم بروتوكول http للاتصال بين خادم WEB وعملائه. كان هذا هو الهدف من الفقرة السابقة.

وقد سمح لنا عميل الويب الموصوف في الفقرة «رابط» باكتشاف جزء من بروتوكول HTTP.

Image

في أبسط صورها، تتم عمليات التبادل بين العميل والخادم على النحو التالي:

  • يقوم العميل بفتح اتصال مع المنفذ 80 لخادم الويب؛
  • يقوم بطلب مستند؛
  • يرسل خادم الويب المستند المطلوب ويغلق الاتصال؛
  • يغلق العميل بدوره الاتصال؛

يمكن أن يكون المستند من أنواع مختلفة: نص بتنسيق HTML، صورة، فيديو... يمكن أن يكون مستندًا موجودًا (مستند ثابت) أو مستندًا تم إنشاؤه على الفور بواسطة برنامج نصي (مستند ديناميكي). في الحالة الأخيرة، نتحدث عن برمجة الويب. يمكن كتابة البرنامج النصي لتوليد المستندات ديناميكيًا بعدة لغات: PHP، Python، Perl، Java، Ruby، C#، VB.net...

فيما يلي، سنستخدم البرامج النصية PHP لإنشاء مستندات نصية ديناميكيًا.

Image

  • في [1]، يقوم العميل بفتح اتصال مع الخادم، ويطلب البرنامج النصي PHP، ويرسل أو لا يرسل معلمات إلى هذا البرنامج النصي؛
  • في [2]، يقوم خادم الويب بتنفيذ البرنامج النصي PHP بواسطة المترجم PHP. يقوم البرنامج النصي بإنشاء مستند يتم إرساله إلى العميل [3]؛
  • يقوم الخادم بإنهاء الاتصال. ويقوم العميل بالمثل؛

يمكن لخادم الويب معالجة عدة عملاء في وقت واحد.

مع حزمة البرامج [Laragon]، يكون خادم الويب هو خادم Apache، وهو خادم مفتوح المصدر تابع لمؤسسة Apache (http://www.apache.org/). في التطبيقات التالية، يجب تشغيل [Laragon]:

Image

يؤدي هذا إلى تشغيل خادم الويب Apache بالإضافة إلى SGBD وMySQL.

سيتم كتابة البرامج النصية التي ينفذها خادم الويب باستخدام أداة NetBeans. وقد كتبنا حتى الآن برامج نصية PHP تُنفذ في سياق وحدة التحكم:

Image

يستخدم المستخدم وحدة التحكم لطلب تنفيذ برنامج نصي PHP وتلقي نتائجه.

في تطبيقات العميل/الخادم التالية:

  • يتم تنفيذ البرنامج النصي الخاص بالعميل في سياق وحدة التحكم؛
  • يتم تنفيذ البرنامج النصي الخاص بالخادم في سياق الويب؛

Image

لا يمكن أن يكون البرنامج النصي PHP الخاص بالخادم موجودًا في أي مكان في نظام الملفات. في الواقع، يبحث خادم الويب في المواقع المحددة في التكوين عن المستندات الثابتة والديناميكية المطلوبة منه. تؤدي الإعدادات الافتراضية لـ Laragon إلى البحث عن المستندات في المجلد <Laragon>/www حيث يمثل <Laragon> مجلد تثبيت Laragon. وبالتالي، إذا طلب عميل الويب مستند D من المجلد URL [http://localhost/D]، فسيقدم له خادم الويب المستند D الموجود في المسار [<Laragon>/www/D].

في الأمثلة التالية، سنضع البرامج النصية للخادم في المجلد [www/php7/scripts-web]. إذا كان اسم أحد البرامج النصية للخادم هو S.php، فسيتم طلبه من خادم الويب باستخدام المسار URL [http://localhost/php7/scripts-web/S.php]. سيتم عندئذ تقديم المستند [<Laragon>/www/php7/scripts-web/S.php] إليه.

Image

  • إلى [1]، المجلد [<laragon>/www]؛
  • إلى [2]، المجلد [php7/scripts-web]؛

لإنشاء نصوص برمجية للخادم باستخدام NetBeans، سنتبع الخطوات التالية:

Image

  • في [1-2]، نقوم بإنشاء مشروع جديد
  • في [3-4]، نختار الفئة [PHP] والمشروع [PHP Application]

Image

  • في [5]، اسم المشروع؛
  • في [6]، مجلد المشروع في نظام الملفات. لاحظ أن هذا المجلد موجود في المجلد [<laragon>/www] حيث يجب أن يكون؛
  • في [7-8]، اقبل القيم الافتراضية المقترحة؛
  • في [9-10]، اقبل القيم الافتراضية المقترحة. في [10]، لاحظ أن URL من البرامج النصية التي سنضعها في هذا المشروع ستبدأ بمسار [http://localhost/php7/scripts-web/]؛

Image

  • في [11]، تُعرض عليكم أطر عمل الويب المكتوبة بلغة PHP. وتُعد هذه الأطر ضرورية بمجرد أن يكتسب تطبيق الويب بعض الحجم؛
  • في [12]، يمكن إضافة مكتبات PHP باستخدام أداة [Composer]. استخدمنا هذه الأداة مرتين في نافذة [Terminal] في Laragon:
    • لتثبيت المكتبة [SwiftMailer] التي تتيح إرسال رسائل البريد الإلكتروني؛
    • لتثبيت المكتبة [php-mime-mail-parser] التي تتيح قراءة رسائل البريد الإلكتروني؛
  • في [13]، بمجرد الموافقة على معالج إنشاء المشروع، يظهر المشروع في [13] في علامة تبويب المشاريع؛

17.2. كتابة صفحة ثابتة

ملاحظة: للمتابعة، يجب تشغيل [Laragon].

سنوضح كيفية إنشاء صفحة ثابتة HTML (لغة الترميز HyperText) باستخدام NetBeans:

Image

  • في [1-5]، نقوم بإنشاء مجلد باسم [01]؛

Image

Image

  • في [6-12]، نقوم بإنشاء ملف HTML [exemple-01.html]؛

يتم إنشاء الملف [exemple-01.html] مملوءًا مسبقًا على النحو التالي (مايو 2019):


<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

لنقوم بتعديل محتواه على النحو التالي:


<!DOCTYPE html>
<html>
    <head>
        <title>PHP7 par l'exemple</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div><b>Ceci est un exemple de page statique</b></div>
    </body>
</html>

لقد قمنا بتغيير عنوان الصفحة (السطر 4) ومحتواها (السطر 9).

والآن دعونا نعرض هذه الصفحة HTML عبر خادم Apache الخاص بـ Laragon:

Image

  • في [1-2]، نقوم بعرض الصفحة عبر خادم Apache الخاص بـ Laragon؛
  • في [3]، URL للصفحة المعروضة؛
  • في [4]، العنوان الذي قمنا بتعديله؛
  • في [5]، المحتوى الذي قمنا بتعديله؛

الصفحة المعروضة هي صفحة ثابتة: يمكن تحميلها في المتصفح (F5) أي عدد من المرات نريده، وسيظل المحتوى المعروض هو نفسه دائمًا.

تتيح معظم المتصفحات الوصول إلى البيانات المتبادلة بين العميل والخادم، وهي تلك التي تم وصفها في الفقرة «الرابط». مع متصفح Firefox (مايو 2019)، يجب إدخال F12 للوصول إلى هذه البيانات:

Image

كما هو موضح في [1]، لنقم بإعادة تحميل الصفحة (F5):

Image

  • في [2]، المستند الذي تم تحميله بواسطة المتصفح: نقوم بتحديده؛

Image

  • في [5]، يتم تحديد المستند المراد تحليله؛
  • في [3-4]، نطلب عرض التبادلات بين العميل والخادم؛
  • في [6]، هذه التبادلات؛

Image

  • في [7]، يتم تحديد علامة تبويب الرؤوس؛
  • في [8]، URL المطلوب من قبل المتصفح؛
  • في [9]، الأمر المرسل إلى الخادم هو [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1]؛
  • في [10]، الرؤوس HTTP التي أرسلها المتصفح (العميل) بعد ذلك؛
  • في [11]، الرؤوس HTTP الخاصة برد الخادم؛

Image

  • في [12-14]، رد الخادم المرسل بعد رؤوس HTTP؛
  • في [14]، نرى أن متصفح العميل قد تلقى الصفحة HTML التي قمنا بإنشائها. ثم قام بتفسير هذا الكود لعرض ما يلي:

Image

17.3. إنشاء صفحة ديناميكية في PHP

نقوم الآن بكتابة صفحة ديناميكية باستخدام PHP:

Image

Image

  • في [1-8]، نقوم بإنشاء صفحة [exemple-01.php]؛

يتم إنشاء الملف [exemple-01.php] مملوءًا مسبقًا على النحو التالي (مايو 2019):


<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <?php
        // ضع الكود الخاص بك هنا
        ?>
    </body>
</html>

نقوم بتعديل الكود أعلاه على النحو التالي:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // الوقت: عدد الميلي ثانية بين الوقت الحالي و 01/01/1970
        // تنسيق عرض التاريخ والوقت
        // d: اليوم برقمين
        // m: الشهر برقمين
        // y: السنة برقمين
        // H: الساعة 0,23
        // I: الدقائق
        // s: الثواني
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

تعليقات

  • السطر 5: قمنا بتغيير عنوان الصفحة؛
  • السطر 17: يكتب التاريخ والوقت الحاليين؛

بشكل أساسي، يقوم البرنامج النصي PHP أعلاه بكتابة الوقت الحالي على وحدة التحكم. ومع ذلك، عند تنفيذه بواسطة خادم ويب، يتم إعادة توجيه مخرجات الأمر [print] — التي ترتبط عادةً بوحدة التحكم الخاصة بتنفيذ البرنامج النصي — إلى الاتصال الذي يربط الخادم بعميله. وبالتالي، في سياق الويب، يرسل البرنامج النصي أعلاه الوقت الحالي في شكل نص إلى العميل، وهو في هذه الحالة متصفح.

لنقم بتشغيل البرنامج النصي [exemple-01.php]:

Image

  • إلى [3]، و URL المطلوب من خادم الويب Apache؛
  • إلى [4]، وهو عنوان الصفحة الذي قمنا بتغييره؛
  • في [5]، المحتوى الذي تم إنشاؤه بواسطة الأمر [print]؛

لدينا هنا صفحة ديناميكية لأنه إذا أعدنا تحميل الصفحة عدة مرات في المتصفح (F5)، فإن محتواها يتغير (تتغير الساعة).

تلقى المتصفح تدفقًا HTML. لمعرفة هذا التدفق، يجب إظهار كود المصدر للصفحة في المتصفح:

Image

  • للحصول على القائمة [1]، انقر بزر الماوس الأيمن على الصفحة في المتصفح؛
  • في [2]، يظهر URL الخاص بالصفحة [exemple-01.php] ولكن مسبوقًا بـ [view-source :] و[3]؛
  • في [4]، المحتوى HTML الذي عرضه المتصفح؛

لذا يجب أن نتذكر أن البرنامج النصي PHP المخصص للتنفيذ بواسطة خادم ويب يجب أن ينتج تدفقًا HTML.

لنلقِ نظرة الآن (F12) على الرؤوس HTTP التي أرسلها الخادم إلى متصفح العميل:

Image

  • في [3]، يوجد رأس HTTP الذي لم يكن موجودًا عند طلب الصفحة الثابتة. يُظهر هذا الرأس أن استجابة الخادم تم إنشاؤها بواسطة برنامج نصي PHP؛

لقد رأينا أن استجابة الخادم (التدفق HTML هنا) يمكن أن يتم إنشاؤها بواسطة البرنامج النصي PHP. كما يمكن للبرنامج النصي إنشاء الرؤوس HTTP وجميع عناصر استجابة الخادم تقريبًا.

17.4. أساسيات لغة HTML

لن يتطرق هذا الفصل بالتفصيل إلى البرمجة بلغة WEB في PHP. تم تطوير تطبيق ويب MVC في الفقرة الرابط. بل يركز هذا الفصل على خدمات الويب: وهي صفحات PHP التي تقدم، عبر خادم ويب، بيانات موجهة إلى عملاء PHP آخرين. ومع ذلك، رأينا أنه من المفيد تزويد القارئ ببعض أساسيات لغة HTML.

يمكن لمتصفح الويب عرض مستندات متنوعة، وأكثرها شيوعًا هو مستند HTML (لغة الترميز HyperText). وهو عبارة عن نص منسق بعلامات على شكل <balise>texte</balise>. وبالتالي، فإن النص <b>important</b> سيعرض النص important بخط عريض. توجد علامات منفردة، مثل العلامة <hr/> التي تعرض خطًا أفقيًا. لن نستعرض العلامات التي يمكن العثور عليها في نص HTML. هناك العديد من برامج WYSIWYG التي تتيح إنشاء صفحة WEB دون كتابة سطر واحد من كود HTML. تقوم هذه الأدوات تلقائيًّا بإنشاء كود HTML لتصميم الصفحة الذي يتم إجراؤه باستخدام الماوس وعناصر التحكم المحددة مسبقًا. وبذلك يمكن إدراج (باستخدام الماوس) جدولًا في الصفحة، ثم الاطلاع على كود HTML الذي أنشأه البرنامج لمعرفة العلامات التي يجب استخدامها لتعريف جدول في صفحة WEB. الأمر ليس أكثر تعقيدًا من ذلك. من ناحية أخرى، فإن معرفة لغة HTML أمر لا غنى عنه، حيث يتعين على تطبيقات الويب الديناميكية أن تولد بنفسها الكود HTML لإرساله إلى العملاء WEB. يتم إنشاء هذا الكود برمجيًا، ومن الضروري بالطبع معرفة ما يجب إنشاؤه حتى يحصل العميل على صفحة الويب التي يرغب فيها.

باختصار، ليس هناك حاجة لمعرفة لغة HTML بالكامل لبدء البرمجة على الويب. ومع ذلك، فإن هذه المعرفة ضرورية ويمكن اكتسابها من خلال استخدام برامج إنشاء صفحات الويب مثل WYSIWYG، مثل WEB وDreamWeaver وعشرات البرامج الأخرى. هناك طريقة أخرى لاكتشاف خفايا لغة HTML وهي تصفح الويب وعرض الكود المصدري للصفحات التي تتميز بخصائص مثيرة للاهتمام وما زالت غير معروفة بالنسبة لك.

لنأخذ المثال التالي الذي يعرض بعض العناصر التي يمكن العثور عليها في مستند WEB مثل:

  • جدول؛
  • صورة؛
  • رابط.

Image

يكون المستند HTML بالشكل العام التالي:

<html> <head> <title>عنوان</title> ... </head> <سمات body> ... </body></html>

يتم تأطير المستند بأكمله بعلامات <html>…</html>. ويتكون من جزأين:

  1. <head>…</head>: هذا هو الجزء غير القابل للعرض من المستند. وهو يوفر معلومات للمتصفح الذي سيعرض المستند. غالبًا ما نجد فيه العلامة <title>…</title> التي تحدد النص الذي سيتم عرضه في شريط عنوان المتصفح. ويمكن أن نجد فيه علامات أخرى، لا سيما العلامات التي تحدد الكلمات المفتاحية للمستند، وهي الكلمات المفتاحية التي تستخدمها محركات البحث لاحقًا. كما يمكن العثور في هذا الجزء على نصوص برمجية، مكتوبة غالبًا بلغة جافا سكريبت أو VBScript، والتي سيتم تنفيذها بواسطة المتصفح.
  1. <body attributs>…</body>: هذا هو الجزء الذي سيعرضه المتصفح. تشير العلامات HTML الموجودة في هذا الجزء إلى المتصفح الشكل المرئي «المطلوب» للوثيقة. سيقوم كل متصفح بتفسير هذه العلامات بطريقته الخاصة. وبالتالي، قد يعرض متصفحان نفس المستند على الويب بشكل مختلف. وهذا عادةً ما يمثل أحد التحديات التي يواجهها مصممو الويب.

الرمز HTML لمستندنا النموذجي هو كما يلي:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Quelques balises HTML</title>
    </head>

    <body style="background-image: url(images/standard.jpg)">
        <h1 style="text-align: left">Quelques balises HTML</h1>
        <hr />

        <table border="1">
            <thead>
                <tr>
                    <th>Colonne 1</th>
                    <th>Colonne 2</th>
                    <th>Colonne 3</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>cellule(1,1)</td>
                    <td style="text-align: center;">cellule(1,2)</td>
                    <td>cellule(1,3)</td>
                </tr>
                <tr>
                    <td>cellule(2,1)</td>
                    <td>cellule(2,2)</td>
                    <td>cellule(2,3</td>
                </tr>
            </tbody>
        </table>
        <br/><br/>
        <table border="0">
            <tr>
                <td>Une image</td>
                <td>
                    <img border="0" src="images/cerisier.jpg"/></td>
            </tr>
            <tr>
                <td>Le site de Polytech'Angers</td>
                <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
            </tr>
        </table>
    </body>
</html>
Elément
العلامات والأمثلة HTML
titre du document
<title>بعض العلامات HTML</title> (السطر 5)
سيظهر النص [Quelques balises HTML] في شريط عنوان المتصفح الذي سيعرض المستند
barre horizontale
<hr />: يعرض خطًا أفقيًا (السطر 10)
tableau
<سمات الجدول>….</table>: لتعريف الجدول (السطران 12 و32)
<thead>…</thead>: لتعريف عناوين الأعمدة (السطران 13 و19)
<tbody>…</tbody>: لتعريف محتوى الجدول (السطر 20، 31)
<tr attributs>…</tr>: لتعريف صف (السطران 21 و25)
<td سمات>…</td>: لتعريف خلية (السطر 22)
أمثلة:
<table border="1">…</table>: تحدد السمة border سماكة حدود الجدول
<td style="text-align: center;">cellule(1,2)</td> (السطر 23): يحدد خلية سيكون محتواها cellule(1,2). سيتم توسيط هذا المحتوى أفقيًا (text-align: center).
image
<img border="0" src="images/cerisier.jpg"/> (السطر 38): يحدد صورة بدون حدود (border=0")، وملفها المصدر هو [images/cerisier.jpg] على خادم الويب (src="images/cerisier.jpg"). يوجد هذا الرابط في مستند ويب تم الحصول عليه باستخدام URL http://localhost/php7/scripts-web/01/balises.html. كما سيطلب المتصفح ملف URL http://localhost/php7/scripts-web/01/images/cerisier.jpg للحصول على الصورة المشار إليها هنا.
lien
<a href="http://www.polytech-angers.fr/fr/index.html">هنا</a> (السطر 42): يجعل النص ici بمثابة رابط إلى URL http://www.polytech-angers.fr/fr/index.html.
fond de page
<body style="background-image: url(images/standard.jpg)"> (السطر 8): يشير إلى أن الصورة التي ستُستخدم كخلفية للصفحة موجودة في URL [images/standard.jpg] على الخادم WEB. في سياق مثالنا، سيطلب المتصفح من http://localhost/php7/scripts-web/01/images/standard.jpg الحصول على هذه الصورة الخلفية.

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

  1. http://localhost/php7/scripts-web/01/images/balises.html للحصول على مصدر المستند HTML
  2. http://localhost/php7/scripts-web/01/images/cerisier.jpg للحصول على الصورة cerisier.jpg
  3. http://localhost/php7/scripts-web/01/images/standard.jpg للحصول على صورة الخلفية standard.jpg

وهذا ما تظهره عمليات التبادل عبر الشبكة بين العميل والخادم (F12 في المتصفح):

Image

  • في [3-5]، نرى الطلبات الثلاثة التي أرسلها المتصفح؛

17.5. تحويل صفحة ثابتة إلى صفحة ديناميكية

لنوضح كيف يمكننا تحويل الصفحة HTML [exemple-01.html] إلى صفحة ديناميكية. لننسخ المحتوى

Image

لقد قمنا بنسخ محتوى [exemple-01.html] إلى الملف [page-01.php]. إذا قمنا بتنفيذ هذا البرنامج النصي للويب [2]، فسنحصل على ما يلي في المتصفح:

Image

  • في [3]، URL المطلوب؛
  • في [4]، عنوان الصفحة؛
  • في [5]، محتوى الصفحة؛

إذا قمنا بعرض الكود الذي استلمه المتصفح، فسنجد ما يلي:

Image

  • في [7]، لدينا الرمز HTML الموضوع في البرنامج النصي [exemple-01.php]

قام المُفسر PHP بتفسير البرنامج النصي [page-01.php] وأنتج نفس التدفق HTML الذي أنتجته الصفحة الثابتة [exemple-01.html]. في البرنامج النصي [page-01.php]، لم يكن هناك PHP، بل كان هناك فقط HTML. وهكذا نتعلم شيئًا: عندما يجد المترجم PHP HTML في البرنامج النصي PHP، فإنه لا يلمسه ويرسله كما هو إلى العميل.

الآن، لنضع بعض التعليمات PHP في البرنامج النصي [page-01.php] حتى يكون للمترجم PHP ما يقوم به:


<!DOCTYPE html>
<html>
    <head>
        <title><?php print $page->title ?></title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div><b><?php print $page->contents ?></b></div>
    </body>
</html>

في السطرين 4 و9، أضفنا كود PHP لتوليد عنوان الصفحة ومحتواها ديناميكيًا. نفترض هنا أن المتغير [$page] هو كائن يحتوي على البيانات المراد عرضها.

إذا قمنا بتنفيذ هذا الرمز الجديد، فسنحصل على النتيجة التالية في المتصفح:

Image

  • في [1]، URL المطلوب؛
  • في [2]، تعذر عرض عنوان الصفحة لأن المتغير [$page] لم يكن مُعرَّفًا؛
  • في [3]، الأمر نفسه بالنسبة للمحتوى؛

الآن، لنكتب البرنامج النصي التالي [exemple-02.php]:

Image

سيكون البرنامج النصي [exemple-02.php] كما يلي:


<?php

// يتم تحديد عناصر الصفحة المراد عرضها
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// يتم عرض [page-01]
require_once "page-01.php";
  • الأسطر 4-6: نحدد الكائن [$page]؛
  • السطر 8: يتم تضمين البرنامج النصي [page-01.php]. وسيتم تفسير كود هذا البرنامج النصي بدوره:
    • المتغير [$page] أصبح الآن مُعرَّفًا، وسيقوم المُترجم PHP باستخدامه؛
    • سيتم إرسال الرمز HTML من [page-01.php] إلى العميل كما هو؛
    • سيتم تضمين نتائج العمليات PHP و [print] في تدفق النص المرسَل إلى العميل؛

والآن، إذا قمنا بتنفيذ البرنامج النصي للويب [exemple-02.php]، فسنحصل على ما يلي في المتصفح:

Image

إذا قمنا بعرض المحتوى النصي الذي استلمه المتصفح:

Image

  • تم استبدال الرموز PHP التي كانت في [2] و [3] بنتائج الأمرين [print

من هذا المثال، يمكننا استخلاص أمرين:

  • يمكن عزل الصفحات HTML المخصصة للمتصفح في نصوص برمجية PHP لا تحتوي إلا على هذا الرمز HTML وبعض الأجزاء الديناميكية التي تم إنشاؤها بواسطة الرمز PHP. يجب أن يكون هناك أقل قدر ممكن من PHP في هذه الصفحات؛
  • يجب عزل كل المنطق الذي يولد البيانات الديناميكية المضمنة في صفحات HTML في نصوص برمجية PHP خالصة، لا تحتوي على أي كود لتصميم الصفحات (HTML، CSS، جافا سكريبت...)؛

وهذا يسمح بفصل المهام:

  • مهمة إنشاء صفحات الويب المراد عرضها (HTML، CSS، جافا سكريبت...)؛
  • مهمة منطق تطبيق الويب الذي نقوم ببنائه. يمكن تنفيذ هذا المنطق باستخدام بنية ثلاثية الطبقات، تمامًا كما فعلنا مع نصوص الأوامر؛

بعد ذلك، سنقوم بإنشاء نصوص برمجية ويب مخصصة؛

  • وستقوم هذه البرامج النصية بإرسال البيانات فقط إلى العميل دون أي عناصر تجميلية (HTML، CSS، جافا سكريبت). وبالتالي، ستكون بمثابة خوادم بيانات بدلاً من صفحات ويب؛
  • وسيكون عملاء هذه البرامج النصية على الويب عبارة عن برامج نصية على وحدة التحكم تتولى استرداد البيانات المرسلة من الخادم واستخدامها في أغراض معينة؛

17.6. تطبيق عميل/خادم للتاريخ/الوقت

ننتقل الآن إلى التكوين التالي:

Image

سنقوم بكتابة:

  • نص برمجي ويب [1] يرسل إلى عميله التاريخ والوقت الحاليين؛
  • نص برمجي لوحدة التحكم [2] سيكون عميل النص البرمجي للويب: سيسترد التاريخ والوقت المرسَلين من النص البرمجي للويب ويعرضهما على وحدة التحكم؛

Image

  • في [1]، البرنامج النصي للويب [date-time-server.php]؛
  • إلى [2]، وهو البرنامج النصي لوحدة التحكم [date-time-client] الذي يعمل كعميل للبرنامج النصي على الويب؛

17.6.1. البرنامج النصي الخاص بالخادم

لقد كتبنا بالفعل برنامجًا نصيًّا على الويب يُنشئ التاريخ والوقت الحاليين في الفقرة الرابط. وكان هذا هو البرنامج النصي [exemple-01.php] التالي:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // time: عدد الميلي ثانية منذ 01/01/1970
        // تنسيق عرض التاريخ والوقت
        // d: اليوم برقمين
        // m: الشهر برقمين
        // y: السنة برقمين
        // H: الساعة 0,23
        // i: الدقائق
        // s: الثواني
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

قلنا إننا سنكتب خوادم بيانات: بيانات خام بدون تنسيق HTML. سيكون البرنامج النصي للخادم [date-time-server.php] كما يلي:


<?php

// يتم تعيين الرأس HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// يتم إرسال التاريخ والوقت
// الوقت: عدد الميلي ثانية منذ 01/01/1970
// تنسيق عرض التاريخ والوقت
// d: اليوم برقمين
// m: الشهر برقمين
// y: السنة برقمين
// H: الساعة 0,23
// i: الدقائق
// s: الثواني
print date("d/m/y H:i:s", time());
  • السطر 4: نحدد الرأس HTTP [Content-Type] الذي يخبر العميل بطبيعة المستند الذي سيتلقاه. حتى الآن، كان [Content-Type] هو: [Content-Type: text/html; charset=UTF-8]. هنا، نُعلم العميل أن المستند عبارة عن نص بدون تنسيق HTML. هذا الأمر غير مهم بالنسبة لعميل وحدة التحكم لدينا الذي لن يحاول الاستفادة من هذا الرأس. لكنه أكثر أهمية بالنسبة لمتصفحات العملاء التي تستفيد من هذا الرأس؛

لنقم بتنفيذ هذا البرنامج النصي للخادم:

Image

إذا فحصنا في المتصفح استجابة الخادم (F12)، نرى في [5] الرأس HTTP الذي حدده البرنامج النصي للخادم، وفي [8]، المستند النصي المستلم؛

Image

17.6.2. برنامج العميل النصي

في الفقرة السابقة، قمنا بتطوير عدة عملاء HTTP. يمكننا استخدامها لاسترداد المستند النصي الذي أرسله البرنامج النصي للخادم [date-time-server.php]. لكننا لن نقوم بذلك. وكما فعلنا مع البروتوكولين SMTP و IMAP، سنستخدم مكتبة خارجية، وهي المكون [HttpClient] من إطار عمل Symfony [https://symfony.com/doc/master/components/http_client.html].

وكما هو الحال مع المكتبتين السابقتين، نستخدم الأداة [Composer] لتثبيت المكون [HttpClient] من Symfony. في نافذة [Terminal] في Laragon (انظر الفقرة الرابط)، نكتب الأمر التالي:

Image

  • في [3]، تأكد من أنك في المجلد [<laragon>/www/] حيث <laragon> هو مجلد تثبيت Laragon؛
  • في [4]، الأمر [composer] الذي يقوم بتثبيت مكتبة Symfony [HttpClient
  • في [5]، لم يتم تثبيت أي شيء لأن المكتبة [HttpClient] كانت قد تم تثبيتها مسبقًا على هذا الجهاز؛
  • في [6-7]، تظهر مجلدات جديدة في [<laragon>/www/vendor/symfony]؛

بدلاً من [5]، يجب أن يكون لديك شيء مثل التالي:


C:\myprograms\laragon-lite\www
? composer require symfony/http-client
Using version ^4.3 for symfony/http-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-php73 (v1.11.0): Downloading (100%)
  - Installing symfony/http-client-contracts (v1.1.1): Downloading (100%)
  - Installing psr/log (1.1.0): Loading from cache
  - Installing symfony/http-client (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files

تأكد من أن المجلد [<laragon>/www/vendor] جزء من الفرع [Include Path] لمشروعك (انظر الفقرة ذات الرابط):

Image

بعد ذلك، يمكننا كتابة البرنامج النصي لوحدة التحكم [date-time-client.php]:

Image

سيستخدم البرنامج النصي [date-time-client.php] الملفين التاليين: jSON و [config-date-time-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server.php"
}
  • السطر 2: URL من البرنامج النصي للخادم؛

سيكون البرنامج النصي للعميل [date-time-client.php] كما يلي:


<?php

// عميل خدمة التاريخ/الساعة
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-date-time-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create();

try {
  // إجراء الاستعلام
  $response = $httpClient->request('GET', $config['url']);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // استرداد نص الرد
  $content = $response->getContent();
  // يتم عرضه
  print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
  // عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
  exit;
}

تعليقات

  • السطر 10: كما فعلنا مع المكتبات السابقة، نقوم بتحميل الملف [<laragon>/www/vendor/autoload.php
  • السطر 11: نعلن عن الفئة [HttpClient] التي سنستخدمها؛
  • الأسطر 13-24: نسترد تكوين البرنامج النصي من القاموس [$config
  • السطر 27: ننشئ كائنًا من النوع [HttpClient]؛
  • السطر 31: نطلب URL من البرنامج النصي للخادم باستخدام الأمر GET: [GET URL HTTTP/1.1]. هذه العملية غير متزامنة. يستمر التنفيذ في السطر 33 دون انتظار استلام الرد؛
  • السطر 33: يتم الاستعلام عن حالة الرد. يوجد هذا الحالة في الرأس الأول HTTP الذي أرسله الخادم. وبالتالي، إذا كان هذا الرأس هو [HTTP/1.1 200 OK]، فإن حالة الاستجابة هي 200. هذه العملية معطلة: لا يتم العودة منها إلا بعد أن يتلقى العميل الاستجابة الكاملة من الخادم؛
  • السطر 37: نطلب الرؤوس HTTP الخاصة بالاستجابة؛
  • السطر 42: يتم طلب المستند الذي أرسله الخادم: ونعلم أن هذا المستند هنا عبارة عن نص.
  • الأسطر 45-49: في حالة حدوث خطأ، يتم عرض رسالة الخطأ؛

عند تشغيل البرنامج النصي للعميل (يجب تشغيل Laragon حتى يمكن الوصول إلى البرنامج النصي للخادم)، نحصل على النتيجة التالية على وحدة التحكم:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Thu, 30 May 2019 14:42:03 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
content-length: 17
content-type: text/plain; charset=UTF-8
---Réponse du serveur : [30/05/19 14:42:03]

يتم بالفعل استرداد تاريخ ووقت اللحظة الحالية في السطر 8.

قد نشعر بالفضول لمعرفة ما أرسله البرنامج النصي للعميل إلى الخادم. ولهذا الغرض، سنستخدم خادمنا العام TCP (انظر الفقرة «الرابط»):

Image

  • في [1]، مجلد الأدوات المساعدة؛
  • في [2]، يتم تشغيل الخادم TCP على المنفذ 100؛
  • في [3]، في انتظار إدخال أمر عبر لوحة المفاتيح؛

نقوم بتعديل ملف إعدادات البرنامج النصي [date-time-client.php]:


{
    "url": "http://localhost:100/php7/scripts-web/02/date-time-server.php"
}

هذه المرة، يتصل العميل بالخادم [localhost] على المنفذ 100. وبالتالي، سيتم استدعاء خادمنا العام TCP. عند تشغيل البرنامج النصي [date-time-client.php] في وحدة التحكم، تتغير وحدة التحكم الخاصة بالخادم العام TCP على النحو التالي:

Image

  • إلى [3]، الأمر HTTP GET الذي أنشأه البرنامج النصي للعميل؛
  • إلى [4]، توقيع البرنامج النصي الخاص بوحدة التحكم؛
  • في [5]، استجابة الخادم لبرنامج العميل النصي. تجدر الإشارة إلى أن هذه ليست استجابة HTTP صالحة:
    • يجب أن تحتوي على رؤوس HTTP؛
    • ثم سطر فارغ؛
    • ثم المستند النصي المرسل إلى العميل؛
  • في [6]، يتم إغلاق الاتصال مع البرنامج النصي للعميل حتى يكتشف هذا الأخير أنه تلقى الرد بالكامل؛

من جانب البرنامج النصي للعميل، يظهر ما يلي على وحدة التحكم:

Image

  • في [7]، ما تلقّاه عميل Symfony؛

17.6.3. برنامج الخادم – الإصدار 2

بشكل أساسي، وظائف PHP لكتابة برنامج ويب ليست موجهة للكائنات. من جانب الخادم، نضطر إذن إلى المزج بين الفئات والوظائف التقليدية لـ PHP. وللحصول على أسلوب كتابة أكثر اتساقًا، سنستخدم مكتبة [HttpFoundation] الخاصة بإطار عمل Symfony. فقد قامت هذه المكتبة بتغليف جميع الوظائف التقليدية PHP الخاصة بخدمة الويب في نظام من الفئات والواجهات. تتوفر وثائق المكتبة على الرابط URL [https://symfony.com/doc/current/components/http_foundation.html] (مايو 2019).

لتثبيت المكتبة، نتبع الخطوات التالية في محطة عمل Laragon (انظر الفقرة ذات الرابط):

Image

  • [2-3]: تأكد من أنك موجود في المجلد [<laragon>/www]؛
  • [4]: الأمر [composer] الذي سيقوم بتثبيت المكتبة [HttpFoundation]؛
  • [5]: في هذا المثال، كانت المكتبة مثبتة بالفعل؛

عند التثبيت الأول، من المفترض أن تظهر سجلات وحدة التحكم على النحو التالي:


C:\myprograms\laragon-lite\www
? composer require symfony/http-foundation
Using version ^4.3 for symfony/http-foundation
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing symfony/mime (v4.3.0): Downloading (100%)
  - Installing symfony/http-foundation (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files

الإصدار الثاني من خادم الويب [date-time-server-2.php] هو التالي:


<?php

// استخدام مكتبات Symfony

// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;

// تحديد رأس Content-Type
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");

// يتم تعيين محتوى الرد
//
// إرسال التاريخ والوقت
// الوقت: عدد الميلي ثانية منذ 01/01/1970
// تنسيق عرض التاريخ والوقت
// d: اليوم برقمين
// m: الشهر برقمين
// y: السنة برقمين
// H: الساعة 0,23
// i: الدقائق
// s: الثواني
$response->setContent(date("d/m/y H:i:s", time()));

// يتم إرسال الرد
$response->send();

تعليقات

  • السطر 7: تتولى الفئة [Response] من مكتبة Symfony [HttpFoundation] إدارة الاستجابة الكاملة لعملاء خدمة الويب؛
  • السطر 10: إنشاء مثيل للفئة [Response]؛
  • السطر 11: يُشار إلى أن الرد من النوع [text/plain]؛
  • السطر 12: الرد عبارة عن نص من نوع UTF-8؛
  • السطر 25: يتم تحديد مستند الرد، وهو ما طلبه العميل؛
  • السطر 28: يتم إرسال الرد إلى العميل؛

17.6.4. نص برمجي العميل – الإصدار 2

لم يتغير البرنامج النصي للعميل. تم تغيير ملف التكوين الخاص به فقط [config-date-time-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server-2.php"
}

النتائج هي نفسها كما في الإصدار 1.

17.7. خادم بيانات jSON

يمكن أن تتألف استجابة البرنامج النصي على الويب من عدة بيانات يمكن تجميعها في جداول وكائنات. ويمكن للبرنامج النصي عندئذٍ إرسال هذه العناصر المختلفة ضمن سلسلة jSON التي سيقوم العميل بفك تشفيرها.

Image

17.7.1. نص البرمجة الخاص بالخادم

يستخدم البرنامج النصي [json-server.php] الفئة [Personne] التالية:


<?php

namespace Modèles;

class Personne implements \JsonSerializable {
  // السمات
  private $nom;
  private $prénom;
  private $âge;

  // تحويل مصفوفة مرتبطة إلى كائن [Personne]
  public function setFromArray(array $assoc): Personne {
    // تتم تهيئة الكائن الحالي باستخدام المصفوفة الترابطية
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // النتيجة
    return $this;
  }

  // دالات الاسترجاع والتعيين
  public function getNom() {
    return $this->nom;
  }

  public function getPrénom() {
    return $this->prénom;
  }

  public function setNom($nom) {
    $this->nom = $nom;
    return $this;
  }

  public function setPrénom($prénom) {
    $this->prénom = $prénom;
    return $this;
  }

  public function getÂge() {
    return $this->âge;
  }

  public function setÂge($âge) {
    $this->âge = $âge;
    return $this;
  }

  // toString
  public function __toString(): string {
    return "Personne [$this->prénom, $this->nom, $this->âge]";
  }

  // تنفيذ واجهة JsonSerializable
  public function jsonSerialize(): array {
    // يتم إرجاع مصفوفة مرتبطة باستخدام سمات الكائن كمفاتيح
    // يمكن بعد ذلك ترميز هذا المصفوفة إلى jSON
    return get_object_vars($this);
  }

  // تحويل jSON إلى كائن [Personne]
  public static function jsonUnserialize(string $json): Personne {
    // يتم إنشاء شخص من السلسلة jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }

}

تعليقات

  • السطر 5: تُنفذ الفئة واجهة PHP [JsonSerializable]. وهذا يفرض عليها تنفيذ الطريقة [jsonSerialize] الواردة في الأسطر 55-59. يجب أن تُرجع الطريقة مصفوفة مرتبطة يجب تسلسلها إلى jSON. عند استخدام التعبير [json_encode($personne)]، تتحقق الدالة [json_encode] مما إذا كانت الفئة [Personne] تُنفِّذ الواجهة [JsonSerializable]. إذا كان الأمر كذلك، يصبح التعبير [json_encode($personne→serialize())]؛
  • الأسطر 12-19: لا تحتوي الفئة على منشئ، بل على مُهيئ. وبالتالي، يمكن إنشاء مثيل للفئة [Personne] باستخدام التعبير [(new Personne())→setFromArray($array)]. يمكن أن يكون هناك أنواع مختلفة من المُهيئات، في حين لا يمكن أن يكون هناك سوى مُنشئ واحد. تتيح هذه المُهيئات طرقًا متنوعة لإنشاء مثيل من النوع [(new Personne())→initialiseuri(…)]؛
  • الأسطر 62-65: تسمح الدالة الثابتة [jsonUnserialize] بإنشاء كائن [Personne] من سلسلته jSON؛

سيكون البرنامج النصي [json-server.php] كما يلي:


<?php

// التبعيات
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;

// يتم تعيين رأس Content-Type ومكتبة الأحرف المستخدمة
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// يتم إنشاء كائن Personne
$personne = (new Personne())->setFromArray([
  "nom" => "de la Hûche",
  "prénom" => "jean-paul",
  "âge" => 27]);
// مصفوفة مرتبطة
$assoc = ["attr1" => "value1",
  "attr2" => [
    "prenom" => "Jean-Paul",
    "nom" => "de la Hûche"
  ]
];
// محتوى الرد هو jSON
$response->setContent(json_encode([$personne, $assoc]));

// إرسال الرد
$response->send();

تعليقات

  • السطران 4-5: يتم استيراد الفئة [Personne]؛
  • السطر 11: يُشار إلى أن المستند سيكون من النوع [application/json]. عند استلام هذا الرأس، ستعرض المتصفحات تنسيقًا لسلسلة jSON بدلاً من عرض النص العادي؛
  • السطر 12: ستحتوي السلسلة jSON على أحرف UTF-8؛
  • الأسطر 15-18: يتم إنشاء كائن [Personne]؛
  • الأسطر 20-25: يتم إنشاء مصفوفة مرتبطة ذات مستويين؛
  • السطر 27: يتم إرسال السلسلة jSON من المصفوفة إلى العميل؛
    • سيتم تسلسل العنصر [$personne] إلى jSON بفضل طريقته [jsonSerialize
    • سيتم تسلسل العنصر [$assoc] تلقائيًا إلى jSON؛

عند تشغيل هذا البرنامج النصي على الخادم (يجب تشغيل Laragon)، نحصل على الاستجابة التالية في المتصفح:

Image

Image

تعليقات

  • في [2]، يتم تنسيق الاستجابة jSON؛
  • في [4]، الرد jSON في صيغته الأولية. تجدر الإشارة إلى ترميز الأحرف المُشَدَّدة؛
  • في [6]، نوع المحتوى [application/json] المرسل من الخادم هو الذي دفع المتصفح إلى إجراء هذا التنسيق؛

17.7.2. العميل

Image

يتم تكوين العميل [json-client.php] بواسطة الملف jSON [config-json-client.json] التالي:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/03/json-server.php"
}

النص البرمجي [json-client.php] هو التالي:


<?php

// عميل خدمة jSON
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;

// تكوين العميل
const CONFIG_FILE_NAME = "config-json-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();

try {
  // إجراء الاستعلام
  $response = $httpClient->request('GET', $config['url']);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // يتم استرداد نص الرد jSON
  list($personne, $assoc) = json_decode($response->getContent(), true);
  // يتم إنشاء مثيل لشخص من جدول سماته
  $personne = (new Personne())->setFromArray($personne);
  // يتم عرض استجابة الخادم
  print "---Réponse du serveur\n";
  print "$personne\n";
  print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}


تعليقات

  • السطران 12-13: استيراد الفئة [Personne
  • السطر 30: إنشاء العميل HTTP؛
  • السطر 44: فك تشفير السلسلة jSON المرسلة من الخادم. نعلم أن ما تم تشفيره هو مصفوفة مكونة من عنصرين تتضمن مصفوفتين ترابطيتين؛
  • السطر 46: يتم إنشاء كائن [Personne] لعرضه بعد ذلك في السطر 49؛
  • السطر 50: يتم عرض الجدول الترابطي الثاني. لا تستطيع التعليمات [print] عرض الجداول. لذا يتم تحويلها إلى سلسلة jSON. للحصول على الأحرف المُشَدَّدة بشكل صحيح، يجب وضع المعلمة الثانية [JSON_UNESCAPED_UNICODE]. وقد رأينا أن الأحرف المُشَدَّدة مُشفَّرة بالفعل في السلسلة jSON؛

يؤدي تنفيذ البرنامج النصي للعميل إلى النتائج التالية:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 09:56:29 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 143
connection: close
content-type: application/json
---Réponse du serveur
Personne [jean-paul, de la Hûche, 27]
tableau={"attr1":"value1","attr2":{"prenom":"Jean-Paul","nom":"de la Hûche"}}

في السطرين 11 و12، تم استرداد الأحرف المُشَدَّدة بشكل صحيح.

17.8. استرداد متغيرات بيئة خدمة الويب

يتم تنفيذ البرنامج النصي للخادم في بيئة ويب يمكنه التعرف عليها. يتم تخزين هذه البيئة في القاموس $_SERVER، وهو متغير عام لـ PHP. إذا استخدمنا المكتبة [HttpFoundation]، فسيتم العثور على هذه البيئة في الحقل [Request→server] حيث [Request] هو الطلب HTTP الذي عالجته البرنامج النصي للويب.

17.8.1. برنامج الخادم النصي

نكتب تطبيق خادم يرسل بيئة التنفيذ الخاصة به إلى عملائه.

Image

نص البرنامج النصي للويب [env-server.php] هو كما يلي:


<?php

// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

// يتم استرداد الطلب
$request = Request::createFromGlobals();
// يتم إعداد الرد
$response = new Response();
// محتوى الرد هو json utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// يتم تحديد محتوى jSON للرد
$response->setContent(json_encode($request->server->all()));
// إرسال الرد
$response->send();
  • السطر 9: نسترد الكائن من النوع [Request] الذي يغلف جميع المعلومات المتاحة حول الطلب HTTP الذي تلقاه البرنامج النصي للويب وكذلك حول بيئة تشغيله؛
  • السطران 13-14: سنرسل نصًا عاديًا يحتوي على أحرف UTF-8 إلى العميل؛
  • السطر 16: ستكون المعلومات المرسلة إلى العميل سلسلة أحرف تم الحصول عليها عن طريق تسلسل jSON للكائن [$request→server→all()]: يمثل [$request→server] بيئة تشغيل البرنامج النصي على الويب. وهو كائن من النوع [ServerBag]، وهو نوع من القواميس. أما [$request→server→all()] فهو قاموس حقيقي، وهو قاموس محتوى [ServerBag]؛
  • السطر 18: يتم إرسال المعلومات؛

إذا تم تنفيذ هذا البرنامج النصي من NetBeans، فسيعرض المتصفح الصفحة التالية:

Image

  • في [2]، المفاتيح المختلفة لقاموس البيئة؛
  • في [3]، قيم هذه المفاتيح؛

17.8.2. البرنامج النصي للعميل

Image

يتم تكوين البرنامج النصي للعميل [env-client.php] بواسطة الملف jSON [config-env-client.json] التالي:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/04/env-server.php"
}

نص العميل [env-client.php] هو التالي:


<?php

// بيئة برنامج نصي على الخادم
//
// معالجة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-env-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();
try {
  // إرسال الطلب إلى الخادم
  $response = $httpClient->request('GET', $config['url']);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // عرض استجابة الخادم
  print "---Réponse du serveur\n";
  $env = json_decode($response->getContent());
  foreach ($env as $key => $value) {
    print "[$key]=>$value\n";
  }
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

تعليقات

  • السطر 42: يتم إزالة التسلسل من استجابة jSON من الخادم. يتم الحصول على مصفوفة ترابطية؛
  • الأسطر 43-45: يتم عرض جميع قيم هذا المصفوف الترابطي؛

ونحصل على النتيجة التالية في وحدة التحكم:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 17:35:50 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 1505
connection: close
content-type: application/json
---Réponse du serveur
[HTTP_HOST]=>localhost
[HTTP_USER_AGENT]=>Symfony HttpClient/Curl
[HTTP_ACCEPT_ENCODING]=>deflate, gzip
[PATH]=>C:\Program Files (x86)\Mail Enable\BIN;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files (x86)\Mail Enable\BIN64;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\myprograms\Microsoft VS Code\bin
[SystemRoot]=>C:\windows
[COMSPEC]=>C:\windows\system32\cmd.exe
[PATHEXT]=>.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
[WINDIR]=>C:\windows
[SERVER_SIGNATURE]=>
[SERVER_SOFTWARE]=>Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
[SERVER_NAME]=>localhost
[SERVER_ADDR]=>::1
[SERVER_PORT]=>80
[REMOTE_ADDR]=>::1
[DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[REQUEST_SCHEME]=>http
[CONTEXT_PREFIX]=>
[CONTEXT_DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[SERVER_ADMIN]=>admin@example.com
[SCRIPT_FILENAME]=>C:/myprograms/laragon-lite/www/php7/scripts-web/04/env-server.php
[REMOTE_PORT]=>63744
[GATEWAY_INTERFACE]=>CGI/1.1
[SERVER_PROTOCOL]=>HTTP/1.1
[REQUEST_METHOD]=>GET
[QUERY_STRING]=>
[REQUEST_URI]=>/php7/scripts-web/04/env-server.php
[SCRIPT_NAME]=>/php7/scripts-web/04/env-server.php
[PHP_SELF]=>/php7/scripts-web/04/env-server.php
[REQUEST_TIME_FLOAT]=>1559496950.644
[REQUEST_TIME]=>1559496950

فيما يلي معنى بعض المتغيرات (لنظام ويندوز. في نظام لينكس، ستكون مختلفة):

HTTP_HOST
القيمة xxx لرأس القيمة HTTP [Host: xxx] المرسلة من العميل
HTTP_USER_AGENT
القيمة xxx لرأس HTTP [User_Agent: xxx] المرسلة من العميل
HTTP_ACCEPT_ENCODING
القيمة xxx لرأس الرسالة HTTP [Accept-Encoding: xxx] المرسلة من العميل
PATH
مسار الملفات التنفيذية على الجهاز الذي يتم تشغيل البرنامج النصي عليه الخادم
COMSPEC
مسار مترجم الأوامر DOS
PATHEXT
امتدادات الملفات القابلة للتنفيذ
WINDIR
مجلد تثبيت Windows
SERVER_SIGNATURE
توقيع خادم الويب. لا يوجد شيء هنا.
SERVER_SOFTWARE
نوع خادم الويب
SERVER_NAME
اسم الإنترنت لجهاز خادم الويب
SERVER_PORT
منفذ الاستماع لخادم الويب
SERVER_ADDR
عنوان جهاز خادم الويب IP، وهو هنا 127:0:0:1
REMOTE_ADDR
عنوان IP للعميل. في هذه الحالة، كان العميل على نفس الجهاز الذي يوجد عليه الخادم.
REMOTE_PORT
منفذ اتصال العميل
DOCUMENT_ROOT
جذر شجرة المستندات التي يقدمها خادم الويب
REQUEST_SCHEME
بروتوكول TCP لطلب URL http://localhost/php7/
SERVER_ADMIN
البريد الإلكتروني لمسؤول خادم الويب
SCRIPT_FILENAME
المسار الكامل لبرنامج الخادم النصي
REMOTE_PORT
المنفذ الذي أرسل العميل طلبه منه
SERVER_PROTOCOL
إصدار بروتوكول HTTP الذي يستخدمه خادم الويب
REQUEST_METHOD
الأمر HTTP الذي استخدمه العميل. وهناك أربعة أوامر: GET، POST، PUT، DELETE
QUERY_STRING
المعلمات المرسلة مع الأمر GET /url?paramètres
REQUEST_URI
URL المطلوب من قبل العميل. إذا طلب المتصفح URL http://machine[:port]/uri، فسيكون لدينا REQUEST_URI=uri
SCRIPT_NAME
$_SERVER['SCRIPT_FILENAME']=$_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME']

17.9. استرداد الخادم للمعلمات المرسلة من قبل العميل

17.9.1. مقدمة

في بروتوكول HTTP، يتوفر للعميل طريقتان لتمرير المعلمات إلى الخادم WEB:

  • يطلب URL للخدمة بالصيغة

GET url?param1=val1&param2=val2&param3=val3… HTTP/1.0

حيث يجب أن تخضع قيم vali مسبقًا لعملية ترميز حتى يتم استبدال بعض الأحرف المحجوزة بقيمتها السداسية العشرية؛

  • يطلب رمز الخدمة URL بالصيغة

POST url HTTP/1.0

ثم يضع الرأس التالي ضمن رؤوس HTTP المرسلة إلى الخادم:

Content-length=N

تنتهي سلسلة الرؤوس المرسلة من قبل العميل بسطر فارغ. يمكنه بعد ذلك إرسال بياناته بالشكل

val1&param2=val2&param3=val3…

حيث يجب ترميز القيم vali مسبقًا، كما هو الحال في الطريقة GET. يجب أن يكون عدد الأحرف المرسلة إلى الخادم هو N، حيث N هي القيمة المعلنة في الرأس

Content-length=N

يسترد البرنامج النصي PHP الخاص بخدمة الويب المعلمات parami السابقة المرسلة من العميل، ويحصل على قيمها من المصفوفة:

  • $_GET["parami"] لطلب GET؛
  • $_POST["parami"] لأمر POST؛

هذا بالنسبة للوظائف الأساسية لـ PHP. إذا تم استخدام المكتبة [HttpFoundation]، فسيتم العثور على هذه المعلمات في:

  • [Request]->query->get(‘parami’) للأمر GET؛
  • [Request]->request->get(‘parami’) لأمر POST؛

حيث يمثل [Request] جميع المعلومات المتعلقة بالطلب الذي تلقاه البرنامج النصي للويب؛

17.9.2. العميل GET – الإصدار 1

Image

يتم تكوين البرامج النصية للعملاء بواسطة الملفين التاليين: jSON و [config-parameters-client.json]:

1
2
3
4
{
    "url-get": "http://localhost/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
  • السطر 1: URL الخاص بالنص البرمجي المستهدف للعملاء GET؛
  • السطر 2: URL الخاص بالبرنامج النصي الويب المستهدف للعميل POST؛

يرسل العملاء GET ثلاثة معلمات [nom, prenom, age] إلى الخادم. العميل [parameters-get-client.php] هو التالي:


<?php

// عميل GET لخادم ويب
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-parameters-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();

try {
  // إعداد المعلمات
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// تشفير المعلومات
  $parameters = "prenom=" . urlencode($prenom) .
    "&nom=" . urlencode($nom) .
    "&age=$age”;
  // إرسال الطلب
  $response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // عرض استجابة الخادم
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

تعليقات

  • الأسطر 33-35: ترميز المعلمات المرسلة إلى الخادم. يتم ترميز المعلمات [$prenom, $nom] التي قد تحتوي على أحرف UTF-8 باستخدام الدالة [urlencode]. يتم استبدال جميع الأحرف غير الأبجدية الرقمية (بالمعنى المقصود في التعبيرات العلائقية) بـ %xx حيث xx هي القيمة السداسية العشرية للحرف. أما المسافات فيتم استبدالها بعلامة +؛
  • السطر 37: هل URL المطلوب هو $URL?$parameters حيث يكون $parameters على شكل nom=val1&prenom=val2&age=val3؛
  • السطر 48: سيكتفي العميل بعرض استجابة الخادم؛

قد نشعر بالفضول لمعرفة ما يتلقاه الخادم عند إرسال طلب GET مع التهيئة المطلوبة. وللقيام بذلك، نقوم بتشغيل خادمنا العام [RawTcpServer] على المنفذ 100 للجهاز المحلي من خلال محطة Laragon (انظر الفقرة «الرابط»):

Image

تأكد من أنك في المجلد الخاص بالأدوات المساعدة في [4].

نقوم بتعديل الملف jSON [parameters-get-client.json] الذي يقوم بتكوين العملاء GET و POST:


{
    "url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
  • السطر 2: قمنا بتغيير منفذ خادم الويب. وبالتالي سيتم الاتصال بـ [RawTcpServer]؛

نقوم بتشغيل العميل. في نافذة [RawTcpServerنحصل على المعلومات التالية:

Image

  • في [1]، الأمر GET المُعدّ الذي أرسله العميل. نرى بوضوح ترميز بعض الأحرف؛

17.9.3. الخادم GET / POST

Image

نص البرنامج النصي للخادم [parameters-server.php] هو كما يلي:


<?php

// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

// استرداد الطلب
$request = Request::createFromGlobals();
// يتم استرداد معلمات الطلب
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();

// يتم إعداد الرد
$response = new Response();
// محتوى الرد هو نص utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// محتوى الرد - جدول مشفر بتنسيق jSON
$response->setContent(json_encode([
  "method" => $request->getMethod(),
  "uri" => $request->getRequestUri(),
  "getParameters" => $getParameters,
  "bodyParameters" => $bodyParameters
    ], JSON_UNESCAPED_UNICODE));
// إرسال الرد
$response->send();

تعليقات

  • السطر 9: إنشاء الكائن [Request] الخاص بالنص البرمجي على الويب. يضم هذا الكائن جميع المعلومات التي تلقّاها النص البرمجي على الويب من العميل؛
  • السطر 11: الكائن [Request→query] هو من النوع [ParameterBag] ويجمع معلمات العملية المحتملة GET الخاصة بأحد العملاء. يتيح التعبير [Request→query→getX»)] الحصول على المعلمة المسماة X ضمن معلمات GET و[nom=val1&prenom=val2&age=val3]. يسمح التعبير [Request→query→all()] بالحصول على قاموس معلمات GET؛
  • السطر 12: الكائن [Request→request] هو من النوع [ParameterBag] ويجمع المعلمات المرسلة كوثيقة من العميل إلى الخادم. ويُقال أيضًا إن هذه المعلمات يتم تحميلها لأنها تنتمي إلى وثيقة يرسلها العميل إلى الخادم. يسمح التعبير [Request→request→getX»)] بالحصول على المعلمة المسماة X ضمن المعلمات التي تم تحميلها [nom=val1&prenom=val2&age=val3]. يسمح التعبير [Request→request→all()] بالحصول على قاموس المعلمات التي تم تحميلها؛
  • السطران 17-18: يُبلغ العميل بأنه سيتم إرسال jSON المشفر في UTF-8 إليه؛
  • الأسطر 20-25: يقوم الخادم بإعادة إرسال جميع المعلمات التي تلقّاها إلى العميل، بالإضافة إلى نوع العملية [GET / POST / …] التي أجراها العميل، وURI المطلوب. يتم الحصول على هذه الطريقة من خلال التعبير [$request→getMethod()]. الوثيقة المرسلة إلى العميل هي السلسلة jSON من جدول ترابطي تحتوي بعض قيمه على جداول ترابطية أخرى. يطلب المعامل [JSON_UNESCAPED_UNICODE] إرسال أحرف Unicode (مثل الأحرف المُشَدَّدة على سبيل المثال) كما هي دون ترميز؛
  • السطر 27: يتم إرسال الرد إلى العميل؛

يؤدي تنفيذ البرنامج النصي للعميل إلى النتائج التالية:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 10:08:45 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 207
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom=jean-paul&nom=de+la+h%C3%BBche&age=45","getParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"},"bodyParameters":[]}]
  • السطر 10:
    • [method]: الطريقة هي GET؛
    • [uri]: تظهر المعلمات المُشفَّرة بـ URL الخاصة بالطلب GET في الطلب URI؛
    • [getParameters]: مصفوفة معلمات الطلب GET؛
    • [bodyParameters]: جدول المعلمات التي تم تحميلها: وهو فارغ؛

17.9.4. العميل GET – الإصدار 2

في الإصدار السابق من البرنامج النصي للعميل، قمنا بأنفسنا بترميز المعلمات المرسلة إلى الخادم باستخدام ترميز URL، لأغراض تعليمية. الكائن [HttpClient] قادر على القيام بهذه المهمة بنفسه. هذا هو البرنامج النصي [parameters-get-client-2.php] التالي:


<?php

// عميل GET لخادم ويب
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-parameters-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();
try {
  // إعداد المعلمات
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // إرسال الطلب إلى الخادم
  $response = $httpClient->request('GET', $config['url-get'],
    ["query" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // عرض استجابة الخادم
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

تعليقات

  • الأسطر 33-37: إضافة معلمات إلى طلب GET الوارد في السطر 32. سيتولى الكائن [HttpClient] بنفسه ترميز URL؛

17.9.5. العميل POST

يرسل العميل HTTP إلى خادم الويب تسلسل النص التالي: رؤوس HTTP، سطر فارغ، المستند. في العميل السابق، كان هذا التسلسل كما يلي:

1
2
3
GET /url?paramètres HTTP/1.1
… autres entêtes HTTP
ligne vide

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

1
2
3
4
POST /url HTTP/1.1
… autres entêtes HTTP
ligne vide
paramètres

هذه المرة، المعلمات التي كانت موجودة في رؤوس HTTP بالنسبة للعميل GET، هي جزء، في العميل POST، من المستند المرسل بعد الرؤوس.

النص البرمجي للعميل POST [parameters-postclient.php] هو كما يلي:


<?php

// عميل POST لخادم ويب
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-parameters-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();
try {
  // إعداد المعلمات
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // إرسال الطلب إلى الخادم
  $response = $httpClient->request('POST', $config['url-post'],
    ["body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // يتم استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // يتم عرض استجابة الخادم
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
  • السطر 32: لدينا الآن طلب HTTP من النوع POST؛
  • الأسطر 33-37: يُطلق على معلمات POST اسم نص (body) الطلب POST: وهو المستند الذي يرسله العميل إلى الخادم. هنا، يتم إرسال ثلاث معلمات [nom, prenom, age
  • السطر 48: يتم عرض استجابة الخادم jSON؛

نتائج تنفيذ البرنامج النصي للعميل هي كما يلي:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 11:43:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 163
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php","getParameters":[],"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
  • السطر 10: الطريقة هي [Post] والمعلمات من النوع [bodyParameters]. لا توجد معلمات [getParameters] كما يظهر في [uri]؛

قد يثير فضولنا معرفة ما يتلقاه الخادم عند إرسال طلب POST. وللقيام بذلك، نقوم بتشغيل خادمنا العام [RawTcpServer] على المنفذ 100 للجهاز المحلي من خلال محطة Laragon (انظر الفقرة «الرابط»):

Image

تأكد من أنك في مجلد الأدوات المساعدة في [4].

نقوم بتعديل الملف jSON [config-parameters-client.json] الذي يقوم بتكوين العميل POST:


{
    "url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost:100/php7/scripts-web/05/parameters-server.php"
}
  • السطر 3: قمنا بتغيير منفذ خادم الويب. وبالتالي، سيتم الاتصال بـ [RawTcpServer]؛

نقوم بتشغيل العميل. في نافذة [RawTcpServer نحصل على المعلومات التالية:

Image

  • في [6]، الأمر POST؛
  • في [7]: يحدد الرأس HTTP [Content-Length] عدد البايتات في المستند الذي سيرسله العميل إلى الخادم. يحدد الرأس HTTP [Content-Type] طبيعة هذا المستند. ويشير النوع [application/x-www-form-urlencoded] إلى نص مشفر باستخدام URL؛
  • أما في [8]، فالسطر الفارغ الذي يُعلن نهاية الرؤوس HTTP وبداية المستند المكون من 44 بايت. ما لا تظهره لقطة الشاشة هو المستند نفسه. وهو سلسلة المعلمات المشفرة بـ URL: [prenom=jean-paul&nom=de+la+h%C3%BBche&age=45]. يمكن للقارئ التحقق من أنها تحتوي بالفعل على 44 حرفًا؛

17.9.6. عميل POST مختلط

في POST، يمكن مزج المعلمات المشفرة في URL وتلك المشفرة في المستند المرسل من قبل العميل بعد رؤوس HTTP. فيما يلي مثال على [parameters-mixte-postclient.php]:


<?php

// عميل POST لخادم ويب
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-parameters-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();
try {
  // إعداد المعلمات
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // إرسال الطلب إلى الخادم
  $response = $httpClient->request('POST', $config['url-post'],
    [
      // معلمات المستند (body)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // معلمات URL (الاستعلام)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // يتم استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // عرض استجابة الخادم
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

تعليقات

  • السطر 32: طلب POST؛
  • الأسطر 40-45: المعلمات المُشفَّرة بتنسيق URL في URL؛
  • الأسطر 35-39: المعلمات المشفرة في عنوان URL في نص (body، document) الطلب؛

عند التنفيذ، نحصل على النتائج التالية في وحدة التحكم:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:34:23 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 270
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
  • السطر 10: نرى أن الخادم تمكن من استرداد كلا النوعين من المعلمات؛

17.9.7. عميل مختلط GET

نحاول القيام بنفس الشيء كما في السابق باستخدام طلب GET. النص البرمجي [parameters-mixte-get-client.php] هو كما يلي:


<?php

// عميل POST لخادم ويب
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-parameters-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();
try {
  // إعداد المعلمات
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // إرسال الطلب إلى الخادم
  $response = $httpClient->request('GET', $config['url-post'],
    [
      // معلمات المستند (body)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // معلمات الاستعلام (query) URL
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // يتم استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // عرض استجابة الخادم
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

تعليقات

  • السطر 32: طلب POST؛
  • الأسطر 40-45: المعلمات المشفرة في عنوان URL في URL؛
  • الأسطر 35-39: المعلمات المشفرة بـ URL في نص (body، document) الطلب؛

عند التنفيذ، نحصل على النتائج التالية في وحدة التحكم:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:41:19 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 217
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":[]}]
  • السطر 10: نلاحظ أن الخادم لم يتلق أي معلمات مشفرة بـ URL في المستند المرسل من قبل العميل. عند النظر إلى رؤوس HTTP المرسلة من قبله، نلاحظ أنه أرسل بالفعل مستندًا مكونًا من 44 حرفًا، لكن الخادم لم يستفد منه؛

أخيرًا، ما هي الطريقة التي يجب اختيارها لإرسال المعلومات إلى الخادم؟

  • تستخدم طريقة [GET URL?param1=val1&param2=val2&…] رابطًا مكونًا من URL يمكن استخدامه كرابط. هذه هي ميزتها الرئيسية: يمكن للمستخدم حفظ مثل هذه الروابط في علاماته المرجعية؛
  • في تطبيقات أخرى، قد لا نرغب في عرض المعلمات المرسلة إلى الخادم في رابط URL. لأسباب أمنية على سبيل المثال. لذا سنستخدم طريقة [POST] وسنضع المعلمات المشفرة بترميز URL في مستند يتم إرساله إلى الخادم؛

17.10. إدارة جلسات الويب

في أمثلة العميل/الخادم السابقة، كان العمل يسير على النحو التالي:

  • يقوم العميل بفتح اتصال بالمنفذ 80 لجهاز خدمة الويب؛
  • يرسل تسلسل النص: رؤوس HTTP، سطر فارغ، [document
  • رداً على ذلك، يرسل الخادم تسلسلاً من نفس النوع؛
  • يقوم الخادم بإنهاء الاتصال مع العميل؛
  • يقوم العميل بإنهاء الاتصال بالخادم؛

إذا أرسل نفس العميل طلبًا جديدًا إلى خادم الويب بعد ذلك بوقت قصير، يتم إنشاء اتصال جديد بين العميل والخادم. ولا يمكن للخادم معرفة ما إذا كان العميل الذي يتصل قد زاره من قبل أم أن هذا هو الطلب الأول. بين اتصالين، «ينسى» الخادم عميله. ولهذا السبب، يُقال إن بروتوكول HTTP هو بروتوكول عديم الحالة. ومع ذلك، من المفيد أن يتذكر الخادم عملاءه. فعلى سبيل المثال، إذا كان التطبيق آمنًا، سيرسل العميل إلى الخادم اسم المستخدم وكلمة المرور لتوثيق هويته. وإذا «نسي» الخادم عميله بين اتصالين، فسيضطر العميل إلى توثيق هويته عند كل اتصال جديد، وهو أمر غير مقبول.

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

من الناحية الفنية، يحدث ذلك على النحو التالي:

  • في الرد الموجه إلى عميل جديد، يضمّن الخادم الرأس HTTP Set-Cookie: MotClé=المعرّف. ولا يقوم بذلك إلا عند الطلب الأول؛
  • في الطلبات التالية، سيقوم العميل بإعادة إرسال معرّفه عبر رأس HTTP Cookie: MotClé=المعرّف حتى يتعرف عليه الخادم؛

قد يتساءل المرء كيف يعرف الخادم أنه يتعامل مع عميل جديد بدلاً من عميل سبق له الزيارة. ويتم تحديد ذلك من خلال وجود الرأس HTTP Cookie ضمن رؤوس HTTP الخاصة بالعميل. أما بالنسبة للعميل الجديد، فإن هذا الرأس يكون غائبًا.

يُطلق على مجموعة الاتصالات الخاصة بعميل معين اسم «جلسة».

17.10.1. ملف التكوين [php.ini]

لكي تعمل إدارة الجلسات بشكل صحيح مع PHP، يجب التحقق من أنه تم تكوينه بشكل صحيح. في نظام Windows، ملف التكوين الخاص به هو php.ini. وفقًا لسياق التنفيذ (وحدة التحكم، الويب)، يجب البحث عن ملف التكوين [php.ini] في مجلدات مختلفة. لاكتشاف هذه المجلدات، سنستخدم البرنامج النصي التالي:

1
2
3
4
<?php

// معلومات PHP
phpinfo();

السطر 4: توفر الدالة phpinfo معلومات عن مترجم PHP الذي يقوم بتنفيذ البرنامج النصي. وتقدم على وجه الخصوص مسار ملف التكوين [php.ini] المستخدم.

لقد استخدمنا هذا البرنامج النصي سابقًا في بيئة وحدة التحكم (انظر الفقرة «الرابط»). وفي بيئة الويب، نحصل على النتيجة التالية:

Image

  • في [1-2]، الملف [php.ini] الذي يقوم بتكوين مترجم البرامج النصية على الويب. يوجد في هذا الملف قسم خاص بالجلسة:
[Session]
session.save_handler = files
session.save_path = "C:/myprograms/laragon-lite/tmp"
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 36000
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
  • السطر 2: يتم حفظ بيانات جلسة عمل العميل في ملف؛
  • السطر 3: المجلد المخصص لحفظ بيانات الجلسة. إذا لم يكن هذا المجلد موجودًا، فلن يتم الإبلاغ عن أي خطأ ولن تعمل إدارة الجلسات؛
  • الأسطر 4-6: تشير إلى أن معرّف الجلسة يُدار بواسطة الرؤوس HTTP وSet-Cookie وCookie؛
  • السطر 7: سيكون رأس Set-Cookie بالصيغة Set-Cookie: PHPSESSID=identifiant_de_session؛
  • السطر 8: لا يتم بدء جلسة عمل العميل تلقائيًا. يجب أن يطلبها البرنامج النصي للخادم صراحةً باستخدام الأمر session_start()؛
  • السطر 9: تظل ملف تعريف الارتباط الخاص بالجلسة ساريًا طالما لم يتم إغلاق متصفح العميل؛
  • السطر 10: المسار الذي يجب إعادة ملف تعريف الارتباط الخاص بالجلسة إليه. إذا كان [session.cookie_path = /xxx]، فعند كل مرة يطلب فيها المتصفح URL من النوع [/xxx/yyy/zzz]، يجب عليه إعادة ملف تعريف الارتباط. هنا يشير المسار [/] إلى أنه يجب إعادة إرسال ملف تعريف الارتباط لأي URL من الموقع؛
  • السطر 13: يجب تسلسل بعض كائنات الجلسة حتى يمكن تخزينها في ملف. ويقوم PHP بضمان عملية التسلسل/إلغاء التسلسل هذه باستخدام الدوال [serialize / unserialize]؛
  • السطر 16: مدة الصلاحية التي بعد انقضائها تُعتبر كائنات الجلسة المخزنة في ملف النسخ الاحتياطي قديمة؛
  • السطر 19: مدة صلاحية الجلسة. بعد هذه المدة، يتم إنشاء جلسة جديدة وتُفقد الكائنات التي تم حفظها في الجلسة السابقة؛

17.10.2. مثال 1

17.10.2.1. الخادم

Image

تتم إدارة معرف الجلسة بشكل شفاف بالنسبة لخدمة الويب. يتم إدارة هذا المعرف بواسطة خادم الويب. يمكن لخدمة الويب الوصول إلى جلسة العميل عبر الأمر session_start(). منذ تلك اللحظة، يمكن لخدمة الويب قراءة/كتابة البيانات في جلسة عمل العميل عبر القاموس $_SESSION. في حالة استخدام المكتبة [HttpFoundation]، تكون جلسة العمل متاحة عبر التعبير [Request→getSession].

يوضح الكود التالي [session-server.php] إدارة ثلاثة عدادات في الجلسة. مع كل طلب جديد، يقوم البرنامج النصي للويب بزيادة قيم هذه العدادات وتخزينها في الجلسة حتى يمكن استرجاعها عند الطلب التالي.


<?php
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

//
// يتم استرداد الطلب
$request = Request::createFromGlobals();
// الجلسة
$session = new Session();
$session->start();
// استرداد ثلاثة عدادات في الجلسة
if ($session->has("N1")) {
  // زيادة عداد N1
  $session->set("N1", (int) $session->get("N1") + 1);
} else {
  // العداد N1 غير موجود في الجلسة - يتم إنشاؤه
  $session->set("N1", 0);
}
if ($session->has("N2")) {
  // زيادة العداد N2
  $session->set("N2", (int) $session->get("N2") + 1);
} else {
  // العداد N2 غير متصل بالجلسة - يتم إنشاؤه
  $session->set("N2", 10);
}
if ($session->has("N3")) {
  // زيادة العداد N3
  $session->set("N3", (int) $session->get("N3") + 1);
} else {
  // العداد N3 غير مسجل في الجلسة - يتم إنشاؤه
  $session->set("N3", 100);
}
// يتم إعداد الرد
$response = new Response();
// محتوى الرد هو نص بتنسيق utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// ستكون الإجابة عبارة عن jSON لجدول يحتوي على العدادات الثلاثة
$response->setContent(json_encode([
  "N1" => $session->get("N1"),
  "N2" => $session->get("N2"),
  "N3" => $session->get("N3")]));

// إرسال الإجابة
$response->send();
  • السطر 10: الكائن [$request] يغلف جميع المعلومات المتعلقة بالطلب الذي تلقّاه البرنامج النصي للويب؛
  • السطران 12-13: يتم إنشاء جلسة عمل وتفعيلها. الكائن [Session] يحتوي على بيانات الجلسة التي تتوافق مع ملف تعريف الارتباط الخاص بالجلسة الذي أرسله العميل. إذا لم يرسل العميل ملف تعريف ارتباط من هذا النوع، فلن تكون هناك أي بيانات مخزنة في [Session]. سيضم البرنامج النصي للويب في استجابته الأولى الرأس HTTP [Set-Cookie : PHPSESSID=xxx]. في طلباته اللاحقة، سيرسل العميل الرأس HTTP [Cookie : PHPSESSID=xxx] للإشارة إلى الجلسة التي يريد استخدام محتواها. الجلسة هي ذاكرة العميل؛
  • السطر 15: نتحقق مما إذا كانت الجلسة تحتوي على مفتاح باسم [N1]. سيكون هذا هو اسم عدادنا الأول. إذا لم يكن الأمر كذلك (السطر 20)، فإننا نعطيه القيمة 0 ونضعه في الجلسة. إذا كان الأمر كذلك (السطر 23)، فإننا:
    • نسترده من الجلسة؛
    • نزيد قيمته بمقدار 1؛
    • نعيده إلى الجلسة؛
  • الأسطر 22-35: نفعل الشيء نفسه مع العدادين الآخرين N2 و N3؛
  • الأسطر 36-40: نُعد إجابة من النوع [application/json]؛
  • الأسطر 42-45: سيكون الرد هو السلسلة jSON من جدول يحتوي على العدادات الثلاثة؛
  • السطر 48: يتم إرسال الرد إلى العميل؛

في العلاقة بين العميل والخادم، تعتمد إدارة جلسة عمل العميل على الخادم على طرفين، هما العميل والخادم:

  • يتولى الخادم مسؤولية إرسال معرّف إلى العميل عند الطلب الأول
  • يتعين على العميل إعادة إرسال هذا المعرّف مع كل طلب جديد. وإذا لم يقم بذلك، فسيعتبر الخادم أنه عميل جديد وسيقوم بإنشاء معرّف جديد لجلسة جديدة.

النتائج

نستخدم متصفح الويب كعميل. بشكل افتراضي (بناءً على الإعدادات في الواقع)، يقوم المتصفح بإعادة إرسال معرّفات الجلسة التي يرسلها الخادم إليه. مع توالي الطلبات، سيتلقى المتصفح العدادات الثلاثة المرسلة من الخادم وسيلاحظ زيادة قيمها.

Image

  • في [2]، الطلب الأول إلى خدمة الويب؛
  • في [4]، يُظهر الطلب الرابع أن العدادات قد زادت بالفعل. يتم بالفعل حفظ قيم العدادات مع توالي الطلبات؛

لنستخدم وضع التطوير لمشاهدة رؤوس القسمات HTTP المتبادلة بين الخادم والعميل. نغلق Firefox لإنهاء الجلسة الحالية مع الخادم، ثم نعيد فتحه ونقوم بتنشيط وضع التطوير (F12). سيؤدي هذا إلى حذف الجلسة الحالية للمتصفح، الذي سيبدأ جلسة جديدة. نطلب الخدمة [session-server.php]:

Image

في [5]، نرى معرّف الجلسة الذي أرسله الخادم في رده على الطلب الأول من العميل. ويستخدم رأس HTTP Set-Cookie.

لنقم بإرسال طلب جديد عن طريق تحديث (F5) الصفحة في متصفح الويب:

Image

فيما سبق، نلاحظ أمرين:

  • في [11]، يعيد متصفح الويب معرف الجلسة مع الرأس HTTP Cookie.
  • أما في [12]، فلا تدرج خدمة الويب هذا المعرّف في ردها. وأصبح الآن على العميل مسؤولية إرساله مع كل طلب من طلباته.

17.10.2.2. العميل

نقوم الآن بكتابة برنامج نصي للعميل مستندًا إلى البرنامج النصي للخادم السابق. في إدارة الجلسة، يجب أن يتصرف مثل متصفح الويب:

  • في رد الخادم على طلبه الأول، يجب أن يعثر على معرّف الجلسة الذي يرسله إليه الخادم. وهو يعلم أنه سيجده في الرأس HTTP Set-Cookie.
  • ويجب عليه، في كل طلب لاحق، أن يعيد إرسال معرّف الجلسة الذي تلقّاه إلى الخادم. وسيقوم بذلك باستخدام الرأس HTTP Cookie.

Image

يتم تكوين العميل [session-client] بواسطة الملف jSON [config-session-client.json] التالي:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/06/session-server.php"
}

رمز العميل [session-client] هو التالي:


<?php

// إدارة الجلسة
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-session-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// إنشاء عميل HTTP
$httpClient = HttpClient::create();
try {
  // سيتم إجراء 10 طلبات
  for ($i = 0; $i < 10; $i++) {
    // إرسال الطلب إلى الخادم
    if (!isset($sessionCookie)) {
      // بدون جلسة عمل
      $response = $httpClient->request('GET', $config['url']);
    } else {
      // مع جلسة
      $response = $httpClient->request('GET', $config['url'],
        ["headers" => ["Cookie" => $sessionCookie]]);
    }
    // حالة الرد
    $statusCode = $response->getStatusCode();
    print "---Réponse avec statut : $statusCode\n";
    // نسترد الرؤوس
    print "---Entêtes de la réponse\n";
    $headers = $response->getHeaders();
    foreach ($headers as $type => $value) {
      print "$type: " . $value[0] . "\n";
    }
    // يتم استرداد ملف تعريف الارتباط الخاص بالجلسة إن وجد
    if (isset($headers["set-cookie"])) {
      // ملف تعريف ارتباط الجلسة؟
      foreach ($headers["set-cookie"] as $cookie) {
        $match = [];
        $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
        if ($match) {
          $sessionCookie = "PHPSESSID=" . $champs[1];
        }
      }
    }
  }
  // عرض الرد jSON من الخادم
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}


تعليقات

  • السطر 27: إنشاء العميل HTTP؛
  • السطر 30: سنقوم بإرسال نفس الطلب 10 مرات إلى الخادم [session-server.php]؛
  • السطر 32: ستكون قيمة المتغير [$sessionCookie] هي قيمة الرأس HTTP [Set-Cookie] التي استلمها العميل؛
  • الأسطر 32-34: إذا لم يكن هذا المتغير موجودًا، فهذا يعني أن الجلسة لم تبدأ بعد. يتم إرسال الأمر [GET] بدون الرأس [Cookie]؛
  • الأسطر 35-38: وإلا فإن الجلسة قد بدأت، ويتم إرسال الأمر [GET] مع الرأس [Cookie]. وستكون قيمة هذا الرأس هي [$sessionCookie]؛
  • السطر 50: إذا كان الرأس [Set-Cookie] ضمن الرؤوس HTTP المستلمة، فسيتم البحث عن ملف تعريف ارتباط الجلسة؛
  • السطر 52: يمكن لخادم الويب إرسال عدة رؤوس [Set-Cookie]. ملف تعريف الارتباط الخاص بالجلسة هو واحد منها فقط. في مثالنا، يتميز هذا الملف بأنه على الشكل [PHPSESSID=xxx;]؛
  • الأسطر 53-57: نستخدم تعبيرًا منتظمًا للعثور على ملف تعريف ارتباط الجلسة؛
  • السطر 62: بمجرد إرسال الطلبات العشرة، يتم عرض آخر استجابة من الخادم وهي jSON؛

النتائج

يؤدي تنفيذ البرنامج النصي للعميل إلى عرض ما يلي في وحدة التحكم في NetBeans:


"C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\poly\scripts-console\clients web\06\session-client.php"
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=1cerjgsgdlc35e1mkenvtltmh8; path=/
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
…………………………………………………………
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse du serveur : {"N1":9,"N2":19,"N3":109}
  • السطر 8: في الرد الأول، يرسل الخادم معرّف الجلسة. وفي الردود التالية، لا يرسله بعد ذلك؛
  • السطر 41: تم بالفعل زيادة قيم العدادات الثلاثة [N1, N2, N3] بمقدار 9 مرات. وعند الاستعلام رقم 1، تمت إعادة تعيين قيمها إلى الصفر؛

يوضح المثال التالي أنه يمكن أيضًا حفظ قيم مصفوفة أو كائن في الجلسة.

17.10.3. المثال 2

17.10.3.1. الخادم

Image

سنقوم بتخزين كائن [Personne] في الجلسة. تعريف هذه الفئة هو كما يلي:


<?php

namespace Modèles;

class Personne implements \JsonSerializable {
  // السمات
  private $nom;
  private $prénom;
  private $âge;

  // تحويل مصفوفة مرتبطة إلى كائن [Personne]
  public function setFromArray(array $assoc): Personne {
    // يتم تهيئة الكائن الحالي باستخدام المصفوفة الترابطية
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // النتيجة
    return $this;
  }

  // دالات الاسترجاع والتعيين
  public function getNom() {
    return $this->nom;
  }

  public function getPrénom() {
    return $this->prénom;
  }

  public function setNom($nom) {
    $this->nom = $nom;
    return $this;
  }

  public function setPrénom($prénom) {
    $this->prénom = $prénom;
    return $this;
  }

  public function getÂge() {
    return $this->âge;
  }

  public function setÂge($âge) {
    $this->âge = $âge;
    return $this;
  }

  // toString
  public function __toString(): string {
    return "Personne [$this->prénom, $this->nom, $this->âge]";
  }

  // تنفيذ واجهة JsonSerializable
  public function jsonSerialize(): array {
    // تُرجع مصفوفة مرتبطة تستخدم سمات الكائن كمفاتيح
    // يمكن بعد ذلك ترميز هذا المصفوفة إلى jSON
    return get_object_vars($this);
  }

  // تحويل jSON إلى كائن [Personne]
  public static function jsonUnserialize(string $json): Personne {
    // يتم إنشاء شخص من السلسلة jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }

}

سيكون البرنامج النصي للخادم كما يلي:


<?php

// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;

//
// استرداد الطلب الحالي
$request = Request::createFromGlobals();

// الجلسة
$session = new Session();
$session->start();

// استرداد بيانات متنوعة من الجلسة
// مصفوفة
if ($session->has("tableau")) {
  // الجدول موجود في الجلسة - يتم زيادة جميع قيمه
  $tableau = $session->get("tableau");
  for ($i = 0; $i < count($tableau); $i++) {
    $tableau[$i] += 1;
  }
  // إعادة المصفوفة إلى الجلسة
  $session->set("tableau", $tableau);
} else {
  // المصفوفة غير موجودة في الجلسة - يتم إنشاؤها
  $tableau = [0, 10, 100];
  // يتم وضعه في الجلسة
  $session->set("tableau", $tableau);
}
// قاموس
if ($session->has("assoc")) {
  // [assoc] موجود في الجلسة - يتم زيادة جميع عناصره
  $assoc = $session->get("assoc");
  foreach ($assoc as $key => $value) {
    $assoc[$key] = $value + 1;
  }
  // نضع $assoc في الجلسة
  $session->set("assoc", $assoc);
} else {
  // [assoc] غير موجود في الجلسة - يتم إنشاؤه
  $assoc = ["un" => 0, "deux" => 10, "trois" => 100];
  // يتم وضع $assoc في الجلسة
  $session->set("assoc", $assoc);
}
// كائن «شخص»
if ($session->has("personne")) {
  // [personne] موجود في الجلسة - يتم زيادة عمره
  $personne = $session->get("personne");
  $personne->setÂge($personne->getÂge() + 1);
} else {
  // [personne] غير موجود في الجلسة - يتم إنشاؤه
  $personne = (new Personne())->setFromArray(
    ["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
  // يتم إدراج $personne في الجلسة
  $session->set("personne", $personne);
}
// يتم إعداد الرد
$response = new Response();
// محتوى الرد هو jSON utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
$response->setContent(json_encode([
  "tableau" => $tableau,
  "assoc" => $assoc,
  "personne" => $personne], JSON_UNESCAPED_UNICODE));

// إرسال الرد
$response->send();

تعليقات

  • السطران 16-17: نسترد الجلسة الحالية ونقوم بتنشيطها؛
  • السطور 21-34: إدارة مصفوفة [tableau] موضوعة في الجلسة. مع كل طلب جديد، يتم زيادة عناصرها بمقدار 1؛
  • الأسطر 36-49: إدارة مصفوفة ترابطية [assoc] موضوعة في الجلسة. مع كل طلب جديد، يتم زيادة عناصرها بمقدار 1؛
  • الأسطر 51-61: يتم إدارة كائن [Personne] في الجلسة. مع كل طلب جديد، يتم زيادة عمر هذا الشخص بمقدار 1؛
  • الأسطر 62-73: يتم إرسال استجابة jSON إلى العميل: السلسلة jSON من مصفوفة مرتبطة؛

لنقم بتنفيذ هذا البرنامج النصي من NetBeans. تعطي الطلبات الأولى والثانية النتائج التالية (F5 في المتصفح للطلب الثاني):

Image

  • نلاحظ أنه في [6-8]، تمت زيادة جميع العدادات؛

17.10.3.2. العميل

Image

العميل هو نفسه الموجود في المثال 1 (فقرة الرابط). لا يتم تعديل سوى ملف التكوين الخاص به [config-session-client]:


{
    "url": "http://localhost/php7/scripts-web/07/session-server.php"
}

يؤدي التنفيذ إلى النتائج التالية:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=qbfrj8clr20mod3eriur71mao6; path=/
content-length: 119
connection: close
content-type: application/json
---Réponse avec statut : 200
………….……………………………………………………….
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 119
connection: close
content-type: application/json
---Réponse du serveur : {"tableau":[9,19,109],"assoc":{"un":9,"deux":19,"trois":109},"personne":{"nom":"Hûche","prénom":"Léonard","âge":9}}
  • في السطر [22]، نلاحظ أن جميع العدادات قد تمت زيادتها؛

17.11. المصادقة

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

17.11.1. العميل

Image

رمز العميل [auth-client.php] هو كما يلي:


<?php

// إدارة جلسة
//
// إدارة الأخطاء
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// تكوين العميل
const CONFIG_FILE_NAME = "config-auth-client.json";

// استرداد التكوين
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    // "verify_peer" => false,
    // "verify_host" => false
  ]);


try {
  // يتم إرسال الطلب إلى الخادم
  $response = $httpClient->request('GET', $config['url']);
  // حالة الرد
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // يتم استرداد الرؤوس
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // يتم عرض الرد jSON الوارد من الخادم
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // يتم عرض الخطأ
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

تعليقات

  • الأسطر 27-31: تم تمرير معلمة إلى الدالة الثابتة [HttpClient::create]، وهي مصفوفة مرتبطة؛
  • السطر 28: قيمة المفتاح [auth_basic] هي مصفوفة مكونة من عنصرين [user, password]. سيستخدم العميل هذين العنصرين لتوثيق هويته لدى خدمة الويب. تشير المفتاح [auth_basic] إلى نوع من أنواع المصادقة يُسمى [Autorization Basicوهو اسم الرأس HTTP الذي سيصدره العميل. وهناك أنواع أخرى من المصادقة؛
  • بخلاف هذا الرمز، فإن العميل مطابق للعملاء السابقين؛

للاطلاع على الرؤوس HTTP المرسلة من العميل، سنقوم بتوصيله بالخادم العام TCP [RawTcpServer] كما فعلنا ذلك مرارًا وتكرارًا:

Image

نقوم بتشغيل العميل باستخدام التكوين [config-auth-client.json] التالي:


{
    "url": "http://localhost:100/php7/scripts-web/08/auth-server.php"
}

ثم يتلقى الخادم [RawTcpServer] الأسطر التالية:

Image

  • في [5]، نرى الرأس [Autorization : Basic XXX] الذي أرسله العميل. السلسلة XXX هي السلسلة [user:password] المشفرة باستخدام Base64؛

للتأكد من ذلك، يمكنك فك تشفير السلسلة المستلمة على الموقع [https://www.base64decode.org/]:

Image

17.11.2. الخادم

Image

الخادم [auth-server.php] هو التالي:


<?php

// التبعيات
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

// المستخدمون المصرح لهم
$users = ["admin" => "admin"];
//
// يتم استرداد الطلب الحالي
$request = Request::createFromGlobals();
// المصادقة
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// هل المستخدم موجود؟
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// تحضير الرد
$response = new Response();
// تحديد رمز حالة الرد
if (!$trouvé) {
  // لم يتم العثور عليه - الرمز 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 par l'exemple\"")]);
} else {
  // تم العثور عليه - الرمز 200
  $response->setStatusCode(Response::HTTP_OK);
}
// الرد لا يحتوي على محتوى، بل رؤوس فقط HTTP
$response->send();

تعليقات

  • السطر 9: المستخدمون المصرح لهم، وهنا مستخدم واحد فقط باسم تسجيل الدخول [admin] وكلمة المرور [admin]؛
  • السطر 14: يتم استرداد معرف المستخدم من الرأس [PHP-AUTH-USER]. هذا ليس رأسًا أرسله العميل، بل رأسًا أنشأه الخادم PHP؛
  • السطر 15: يتم استرداد كلمة مرور المستخدم من الرأس [PHP-AUTH-PW]، وهو رأس تم إنشاؤه بواسطة PHP؛
  • السطر 17: يتم البحث عن المستخدم الذي يرغب في تسجيل الدخول في قائمة المستخدمين المصرح لهم؛
  • السطران 23-24: إذا لم يتم التعرف على المستخدم، يتم إرسال
    • السطر 23: الرمز [401 Unauthorized
    • السطر 24: رأس [WWW-Authenticate: Basic realm=”quelque chose”]. تتعرف معظم المتصفحات على هذا الرأس وتقوم بعرض نافذة مصادقة تطلب من المستخدم المصادقة. يجب ترميز الرؤوس HTTP بـ ISO 8859-1. أما نصوص Netbeans فهي مشفرة بـ UTF-8. وتضمن الدالة [utf8_decode] التحويل من UTF-8 إلى ISO 8859-1. وهنا، لم تكن هناك حاجة إليها لأن أحرف السلسلة [PHP7 par lexemple] هي نفسها في UTF-8 و ISO 8859-1. الدالة موجودة هنا فقط لتذكير بالترميز المستخدم في الرؤوس HTTP؛
  • السطر 25: إذا تم التعرف على المستخدم، يتم إرسال الرمز [200 OK] إلى العميل؛

لنطلب URL [auth-server.php] باستخدام متصفح:

Image

نلاحظ أن المتصفح يعرض نافذة مصادقة. في [2]، نرى قيمة الرأس [WWW-Authenticate] المرسلة من الخادم. إذا نظرنا إلى الرؤوس HTTP التي استقبلها المتصفح، نجد ما يلي:

1
2
3
4
5
6
7
8
9
HTTP/1.0 401 Unauthorized
Date: Fri, 07 Jun 2019 09:11:23 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
WWW-Authenticate: Basic realm="PHP7 par l'exemple"
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
  • السطر 1: رمز الاستجابة [401 Unauthorized]؛
  • السطر 6: الرأس HTTP [WWW-Authenticate
  • السطر 7: نص الاستجابة فارغ؛

إذا تم إدخال [admin] مرتين في [3-4]، فإن استجابة الخادم تكون كما يلي:

1
2
3
4
5
6
7
8
HTTP/1.0 200 OK
Date: Fri, 07 Jun 2019 09:21:00 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
  • السطر 1: الرمز 200 OK للرد؛
  • السطر 6: نص الرد فارغ؛

إذا أدخلنا بيانات اعتماد خاطئة في [3-4]، فإن المتصفح [Firefox] المستخدم في الاختبارات يعرض نافذة المصادقة بشكل متكرر إلى ما لا نهاية حتى يتم إدخال بيانات الاعتماد الصحيحة. في كل مرة تحدث عملية ذهاب وإياب مع الخادم، يكون الرد هو نفسه دائمًا، مما يؤدي إلى ظهور نافذة المصادقة في المتصفح.

لنقم بتشغيل العميل [auth-client.php] باستخدام مستخدم غير مصرح له. يكون رد الخادم كما يلي:


---Réponse avec statut : 401
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 401 Unauthorized returned for "https://localhost/php7/scripts-web/08/auth-server.php".
  • في [1]، تلقى العميل بالفعل رمز 401؛
  • في [3]، تم إطلاق استثناء في العميل. وقد أطلقها عميل Symfony [HttpClient]: فهو يطلق استثناءً عندما يشير رمز حالة الرد HTTP إلى وجود خطأ من جانب الخادم، ويحاول العميل قراءة رؤوس الرد أو محتواه. تُظهر الرسالة الواردة في السطر 3 أن الخادم رد بـ [HTTP/1.0 401 Unauthorized] للإشارة إلى أن المستخدم لم يتم التعرف عليه؛

لنقوم الآن بتشغيل العميل [auth-client.php] باستخدام المستخدم المصرح له [‘admin’,’admin’]. وعندئذٍ تكون استجابة الخادم كما يلي:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:11:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :

  • السطر 1: رد الخادم بـ [HTTP/1. 200 OK
  • السطر 7: لا يحتوي الرد على أي محتوى (0 بايت)؛

17.11.3. تأمين الاتصال بين العميل والخادم

لقد رأينا أنه من أجل المصادقة لدى الخادم، يرسل العميل الرأس:

authorization: Basic YWRtaW46YWRtaW4=

إذا تم اعتراض هذا السطر بواسطة برنامج تجسس، فسيتمكن هذا البرنامج بسهولة من استرجاع معرّفات [login, mot de passe] المشفرة بنظام قاعدة 64 في السلسلة [YWRtaW46YWRtaW4=]. لهذا السبب، يجب أن تتم المصادقة عبر اتصال آمن بين العميل والخادم. تستخدم معرفات URL الآمنة بروتوكول [HTTPS] بدلاً من بروتوكول HTTP. بروتوكول [HTTPS] هو بروتوكول HTTP ضمن اتصال آمن بين العميل والخادم. تأخذ عناوين URL الآمنة الشكل [https://chemin_document].

لا تقبل جميع خوادم الويب بروتوكولات URL بهذا الشكل. لذا يجب تعديلها لتصبح آمنة. خادم Apache الخاص بـ Laragon هو خادم آمن، لكن بروتوكول HTTPS غير نشط بشكل افتراضي. يجب تفعيله من قائمة Laragon:

Image

  • إلى [4]، يجب تفعيل تشفير SSL لخادم Apache؛

وبمجرد القيام بذلك، يتم إعادة تشغيل خادم Apache تلقائيًا:

Image

  • إلى [1]، يظهر قفل أخضر: وهذا دليل على أن بروتوكول HTTPS قد تم تفعيله؛
  • في [2]، يظهر منفذ خدمة جديد، وهو المنفذ 443 هنا. هذا هو منفذ خدمة بروتوكول HTTPS الآمن؛

والآن بعد أن أصبح لدينا خادم آمن، دعونا نعدل ملف التكوين [config-auth-client.json] الخاص بالعميل على النحو التالي:


{
    "url": "https://localhost:443/php7/scripts-web/08/auth-server.php"
}

في [2]، أصبح البروتوكول هو [https] والمنفذ هو [443].

الآن، لنقم بتشغيل العميل [auth-client.php] باستخدام المستخدم المصرح له [admin, admin]. نتائج وحدة التحكم هي كما يلي:

Erreur de communication avec le serveur : Peer certificate cannot be authenticated with given CA certificates for"https://localhost/php7/scripts-web/08/auth-server.php".

أطلق عميل Symfony [HttpClient] استثناءً لأن الخادم أرسل له شهادة موثوقة لم يقبلها [HttpClient]. تتم الاتصالات SSL باستخدام شهادات موثوقة معتمدة من قبل هيئات رسمية. عند تفعيل بروتوكول HTTPS على خادم Apache في Laragon، تم إنشاء شهادة موقعة ذاتيًا لخادم Apache. الشهادة الموقعة ذاتيًا هي شهادة لم يتم التحقق من صحتها من قبل جهة رسمية. وقد رفض عميل Symfony [HttpClient] هذه الشهادة الموقعة ذاتيًا.

يمكن طلب من [HttpClient] عدم التحقق من صحة الشهادة المرسلة من الخادم. ويتم ذلك باستخدام خيارات في الطريقة [HttpClient::create]:


// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    "verify_peer" => false
  ]);

السطر 4 يطلب عدم التحقق من شهادة الخادم. كنا قد واجهنا هذه المشكلة من قبل في البرنامج النصي [http-02.php] المذكور في الفقرة «الرابط». كان هذا البرنامج النصي يستخدم المكتبة [libcurl] للاتصال بموقعي HTTP و HTTPS. وقد استخدمنا حينها التكوين التالي لهذه المكتبة:


// تم تهيئة جلسة عمل cURL
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // حدث خطأ
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // خيارات curl
  $options = [
    // الوضع التفصيلي
    CURLOPT_VERBOSE => true,
    // اتصال جديد - لا يوجد ذاكرة تخزين مؤقت
    CURLOPT_FRESH_CONNECT => true,
    // مهلة انتظار الطلب (بالثواني)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // عدم التحقق من صحة الشهادات SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // متابعة عمليات إعادة التوجيه
    CURLOPT_FOLLOWLOCATION => true,
    // استرداد المستند المطلوب في شكل سلسلة أحرف
    CURLOPT_RETURNTRANSFER => true
  ];

  // إعدادات curl
curl_setopt_array($curl, $options);

في السطر 17، تتحكم الثابتة [CURLOPT_SSL_VERIFYPEER] في التحقق من الشهادة المرسلة من الخادم من عدمه. يُعد العميل [HttpClient] في الواقع عميلاً [curl] عندما يتم تنشيط الامتداد [curl] في تكوين PHP، كما هو الحال هنا. وبالتالي، فإن الفئة التي يتم إنشاء مثيل لها بواسطة [HttpClient::create] هي الفئة [CurlHttpClient]. وتكون ثوابت [curl] متاحة في هذه الفئة ولكن بأسماء أخرى:


$curlopts = [
            CURLOPT_URL => $url,
            CURLOPT_USERAGENT => 'Symfony HttpClient/Curl',
            CURLOPT_TCP_NODELAY => true,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
            CURLOPT_COOKIEFILE => '', // تتبع ملفات تعريف الارتباط أثناء عمليات إعادة التوجيه
            CURLOPT_CONNECTTIMEOUT_MS => 1000 * $options['timeout'],
            CURLOPT_PROXY => $options['proxy'],
            CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
            CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
            CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
            CURLOPT_CAINFO => $options['cafile'],
            CURLOPT_CAPATH => $options['capath'],
            CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
            CURLOPT_SSLCERT => $options['local_cert'],
            CURLOPT_SSLKEY => $options['local_pk'],
            CURLOPT_KEYPASSWD => $options['passphrase'],
            CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
        ];

لقد قمنا بتمييز الثوابت المستخدمة بواسطة [CurlHttpClient] باللون الأصفر.

إذا قمنا الآن بتشغيل العميل [auth-client] باستخدام حساب المستخدم [admin, admin فسنحصل على النتيجة التالية:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:44:37 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :

تم التعرف على المستخدم بشكل صحيح. إذا قمنا بتشغيل العميل [auth-client] باستخدام مستخدم غير [admin, admin]، فسنحصل على النتيجة التالية:

1
2
3
---Réponse avec statut : 403
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 403 Forbidden returned for "https://localhost/php7/scripts-web/08/auth-server.php".

الآن، أصبحنا نعرف كيفية المصادقة على خادم آمن.