23. تمرين تطبيقي – الإصدار 12
سنقوم في هذا الفصل بكتابة تطبيق ويب يتوافق مع بنية MVC (النموذج-العرض-المتحكم). سيكون بإمكان التطبيق تقديم إجاباته بثلاثة تنسيقات: jSON، XML، HTML. هناك فارق كبير في مستوى التعقيد بين ما سنقوم به الآن وما تم إنجازه سابقًا. سنعيد استخدام معظم المفاهيم التي تناولناها حتى الآن، وسنشرح بالتفصيل جميع الخطوات المؤدية إلى التطبيق النهائي.
23.1. بنية MVC
سنقوم بتنفيذ نموذج البنية المعروف باسم MVC (النموذج – العرض – وحدة التحكم) بالطريقة التالية:

ستتم معالجة طلب العميل على النحو التالي:
- 1 - الطلب
ستكون طلبات URL بالصيغة التالية: http://machine:port/contexte/….?action=uneAction¶m1=v1¶m2=v2&… سيستخدم [Contrôleur principal] ملف تكوين لتوجيه الطلب إلى وحدة التحكم الصحيحة والإجراء الصحيح داخل هذه الوحدة. ولهذا الغرض، سيستخدم الحقل [action] الموجود في URL. أما باقي أجزاء URL و[param1=v1¶m2=v2&…] فهي تتكون من معلمات اختيارية سيتم تمريرها إلى الإجراء. الحرف C في MVC هو هنا السلسلة [Contrôleur principal, Contrôleur / Action]. إذا لم يتمكن أي وحدة تحكم من معالجة الإجراء المطلوب، فسيرد خادم الويب بأن URL المطلوب لم يتم العثور عليه.
- 2 - المعالجة
- يمكن للإجراء المختار [2a] الاستفادة من المعلمات parami التي أرسلها إليه الإجراء [Contrôleur principal]. وقد تأتي هذه المعلمات من عدة مصادر:
- المسار [/param1/param2/…] الخاص بـ URL،
- من المعلمات [param1=v1¶m2=v2] الخاصة بـ URL,
- من المعلمات التي يرسلها المتصفح مع طلبه؛
- في معالجة طلب المستخدم، قد تحتاج العملية إلى الطبقة [métier] [2b]. بمجرد معالجة طلب العميل، يمكن أن يستدعي هذا الطلب استجابات متنوعة. ومن الأمثلة النموذجية على ذلك:
- استجابة خطأ إذا تعذر معالجة الطلب بشكل صحيح؛
- استجابة تأكيد في الحالات الأخرى؛
- سترسل [Contrôleur / Action] ردها [2c] إلى وحدة التحكم الرئيسية بالإضافة إلى رمز الحالة. وستمثل رموز الحالة هذه بشكل فريد الحالة التي يمر بها التطبيق. وستكون هذه الرموز إما رموز نجاح أو رموز خطأ؛
- يمكن للإجراء المختار [2a] الاستفادة من المعلمات parami التي أرسلها إليه الإجراء [Contrôleur principal]. وقد تأتي هذه المعلمات من عدة مصادر:
- 3 - الرد
- حسب ما إذا كان العميل قد طلب استجابة jSON، أو XML أو HTML، سيقوم [Contrôleur principal] بإنشاء مثيل [3a] لنوع الرد المناسب وسيطلب منه إرسال الرد إلى العميل. وسيقوم [Contrôleur principal] بنقل كل من الرد ورمز الحالة المقدمين من [Contrôleur / Action] الذي تم تنفيذه؛
- وإذا كان الرد المطلوب من النوع jSON أو XML، فسيقوم الرد المحدد بتنسيق الرد الوارد من [Contrôleur / Action] الذي تم تزويده به وإرساله إلى [3c]. يمكن أن يكون العميل القادر على استغلال هذه الإجابة عبارة عن برنامج نصي لوحدة التحكم PHP أو برنامج نصي جافا سكريبت مضمن في صفحة HTML؛
- إذا كانت الاستجابة المطلوبة من النوع HTML، فإن الاستجابة المحددة ستختار إحدى العروض HTML أو [Vuei] باستخدام رمز الحالة الذي تم تزويدها به. وهذا هو العرض V الخاص بـ MVC. يرتبط كل رمز حالة بعرض واحد فقط. سيقوم هذا العرض V بعرض استجابة [Contrôleur / Action] التي تم تنفيذها. وهي تقوم بتنسيق بيانات هذه الاستجابة باستخدام HTML وCSS وJavaScript. وتُسمى هذه البيانات «نموذج العرض». وهو الحرف M في MVC. وعادةً ما يكون العميل هو متصفح؛
الآن، دعونا نوضح العلاقة بين بنية الويب MVC وبنية الطبقات. وفقًا للتعريف الذي نعطيه للنموذج، قد يكون هذان المفهومان مرتبطين أو غير مرتبطين. لنأخذ تطبيق ويب أحادي الطبقة MVC كمثال:

في المثال أعلاه، يتضمن كل عنصر من عناصر [Contrôleur / Action] جزءًا من الطبقتين [métier] و [dao]. في الطبقة [web]، توجد بالفعل بنية MVC، لكن التطبيق ككل لا يعتمد على بنية متعددة الطبقات. هنا، هناك طبقة واحدة فقط تتولى كل المهام.
الآن، لننظر إلى بنية ويب متعددة الطبقات:

يمكن تنفيذ الطبقة [web] دون اتباع النموذج MVC. وبذلك يكون لدينا بالفعل بنية متعددة الطبقات، لكن طبقة الويب لا تنفذ النموذج MVC.
على سبيل المثال، في عالم .NET، يمكن تنفيذ الطبقة [web] المذكورةأعلاه يمكن تنفيذها باستخدام ASP.NET و MVC، وبذلك نحصل على بنية متعددة الطبقات تحتوي على طبقة [web] من النوع MVC. وبعد ذلك، يمكن استبدال هذه الطبقة ASP.NET وMVC بطبقة ASP.NET تقليدية (WebForms) مع الحفاظ على بقية (المجال، DAO، برنامج التشغيل) كما هو. وبذلك نحصل على بنية طبقات تحتوي على طبقة [web] التي لم تعد من النوع MVC.
في MVC، ذكرنا أن النموذج M هو نموذج العرض V، c.a.d. مجموعة البيانات المعروضة بواسطة العرض V. يتم تقديم تعريف آخر للنموذج M لـ MVC:

يعتبر العديد من المؤلفين أن ما يقع على يمين الطبقة [web] يشكل النموذج M لـ MVC. لتجنب الغموض، يمكننا التحدث عن:
- نموذج المجال عند الإشارة إلى كل ما يقع على يمين الطبقة [web]؛
- نموذج العرض عند الإشارة إلى البيانات المعروضة بواسطة عرض V؛
23.2. شجرة مشروع Netbeans
سنعتمد لمشروع Netbeans بنية تعكس النموذج MVC:

- [3]: [main.php] هو وحدة التحكم الرئيسية لنموذجنا MVC. وهو يمثل الحرف C في MVC؛
- [4]: سيحتوي الملف [Controllers] على وحدات التحكم الثانوية. كل وحدة تتولى معالجة إجراء معين. ويُشار إلى هذا الإجراء في URL، على سبيل المثال […/main.php?action=authentifier-utilisateur]. وبناءً على هذا الإجراء، سيقوم [Contrôleur principal] و[main.php] باختيار [Contrôleur secondaire]، وهو هنا [AuthentifierUtilisateurController]، لمعالجة الإجراء المطلوب. وتشكل وحدات التحكم هذه أيضًا جزءًا من C الخاص بـ MVC؛
- [5]: سيحتوي المجلد [Model] على الطبقات [métier] و [dao] الخاصة بالتطبيق. وفقًا للمصطلحات المعتمدة سابقًا، تمثل هذه العناصر نموذج المجال ووفقًا للمصطلحات المعتمدة للحرف M، يمكن أن تمثل الحرف M في MVC؛
- [6]: يحتوي الملف [Responses] على الفئات المسؤولة عن إرسال الرد إلى العميل. توجد فئة واحدة لكل نوع من أنواع الردود المطلوبة:
- [JsonResponse]: للاستجابة jSON؛
- [XmlResponse]: للاستجابة XML؛
- [HtmlResponse]: استجابة لـ HTML؛
- [7]: المجلد [Views] يحتوي على طرق العرض HTML عندما يكون الرد المطلوب هو HTML. وهذا هو «V» في MVC. يتم تنشيطها بواسطة الفئة [HtmlResponse] التي تنقل إليها البيانات المراد عرضها. هذه البيانات هي نموذج العرض. وفقًا للمصطلحات المعتمدة لـ «M»، يمكن أن تكون هذه البيانات هي «M» في MVC؛
- [8]: المجلد [Utilities] يحتوي على أدوات مساعدة:
- [Logger]: الفئة التي تسمح بإنشاء سجلات في ملف نصي؛
- [Sendmail]: الفئة التي تسمح بإرسال رسائل البريد الإلكتروني؛
- [9]: المجلد [Logs] يحتوي على ملف السجلات [logs.txt]؛
- [10]: المجلد [Entities] يحتوي على الفئات المستخدمة من قبل مختلف وحدات التحكم؛
باستخدام هذه الشجرة، يمكن وصف مسار معالجة إجراء طلبه أحد العملاء:
- [main.php] [3] يستقبل الطلب؛
- بعد إجراء بعض الفحوصات الأولية (هل الإجراء من ضمن الإجراءات المقبولة؟)، يقوم بإرسال الطلب إلى وحدة التحكم الثانوية [4] المكلفة بمعالجة هذا الإجراء؛
- يقوم المراقب الثانوي بما عليه القيام به. في عمله، قد يحتاج إلى الطبقات [métier] و [dao] و [5] بالإضافة إلى كيانات الملف [10]. ويقدم ردّه إلى وحدة التحكم الرئيسية [main.php] التي قامت بتفعيله؛
- ووفقًا لنوع الرد [jSON, XML, HTML] الذي يطلبه العميل، يقوم المراقب الرئيسي [main.php] بتنشيط أحد الردود الموجودة في الملف [Responses] [6]؛
- تقوم الاستجابات [JsonResponse, XmlResponse] بإرسال الاستجابة jSON أو XML إلى العميل على التوالي؛
- يستخدم الرد [HtmlResponse] إحدى طرق العرض في المجلد [Views] [7] لإرسال الرد HTML إلى العميل؛
- يمكن لمختلف وحدات التحكم الوصول إلى الفئة [Logger] الموجودة في المجلد [8] لكتابة السجلات في ملف السجلات الموجود في المجلد [9]. يتم تسجيل ما يلي:
- الإجراء المطلوب؛
- استجابة وحدة التحكم الخاصة به. يتم تسجيل هذه الاستجابة بتنسيق jSON بغض النظر عن النوع [jSON, XML, HTML] المطلوب؛
- في حالة حدوث خطأ فادح (HTTP_INTERNAL_SERVER_ERROR)، يقوم وحدة التحكم الرئيسية [main.php] بإرسال بريد إلكتروني إلى المسؤول باستخدام الفئة [SendMail] الموجودة في المجلد [8]؛
23.3. إجراءات التطبيق
يقوم العميل بإرسال الإجراء المطلوب تنفيذه إلى خادم الويب في شكل معلمة [action] ضمن URL [/main.php?action=xxx]. يتم سرد الإجراءات المسموح بها في الملف [config.json] الذي يقوم بتكوين وحدة التحكم الرئيسية [main.php]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
- السطر 1: المفتاح [actions] من القاموس jSON؛
- الأسطر 3-9: قاموس [action:contrôleur]. ترتبط كل عملية بوحدة التحكم الثانوية المسؤولة عن معالجتها؛
- السطر 3: [init-session]: يبدأ جلسة محاكاة لحسابات الضرائب. يشير هذا الإجراء إلى نوع الردود المطلوبة [jSON, XML, HTML]؛
- السطر 4: بمجرد تحديد نوع الجلسة، سيتعين على العميل توثيق هويته باستخدام الإجراء [authentifier-utilisateur]. وطالما لم يتم توثيق هويته، تُحظر جميع الإجراءات الأخرى باستثناء [init-session]؛
- السطر 5: بمجرد المصادقة، سيتمكن العميل من إجراء سلسلة من حسابات الضرائب باستخدام الإجراء [calculer-impot]؛
- السطر 6: في أي وقت، يمكن للعميل طلب الاطلاع على قائمة المحاكاة التي أجراها باستخدام الإجراء [lister-simulations]؛
- السطر 7: يمكنه حذف بعضها باستخدام الإجراء [supprimer-simulation]؛
- السطر 8: ينهي العميل جلسة المحاكاة الخاصة به باستخدام الإجراء [fin-session]. ومنذ تلك اللحظة، سيتعين عليه إعادة تسجيل الدخول إذا أراد استخدام التطبيق؛
- السطر 9: في التطبيق HTML، تطلب الإجراء [afficher-calcul-impot] عرض النموذج الذي يسمح بحساب الضريبة؛
23.4. تكوين تطبيق الويب
يتم تكوين التطبيق بواسطة الملف jSON [config.json] التالي:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/Simulation.php",
"/Entities/Database.php",
"/Entities/TaxAdminData.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php",
"/Model/InterfaceServerDao.php",
"/Model/ServerDao.php",
"/Model/ServerDaoWithSession.php",
"/Model/InterfaceServerMetier.php",
"/Model/ServerMetier.php",
"/Responses/InterfaceResponse.php",
"/Responses/ParentResponse.php",
"/Responses/JsonResponse.php",
"/Responses/XmlResponse.php",
"/Responses/HtmlResponse.php",
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
"users": [
{
"login": "admin",
"passwd": "admin"
}
],
"adminMail": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "plantage du serveur de calcul d'impôts",
"tls": "FALSE",
"attachments": []
},
"logsFilename": "Logs/logs.txt",
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
تعليقات
- السطر 2: اسم الملف jSON الذي يحتوي على تكوين الوصول إلى قاعدة البيانات؛
- الأسطر 3-39: تكوين تبعيات المشروع. يتم هنا سرد جميع البرامج النصية PHP الموجودة في شجرة المشروع؛
- الأسطر 40-44: المستخدم المصرح له باستخدام التطبيق؛
- الأسطر 46-54: بيانات البريد الإلكتروني لمسؤول التطبيق؛
- السطر 55: مسار ملف السجلات؛
- الأسطر 56-65: ارتباطات [action => contrôleur secondaire chargé de la traiter]؛
- الأسطر 66-70: ارتباطات [type de réponse => classe Response chargée d’envoyer la réponse au client]؛
- الأسطر 71-75: ارتباطات [vue HTML => tableau des codes d’état menant à cette vue]؛
- السطر 76: يتم عرض العرض [vue-erreurs] في جلسة HTML في كل مرة تحدث فيها خطأ غير عادي:
- عادةً ما يتم استعلام تطبيق jSON أو XML باستخدام عميل مبرمج. يقوم هذا العميل بتمرير معلمات إلى الخادم قد تكون غائبة أو خاطئة. تعالج جميع وحدات التحكم هذه الحالات وترسل رموز الأخطاء إلى العميل. يجب معالجة جميع حالات الخطأ المحتملة؛
- أما مع تطبيق HTML، فالأمر يختلف قليلاً. عند الاستخدام العادي، لا يستخدم تطبيق الويب سوى جزء من حالات الاستخدام المحتملة للعملاء jSON وXML. لنأخذ مثالاً: تتوقع الإجراء [calculer-impot] ثلاثة معلمات مرسلة (يتم إرسالها بواسطة POST): [marié, enfants, salaire].
- إذا كان لدينا عميل jSON يسمح بإدخال URL يدويًّا، يمكننا طلب الإجراء [calculer-impot] باستخدام GET بدلاً من POST، أو باستخدام POST بدون أي معلمة مرسلة في حين أنه يلزم ثلاث معلمات، إلخ... يجب أن يعالج الخادم jSON جميع هذه الحالات؛
- مع تطبيق ويب، سيتم طلب الإجراء [calculer-impot] من نموذج ويب حيث لن يكون أي من الحالتين السابقتين ممكنًا: سيتم طلب الإجراء [calculer-impot] مع POST والمعلمات الثلاثة [marié, enfants, salaire]. قد تكون قيمة بعض هذه المعلمات غير صحيحة، لكنها ستكون موجودة. ومع ذلك، يمكن للمستخدم إعادة إنتاج بعض الأخطاء عن طريق كتابة URL بنفسه في المتصفح. ولأغراض الأمان، يجب التعامل مع هذه الحالة؛
- سيتم عرض العرض [vue-erreurs] في كل مرة يقوم فيها وحدة تحكم ثانوية بإرجاع رمز حالة غير متوافق مع التطبيق الويب، أي رمز حالة غير موجود في الأسطر 72-74 من ملف التكوين. نختار هذا الحل لأغراض تعليمية. هناك خيار آخر ممكن وهو عدم القيام بأي شيء والاكتفاء بإعادة عرض العرض المعروض حاليًا في متصفح العميل حتى يشعر المستخدم بأن الخادم لا يستجيب لطلبات URL التي تم إنشاؤها يدويًّا؛
23.5. تثبيت الأدوات والمكتبات
23.5.1. Postman
[Postman] هي الأداة التي ستسمح لنا باستعلام مختلف URL في تطبيق الويب الخاص بنا. وهي تتيح لنا:
- استخدام أي URL: حيث يتم إنشاؤها يدويًّا؛
- إرسال استعلامات إلى خادم الويب باستخدام ملفات GET، POST، PUT، OPTIONS…؛
- تحديد معلمات GET أو POST؛
- تحديد رؤوس HTTP للطلب؛
- تلقي استجابة بتنسيق jSON، XML، HTML،
- الوصول إلى رؤوس الرد HTTP. وبذلك يمكننا الوصول إلى الرد الكامل HTTP الصادر عن الخادم؛
وبما أننا نقوم يدويًّا بإنشاء الاستعلامات URL، فسنتمكن من اختبار جميع حالات الخطأ المحتملة ورؤية كيفية استجابة الخادم.
[Postman] متاح على URL و[https://www.getpostman.com/downloads/]. الإصدار المتوفر في يونيو 2019 هو الإصدار 7.2. ويحتوي هذا الإصدار على خلل: عند إرسال طلبات متتالية إلى خادم الويب المستهدف، لا يقوم العميل [Postman 7.2] بإعادة إرسال ملفات تعريف الارتباط التي يرسلها إليه الخادم تلقائيًا، ولا سيما ملف تعريف ارتباط الجلسة. للحفاظ على الجلسة، يجب عندئذ نسخ ملف تعريف ارتباط الجلسة يدويًا في رؤوس HTTP للطلبات المتتالية. هذا ليس معقدًا للغاية ولكنه غير عملي. إنه خطأ لم يكن موجودًا في الإصدارات السابقة. وإدراكًا لهذا الخطأ، قامت فريق [Postman] بتصحيحه في إصدار ألفا (قد يكون غير مستقر) يسمى [Postman Canary] متاح على URL [https://www.getpostman.com/downloads/canary]. وهذا هو الإصدار المستخدم هنا. سنشرح كيفية تثبيته. إذا كان هناك إصدار مستقر [Postman 7.3] أو أحدث متاحًا، فيمكنك تنزيله: فمن المرجح أن يكون الخلل قد تم تصحيحه.
قم بتثبيت إصدارك من [Postman]. أثناء التثبيت، سيُطلب منك إنشاء حساب: لن يكون هذا الحساب ضروريًا هنا. يُستخدم حساب [Postman] لمزامنة الأجهزة المختلفة بحيث يتم نسخ إعدادات جهاز ما إلى جهاز آخر. لا فائدة من أي من هذا هنا.
بمجرد التثبيت، يعرض [Postman] الواجهة التالية:

- في [2-3]، يمكن الوصول إلى إعدادات المنتج؛

- في [6]، الإصدار المستخدم في هذا المستند؛
- إذا قمت بإنشاء حساب، تتم عملية مزامنة بين جهازك وخادم [Postman] بعيد. ويُرمز إلى ذلك بالعجلة [7] التي تدور في كل مرة تقوم فيها بإجراء تعديلات في المشروع [Postman]. لإيقاف هذه المزامنة غير الضرورية، قم بتسجيل الخروج من [8-9]؛
23.5.2. مكتبة Symfony / Serializer
لتسلسل الكائنات في jSON و XML، سنستخدم مكتبة [Symfony / Serializer]. وتتميز هذه المكتبة بميزتين:
- إنها متسقة في استخدامها للتسلسل إلى jSON أو XML: وهذا يغني عن تعلم واجهتي برمجة تطبيقات (API) مختلفتين؛
- بشكل أصلي، يمكنها تسلسل الكائنات في jSON أو XML، حتى لو كانت سماتها خاصة. نتذكر أنه في jSON، لتسلسل كائن ما، كان من الضروري أن تقوم فئته بتنفيذ واجهة [\JsonSerializable]. وكانت النتيجة التي يتم الحصول عليها حينها هي السلسلة jSON التي تمثل مصفوفة مرتبطة تحتوي على سمات الفئة كمفاتيح. وعند إزالة التسلسل عن هذه السلسلة jSON، كنا نحصل على المصفوفة المرتبطة البدائية، والتي كان يتعين بعد ذلك تحويلها إلى كائن من الفئة التي تم تسلسلها. مع [Symfony / Serializer]، ينتج عن عملية إزالة التسلسل على الفور كائن من الفئة التي تم تسلسلها. هذا أبسط؛
تتوفر وثائق مكتبة [Symfony / Serializer] على الرابط: URL: [https://symfony.com/doc/current/components/serializer.html] (يونيو 2019).
لتثبيت هذه المكتبة، افتح محطة Laragon (انظر الفقرة الرابط) واكتب الأمر التالي:

- في [1]، أمر تثبيت المكتبة [symfony/serializer]؛
- في [2]، مكتبة أخرى ضرورية لمشروعنا: تسمح بتسلسل الكائنات؛

23.6. كيانات التطبيق

تم استخدام كيانات [BaseEntity, Database, ExceptionImpots, TaxAdminData] بدءًا من الإصدار 08 من خدمة الويب (انظر الفقرة "الرابط").
ستُستخدم الفئة [Simulation] لتغليف عناصر محاكاة حساب الضريبة:
<?php
namespace Application;
class Simulation extends BaseEntity {
// سمات محاكاة حساب الضريبة
protected $marié;
protected $enfants;
protected $salaire;
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// وظائف الاسترجاع
public function getMarié() {
return $this->marié;
}
public function getEnfants() {
return $this->enfants;
}
public function getSalaire() {
return $this->salaire;
}
public function getImpôt() {
return $this->impôt;
}
public function getSurcôte() {
return $this->surcôte;
}
public function getDécôte() {
return $this->décôte;
}
public function getRéduction() {
return $this->réduction;
}
public function getTaux() {
return $this->taux;
}
}
تعليقات
- السطر 5: الفئة [Simulation] تمتد من الفئة [BaseEntity]، وبالتالي ترث الطرق التالية:
- [setFromArrayOfAttributes($arrayOfAttributes)]: الذي يسمح بتهيئة سمات الفئة؛
- [__toString]: الذي يعرض السلسلة jSON للكائن؛
- الأسطر 7-14: سمات المحاكاة؛
- الأسطر 16-47: متغيرات الحصول على القيم (getters) للفئة؛
23.7. الأدوات المساعدة للتطبيق
![]()
تسمح الفئة [Logger] بتسجيل الأحداث في ملف نصي. تم وصف هذه الفئة في الفقرة الرابط.
تسمح الفئة [SendAdminMail] بإرسال بريد إلكتروني إلى مسؤول التطبيق. تم وصف هذه الفئة في الفقرة الرابط.
23.8. الطبقات [métier] و [dao]


يتم تجميع الفئات والواجهات الخاصة بالطبقات [métier] و [dao] في المجلد [Model]. وقد تم تعريفها جميعًا واستخدامها في الإصدارات السابقة:
ExceptionImpots | فئة الاستثناءات التي تطلقها الطبقة [dao]. تم تعريفها في الفقرة الرابط. |
InterfaceServerDao | الواجهة التي تم تنفيذها بواسطة طبقة [dao] للخادم. تم تعريفها في الفقرة الرابط. |
ServerDao | تنفيذ واجهة [InterfaceServerDao]. تنفذ الطبقة [dao] للخادم. محددة في الفقرة الرابط. |
ServerDaoWithSession | تنفيذ واجهة [InterfaceServerDao]. تنفذ طبقة [dao] الخاصة بالخادم. محددة في الفقرة الرابط. |
InterfaceServerMetier | واجهة تم تنفيذها بواسطة طبقة [métier] للخادم. محددة في الفقرة الرابط. |
ServerMetier | تنفيذ واجهة [InterfaceMetier]. تنفذ الطبقة [metier] للخادم. محددة في الفقرة الرابط. |
يستخدم التطبيق قيد الكتابة العديد من العناصر التي تم عرضها واستخدامها سابقًا:
- الطبقات [métier] و [dao]؛
- الأدوات المساعدة [Logger] و [SendAdminMail]؛
- الكيانات [ExceptionImpots, TaxAdminData, Database]؛
سنركز على الطبقة [web] في التطبيق:

23.9. وحدة التحكم الرئيسية [main.php]
23.9.1. مقدمة

- [1-2]: يتم تكوين وحدة التحكم الرئيسية [main.php] [1] بواسطة الملف [config.json] [2]؛
دعونا نستذكر موقع وحدة التحكم الرئيسية في بنيتنا MVC:

في [1]، تعد وحدة التحكم الرئيسية [main.php] العنصر الأول في البنية MVC الذي يعالج طلب العميل. ولها عدة أدوار:
- يقوم أولاً بإجراء عمليات التحقق الأساسية:
- هل ملف التكوين الخاص به موجود وصالح؛
- تحميل جميع التبعيات الخاصة بالمشروع. وهذا يعني تحميل جميع عناصر بنية MVC؛
- هل تم تحديد الإجراء المطلوب؟ إذا كان الجواب نعم، فهل هو صالح؟
- إذا كانت الإجراء المطلوب صالحًا، يتم اختيار [2a] وحدة التحكم الثانوية التي ستعالجه وتمرير المعلومات التي تحتاجها إليها: الطلب HTTP، والجلسة، وتكوين التطبيق؛
- استرداد [2c] استجابة وحدة التحكم الثانوية. وفقًا للنوع (jSON، XML، HTML) التطبيق الذي طلبه العميل، قم باختيار [3a] الاستجابة (JsonResponse، XmlResponse، HtmlResponse) المكلفة بإرسال الرد إلى العميل وتزويده بجميع المعلومات التي يحتاجها (الطلب HTTP، الجلسة، تكوين التطبيق، رد وحدة التحكم الثانوية)؛
- بمجرد إرسال هذا الرد [3c]، الشروع في تحرير الموارد التي تم تخصيصها لمعالجة الطلب؛
23.9.2. [main.php] - 1
رمز وحدة التحكم الرئيسية [main.php] هو كما يلي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// إدارة الأخطاء بواسطة PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// استرداد التكوين
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// خطأ؟
if (!$fileContents) {
// تسجيل الخطأ
$état = 131;
$erreur = TRUE;
$message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
// استرداد الرمز JSON من ملف التكوين في مصفوفة مرتبطة
$config = \json_decode($fileContents, true);
// خطأ؟
if (!$config) {
// يتم تسجيل الخطأ
$erreur = TRUE;
$état = 132;
$message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
}
}
// خطأ؟
if ($erreur) {
// إعداد الرد JSON من الخادم
// لا يمكن الاستعانة بملف التكوين
// التبعيات الخاصة بـ Symfony
require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
// تحضير الرد
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// رمز الحالة
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
// المحتوى
$response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
// الإرسال
$response->send();
// النهاية
exit;
}
…
تعليقات
- الأسطر 10-12: يستخدم المراقب الرئيسي كائنات Symfony التالية:
- [Request]: الطلب HTTP قيد المعالجة؛
- [Session]: جلسة عمل تطبيق الويب؛
- [Response]: الاستجابة HTTP الموجهة إلى العميل؛
- السطر 15: خلال مرحلة التطوير بأكملها، سنبقي هذا السطر في شكل تعليقات: يتم عندئذٍ تضمين أخطاء PHP في تدفق النص المرسل إلى العميل. إذا كان هذا العميل متصفحًا، فإن ذلك يتيح رؤية الأخطاء التي واجهها الخادم. وهذا يساعد في تصحيح الأخطاء؛
- السطر 16: يتم الإبلاغ عن جميع الأخطاء (E_ALL) باستثناء التحذيرات (! E_WARNING) والمعلومات غير الحرجة (! E_NOTICE). على سبيل المثال، إذا تعذر فتح ملف ما، فإن PHP يصدر خطأ من النوع [E_NOTICE]. إذا كان السطر 15 يسمح بعرض الأخطاء، فإن خطأ فتح الملف يظهر في متصفح العميل. هذا جيد إذا نسيت اختبار نتيجة فتح الملف، ولكنه ليس جيدًا إذا كنت قد خططت للاختبار: حيث سيؤدي سطر من [notice] إلى تشويش استجابة الخادم للعميل. في مرحلة التطوير، يجب أيضًا تعليق السطر 16: فأنت لا تريد أن تفوت أي خطأ؛
- السطر 19: يتم قراءة ملف التكوين؛
- الأسطر 22-27: إذا لم تسر عملية القراءة على ما يرام، يتم تسجيل الخطأ (السطر 25)، ووضع التطبيق في الحالة [131] وإعداد رسالة خطأ؛
- السطر 30: يتم فك تشفير السلسلة jSON من ملف التكوين؛
- الأسطر 32-37: إذا فشل فك التشفير، يتم تسجيل الخطأ (السطر 34)، ووضع التطبيق في الحالة [132]، وإعداد رسالة خطأ؛
- الأسطر 40-57: في حالة حدوث خطأ في قراءة ملف التكوين، يتعذر المضي قدمًا. عندئذ يتم إعداد استجابة jSON للعميل:
- السطر 44: نظرًا لعدم قراءة ملف التكوين، يجب استيراد الملف [autoload] الضروري لـ [Symfony] يدويًّا؛
- السطران 46-47: يتم إعداد استجابة jSON؛
- السطر 50: سيكون رمز HTTP للرد هو 500 INTERNAL_SERVER_ERROR؛
- السطر 52: يتم تحديد محتوى الرد jSON. ستحتوي جميع الردود الصادرة عن تطبيق الويب قيد الدراسة على ثلاثة مفاتيح:
- [action]: الإجراء المطلوب من قبل العميل؛
- [état]: حالة التطبيق بعد تنفيذ هذا الإجراء؛
- [réponse]: استجابة خادم الويب؛
- السطر 54: يتم إرسال الاستجابة jSON إلى العميل؛
23.9.3. اختبارات [Postman] - 1
سنقوم بالتحقق من سلوك الخادم في حالة عدم وجود ملف التكوين أو في حالة وجود خطأ فيه:

سنقوم بتجميع الطلبات المختلفة التي سيقوم عميلنا [Postman] بإرسالها إلى خادم الضرائب في مجموعات.
- في [1]، قم بإنشاء مجموعة جديدة؛
- في [2]، قم بتسميتها؛
- في [3]، الوصف اختياري؛

- في المجموعات [4]، تظهر الآن مجموعة باسم [impots-server-tests-version12] [5]؛
- في [6]، يمكن إضافة استعلام جديد إلى المجموعة؛

- في [7]، يتم تسمية الاستعلام؛
- في [8]، يكون الوصف اختياريًا؛

- في [9-11]، الطلب المضاف إلى المجموعة؛
- في [12]، اختيار نوع الطلب، هنا طلب [GET]. في [19]، أنواع الطلبات المختلفة المتاحة؛
- في [13]، يتم هنا إدخال عنوان URL للخادم؛
- في [14]، نضع هنا المعلمات المضافة إلى URL والتي ستصبح بالتالي معلمات لـ GET. والفائدة من إدخالها هنا بدلاً من إدخالها مباشرةً في URL هي أنها ستخضع لترميز URL بواسطة [Postman]. إذا قمت بإدخالها بنفسك في URL، فسيكون عليك أنت القيام بترميز URL لها؛
- في [15]، يستخدم [Authorization] لتحديد المستخدم الذي سيقوم بتسجيل الدخول. لن نحتاج إلى استخدام هذه الإمكانية؛
- في [16]، الرؤوس HTTP التي ستصاحب الطلب. يتم تضمين عدد معين من الرؤوس تلقائيًا في الطلب. يمكنك هنا إضافة رؤوس جديدة؛
- في [17]، يشير [Body] إلى معلمات عملية [POST]. سنضطر إلى استخدام هذا الخيار؛
سنقوم بإجراء الاختبار التالي:
- في [main.php]، نحدد أن ملف التكوين هو [config2.json] الذي لا وجود له:

- يجب إزالة التعليق عن السطر 16 من الكود؛
- السطر 18: الخطأ في اسم ملف التكوين؛
دعونا ندخل إلى [Postman] [13, 20]، وURL الخاص بخادم الويب لحساب الضرائب، ونقوم بتنفيذ [21]:

الرد الذي يرسله الخادم (يجب بالطبع أن يكون Laragon نشطًا) هو التالي:

- في [22]، أرسل الخادم رمزًا HTTP [500 Internal Server Error]؛
- في [23]، يشير [Body] إلى نص الرد، أي المستند الذي أرسله الخادم خلف الرؤوس HTTP [28]؛
- في [26]، نرى أن [Postman] قد تلقى استجابة jSON؛
- في [27]، الرد jSON بعد التنسيق؛
- في [28]، الرد jSON في صيغته الأولية دون تنسيق؛
- في [29]، يُستخدم الوضع [Preview] عندما تكون الاستجابة هي HTML. وعندئذٍ يعرض الوضع [Preview] الصفحة المستلمة؛
- في [30]، يتم عرض استجابة الخادم jSON. هذه هي الاستجابة التي كنا ننتظرها؛
في [25]، تكون الرؤوس HTTP المرسلة في استجابة الخادم كما يلي:

- في [32]، نوع الاستجابة هو jSON؛
وقد أتاح لنا هذا الاختبار الأول أن نرى أنه:
- يمكن إرسال أي نوع من الطلبات إلى الخادم قيد الاختبار؛
- يمكن تعيين معلمات GET أو POST؛
- الحصول على الرد بالكامل: رؤوس HTTP والمستند الذي يلي هذه الرؤوس [Body]؛
الآن، لنقم باختبار ثانٍ:

- في [1-3]، الملف [config3.json] هو ملف jSON غير صحيح من الناحية النحوية؛
- في [4]، تم تكوين [main.php] لاستخدام [config3.json]؛
نضيف استعلامًا جديدًا في [Postman]:

- في [1-3]، نضغط بزر الفأرة الأيمن على [2] ونختار الخيار [duplicate] لتكرار الاستعلام [2]؛
- في [4]، الطلب الجديد له اسم محدد مسبقًا يتم تغييره إلى [5]؛

- إلى [6]، الطلب الذي تم تغيير اسمه؛
- إلى [9-10]، يتم إرسال نفس الطلب GET كما في السابق؛

- في [11]، الرد jSON من الخادم؛
لقد أوضحنا هنا كيف سيتم اختبار الإجراءات المختلفة لخدمة الويب الخاصة بحساب الضريبة.
23.9.4. [main.php] – 2
نواصل دراسة كود وحدة التحكم الرئيسية [main.php]:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// إدارة الأخطاء بواسطة PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// يتم استرداد التكوين
$configFilename = "config.json";
…
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require_once "$rootDirectory$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require_once "$dependency";
}
// إنشاء ملف السجلات
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// تعذر إنشاء ملف السجلات - خطأ داخلي في الخادم
$état = 133;
(new JsonResponse())->send(
NULL, NULL, $config,
Response::HTTP_INTERNAL_SERVER_ERROR,
["action" => "non déterminée", "état" => $état, "réponse" => "Le fichier de logs [{$config['logsFilename']}] n'a pu être créé"],
[]);
// انتهى
exit;
}
تعليقات
- السطر 18: لدينا ملف تكوين [config.json] موجود الآن وصحيح من الناحية النحوية. يجب أيضًا اختبار ما إذا كانت المفاتيح المتوقعة في هذا الملف موجودة بالفعل. سنعتبر أن هذا جزء من العمل العادي لتصحيح الأخطاء الذي يقوم به المطور. كان بإمكاننا اتباع نفس المنطق بالنسبة للخطأين السابقين؛
- الأسطر 20-28: يتم تضمين جميع التبعيات اللازمة لمشروع الويب. وقد صادفنا هذا الكود عدة مرات من قبل؛
- الأسطر 31-43: نحاول إنشاء الكائن [Logger] الذي سيسمح لنا بتسجيل الأحداث في الملف [$config['logsFilename']]. قد يفشل هذا الإنشاء؛
- الأسطر 33-43: معالجة خطأ إنشاء الكائن [Logger]؛
- السطر 35: يتم تعيين رقم الحالة؛
- الأسطر 36-40: إرسال استجابة jSON؛
- السطر 42: إيقاف البرنامج النصي؛
جميع الردود المرسلة إلى العميل تنفذ واجهة [InterfaceResponse] التالية:

رمز واجهة [InterfaceResponse] هو التالي:
<?php
namespace Application;
// التبعيات الخاصة بـ Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// الطلب $request: يجري معالجة الطلب
// جلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الاستجابة
// مسجل $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- الأسطر 19-27: تحتوي الواجهة [InterfaceResponse] على طريقة واحدة هي [send] لإرسال الرد إلى العميل؛
- الأسطر 11-17: معنى المعلمات المختلفة للطريقة [send]؛
- الأسطر 23-25: المعلمات [$statusCode, $content, $headers] موجودة في النتيجة القياسية لوحدات التحكم الثانوية للتطبيق. ومع ذلك، قد تحتاج الاستجابة إلى معلومات أخرى. ولذلك، يتم تزويدها بالمعلمات الثلاثة الأولى (الأسطر 20-22) التي تتيح لها الوصول إلى جميع المعلومات المتعلقة بالطلب والجلسة والتكوين؛
- السطر 26: تحتاج الاستجابة إلى المعلمة [Logger] لأنها ستقوم بتسجيل الاستجابة المرسلة إلى العميل؛
تقوم الفئة [JsonResponse] بتنفيذ واجهة [InterfaceResponse] بالطريقة التالية:
<?php
namespace Application;
// تبعيات Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// الطلب $request: طلب قيد المعالجة
// الجلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الاستجابة
// مسجل $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// تحضير مُسلسل Symfony
$serializer = new Serializer(
[
// ضروري لتسلسل الكائنات
new ObjectNormalizer()],
// المشفّر jSON
// للخيارات، استخدم OU بين الخيارات المختلفة
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// التسلسل jSON
$json = $serializer->serialize($content, 'json');
// رؤوس
$headers = array_merge($headers, ["content-type" => "application/json"]);
// إرسال الرد
parent::sendResponse($statusCode, $json, $headers);
// السجل
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
تعليقات
- السطر 13: تنفذ الفئة الواجهة [InterfaceResponse]؛
- السطر 13: الفئة تمتد من الفئة [ParentResponse]. جميع أنواع [Response] تمتد من هذه الفئة. وهذه الفئة الأم هي التي ترسل الرد إلى العميل (السطر 46). ولأن هذا الكود كان مشتركًا بين جميع أنواع [Response]، فقد تم تجميعه في فئة أم؛
- الأسطر 33-40: إنشاء مثيل للمسلسل [Symfony] الذي سيقوم بترجمة استجابة الخادم [$content] إلى سلسلة jSON (السطر 42)؛
- الأسطر 34-36: المعلمة الأولى لمنشئ [Serializer] هي مصفوفة. يتم وضع مثيل للفئة [ObjectNormalizer] في هذا المصفوف، وهو ضروري لتسلسل الكائنات. تظهر هذه الحالة في هذا التطبيق مع قائمة من عمليات المحاكاة حيث تمثل كل عملية محاكاة مثيلًا للفئة [Simulation]؛
- السطر 39: المعلمة الثانية لمُنشئ [Serializer] هي أيضًا مصفوفة: حيث يتم وضع جميع المُشفِّرات المستخدمة في عملية التسلسل (XML، jSON، CSV...)؛
- السطر 39: لن يكون هناك سوى مشفر واحد هنا، من النوع [JsonEncoder]. كان من الممكن أن يكون المنشئ بدون معلمات كافياً. هنا، قمنا بتمرير معلمة [JsonEncode] إلى المنشئ، فقط لتمرير خيارات الترميز jSON؛
- السطر 39: المعلمة الخاصة بمُنشئ [JsonEncode] هي مصفوفة من الخيارات. هنا نستخدم الخيار [JSON_UNESCAPED_UNICODE] لطلب أن يتم عرض أحرف UTF-8 من السلسلة jSON بشكل أصلي وليس "مهربًا"؛
- السطر 42: يتم تسلسل نص الرد HTTP إلى jSON بفضل أداة التسلسل السابقة؛
- السطر 44: يتم إضافة الرأس HTTP الذي يُعلم العميل بأنه سيتم إرسال jSON إليه؛
- السطر 46: يُطلب من الفئة الأم إرسال الرد إلى العميل؛
- الأسطر 48-50: يتم تسجيل الرد jSON؛
فيما يلي كود الفئة الأم [ParentResponse]:
<?php
namespace Application;
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: رمز حالة الرد HTTP
// سلسلة $content: نص الاستجابة المراد إرسالها
// حسب الحالة، قد تكون سلسلة jSON أو XML أو HTML
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الرد
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// إعداد نص الرد من الخادم
$response = new Response();
$response->setCharset("utf-8");
// رمز الحالة
$response->setStatusCode($statusCode);
// رؤوس الرسالة
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// إرسال الرد
$response->setContent($content);
$response->send();
}
}
تعليقات
- الأسطر 10-13: معنى المعلمات الثلاثة للطريقة [send]؛
- السطر 17: تجدر الإشارة إلى أن نص الرد من النوع [string] وبالتالي جاهز للإرسال (السطر 30)؛
- السطر 22: ستحتوي الاستجابة على أحرف UTF-8؛
- السطر 24: رمز حالة الرد HTTP؛
- الأسطر 26-28: إضافة الرؤوس HTTP المحددة بواسطة الرمز المتصل؛
- السطران 30-31: إرسال الرد إلى العميل؛
لقد قمنا بتفصيل دورة الرد jSON بالكامل. ولن نعود إلى هذا الموضوع لاحقًا. يكفي فقط تذكر توقيع واجهة [InterfaceResponse]:
interface InterfaceResponse {
// الطلب $request: يجري معالجة الطلب
// الجلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الرد
// مسجل $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
يجب أن يلتزم وحدة التحكم الرئيسية [main.php] بهذه التوقيع في كل مرة يطلب فيها إرسال الرد إلى العميل.
23.9.5. اختبارات [Postman] – 2
نقوم بتعديل الملف [config.json] بالطريقة التالية:

- في [1]، نشير إلى أن ملف السجلات هو [Logs]، وهو مجلد [2]. لذا، من المفترض أن يفشل إنشاء الملف [Logs]؛
نقوم بإنشاء طلب جديد [Postman] [3]، يُسمى [erreur-133]:

- [2-4]: نحدد نفس الطلب كما في الاختبارين السابقين؛
- [5-7]: نحصل بالفعل على الاستجابة المتوقعة jSON؛
23.9.6. [main.php] – 3
لنواصل دراسة وحدة التحكم الرئيسية [main.php]:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// إدارة الأخطاء بواسطة PHP
…
// إنشاء ملف السجلات
…
// السجل الأول
$logger->write("\n---nouvelle requête\n");
// الاستعلام الحالي
$request = Request::createFromGlobals();
// الجلسة
$session = new Session();
$session->start();
// قائمة الأخطاء
$erreurs = [];
$erreur = FALSE;
// تتم معالجة الإجراء المطلوب
if (!$request->query->has("action")) {
$erreurs[] = "paramètre [action] manquant";
$erreur = TRUE;
$état = 101;
$action = "";
} else {
// يتم حفظ الإجراء
$action = strtolower($request->query->get("action"));
}
// يتم تسجيل الإجراء
$logger->write("action [$action] demandée\n");
// هل الإجراء موجود؟
if (!$erreur && !array_key_exists($action, $config["actions"])) {
$erreurs[] = "action [$action] invalide";
$erreur = TRUE;
$état = 102;
}
// يجب معرفة نوع الجلسة قبل تنفيذ بعض الإجراءات
if (!$erreur && !$session->has("type") && $action !== "init-session") {
$erreurs[] = "pas de session en cours. Commencer par action [init-session]";
$erreur = TRUE;
$état = 103;
}
// بالنسبة لبعض الإجراءات، يجب أن تكون مصادقًا
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
$erreurs[] = "action demandée par utilisateur non authentifié";
$erreur = TRUE;
$état = 104;
}
// أخطاء؟
if ($erreurs) {
// يتم إعداد الرد دون إرساله
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// يتم تنفيذ الإجراء باستخدام وحدة التحكم الخاصة به
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- يتم إرسال الرد
// حالة الخطأ الفادح HTTP_INTERNAL_SERVER_ERROR
// يتم إرسال بريد إلكتروني إلى المسؤول إن أمكن
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// يعتمد الرد على نوع الجلسة
if ($session->has("type")) {
// يوجد نوع الجلسة في الجلسة
$type = $session->get("type");
} else {
// إذا لم يكن هناك نوع في الجلسة، فسيكون الرد افتراضيًا على النحو التالي: jSON
$type = "json";
}
// نضيف المفاتيح [action, état] إلى استجابة وحدة التحكم
$content = ["action" => $action, "état" => $état] + $content;
// يتم إنشاء مثيل للكائن [Response] المكلف بإرسال الاستجابة إلى العميل
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// تم إرسال الرد - يتم تحرير الموارد
$logger->close();
exit;
تعليقات
- بمجرد إجراء الفحوصات الأولية والتأكد من أنه يمكنه العمل، يركز وحدة التحكم الرئيسية على الإجراء المطلوب منها: يجب أن يستوفي هذا الإجراء شروطًا معينة؛
- السطر 21: يتم تسجيل وجود طلب جديد. لم يكن بالإمكان القيام بذلك من قبل لأننا لم نكن متأكدين من وجود ملف سجلات صالح؛
- السطر 23: يتم تغليف جميع معلومات طلب العميل في كائن Symfony [Request]؛
- السطر 26: نبدأ جلسة جديدة حيث نسترد الجلسة الحالية إن وجدت؛
- السطر 27: يتم تنشيط الجلسة؛
- السطر 29: مصفوفة لرسائل الخطأ؛
- السطر 30: قيمة منطقية تُخبرنا خلال الاختبارات ما إذا كنا قد واجهنا خطأً أم لا؛
- السطر 32: يجب أن يكون المعلمة [action] جزءًا من المعلمة URL في صيغة [main.php?action=uneAction]. وبالتالي، فإن المعلمة [action] تشكل جزءًا من المعلمات [$request→query]؛
- الأسطر 33-36: حالة عدم وجود المعلمة [action] في URL. يتم تسجيل الخطأ وتعيين حالة [101] له؛
- السطر 39: إذا كان المعامل [action] موجودًا في URL، يتم تخزينه؛
- السطر 42: يتم تسجيل نوع الإجراء؛
- الأسطر 45-49: إذا كان المعلمة [action] موجودة، فيجب أن تكون صالحة. يتم تعريف جميع الإجراءات المسموح بها في الجدول الترابطي [$config["actions"]]؛
- الأسطر 46-48: إذا كانت العملية غير صالحة، يتم تسجيل الخطأ وتعيين الحالة [102] لها؛
- الأسطر 52-56: لدينا إجراء صالح. ولا يزال يتعين عليه استيفاء شروط أخرى. يقدم تطبيق الويب ثلاثة أنواع من الاستجابات (jSON، XML، HTML). يتم تحديد هذا النوع من خلال الإجراء [init-session]. يضع هذا الإجراء نوع الجلسة في المفتاح [type]؛
- السطر 52: باستثناء الإجراء [init-session]، يجب أن يتم تنفيذ أي إجراء آخر باستخدام المفتاح [type] في الجلسة؛
- الأسطر 53-55: إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ وتعيين الحالة [103] له؛
- الأسطر 58-63: باستثناء الإجراءين [init-session] و [authentifier-utilisateur]، يجب أن تتم جميع الإجراءات الأخرى بعد المصادقة. ويتم المصادقة باستخدام الإجراء [authentifier-utilisateur]، الذي يقوم، في حالة نجاح المصادقة، بإدراج مفتاح [user] في الجلسة؛
- السطر 59: إذا لم تكن العملية هي [init-session] أو [authentifier-utilisateur]، ولم يكن المفتاح [user] موجودًا في الجلسة، فسيحدث خطأ؛
- الأسطر 60-62: يتم تسجيل الخطأ وتعيين الحالة [104] له؛
- الأسطر 66-71: يتم التحقق مما إذا كان المصفوف [$erreurs] غير فارغ. إذا كان الأمر كذلك، فهذا يعني أن الإجراء المطلوب أو سياق تنفيذه غير صحيح؛
- الأسطر 68-70: يتم إعداد الرد المراد إرساله إلى العميل، ولكن لا يتم إرساله بعد؛
- السطر 68: رمز الحالة HTTP؛
- السطر 69: نص الرد؛
- السطر 70: الرؤوس المراد إضافتها إلى الرد، ولا توجد هنا؛
- السطر 73: لدينا إجراء صالح. سنطلب من وحدة التحكم (الثانوية) الخاصة به معالجته؛
- السطر 74: نبني اسم فئة وحدة التحكم المطلوب تنفيذها. [__NAMESPACE__] هو مساحة الأسماء التي نوجد فيها، وهنا [Application] (السطر 7)؛
- توجد أسماء فئات وحدة التحكم الثانوية في الملف [config.json]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
لكل إجراء وحدة تحكم ثانوية مقابلة. إذا كان الإجراء هو [authentifier-utilisateur]، فإن المتغير [$controller] في السطر 74 سيأخذ القيمة [Application/AuthentifierUtilisateurController]؛
- السطر 75: يتم تسجيل اسم وحدة التحكم الثانوية، للتحقق أثناء التطوير؛
- السطر 76: يتم تنفيذ وحدة التحكم الثانوية. سنعود إلى موضوع وحدات التحكم الثانوية لاحقًا؛
- السطر 76: تُرجع جميع وحدات التحكم الثانوية نفس النوع من النتائج، وهو مصفوفة:
- العنصر الأول في المصفوفة [$statusCode] هو رمز الحالة HTTP للاستجابة المطلوب إرسالها؛
- العنصر الثاني [$état] هو حالة التطبيق بعد تنفيذ وحدة التحكم؛
- العنصر الثالث [$content] هو مصفوفة مرتبطة بالمفتاح الفريد [réponse] الذي يمثل نص الرد المراد إرساله إلى العميل؛
- العنصر الرابع [$headers] هو مصفوفة من الرؤوس HTTP المراد إضافتها إلى الرد المرسل إلى العميل؛
- السطر 79: نصل هنا:
- إما بسبب حدوث خطأ (الأسطر 68-70)؛
- أو بعد تنفيذ وحدة تحكم (الأسطر 72-76)؛
- في كلتا الحالتين، تكون العناصر [$statusCode, $état, $content, $headers] اللازمة لإعداد الرد للعميل معروفة؛
- الأسطر 82-87: تعالج الحالة الخاصة لرمز الحالة [500 Internal Server Error]. إذا قام وحدة التحكم بوضع رمز الحالة هذا، فهذا يعني أن التطبيق لا يمكنه العمل. وهذا هو الحال، على سبيل المثال، عند حساب الضريبة إذا لم يتم تشغيل وحدة التحكم SGBD المستخدمة أو إذا توقفت عن الاستجابة. عندئذ يتم إرسال بريد إلكتروني إلى مسؤول التطبيق لإخطاره بذلك. لن نعلق بشكل خاص على هذا الرمز. وقد تم بالفعل عرض استخدام الفئة [SendAdminMail] (الفقرة المرتبطة)؛
- الأسطر 89-95: يتم تحديد نوع [jSON, XML, HTML] لتطبيق الويب. إذا تم تنفيذ الإجراء [init-session] بنجاح، فإن هذا النوع يكون موجودًا في الجلسة المرتبطة بالمفتاح [type] (السطر 91). وإذا لم يكن الأمر كذلك، يتم تعيين نوع للاستجابة بشكل تعسفي، وهو النوع jSON (السطر 94)؛
- السطر 97: [$content] هو مصفوفة ذات مفتاح واحد [réponse] وقيمة واحدة، وهي نص الاستجابة المراد إرسالها إلى العميل. نضيف إليها المفاتيح [action] و [état]. ستسمح المفتاح [action] بمتابعة سجلات الملف [logs.txt] بشكل أفضل. وسيكون للمفتاح [état] دوران:
- فهي ستسمح للعملاء jSON و XML بمعرفة الحالة التي وصل إليها تطبيق الويب بعد تنفيذ الإجراء؛
- في حالة وجود استجابة HTML، سيسمح باختيار العرض HTML الذي يجب إرساله إلى متصفح العميل؛
- السطر 99: يتم اختيار نوع الفئة [Response] التي سيتم تنفيذها لإرسال الرد إلى العميل؛
لقد قدمنا بالفعل الفئة [JsonResponse] في الفقرة "الرابط". وهي تنفذ الواجهة [InterfaceResponse] وتوسع الفئة [ParentResponse]. وينطبق الأمر نفسه على الفئتين الأخريين [XmlResponse] و [HtmlResponse].
يتم تجميع الإجابات في المجلد [Responses]:

تُنفِّذ جميع هذه الفئات الواجهة [InterfaceResponse] المُقدَّمة أيضًا في الفقرة الرابط:
<?php
namespace Application;
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// الطلب $request: الطلب قيد المعالجة
// جلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// array $headers: الرؤوس HTTP المراد إضافتها إلى الرد
// Logger $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
تحتوي هذه الواجهة على طريقة واحدة هي [send]، وهي مسؤولة عن إرسال الاستجابة إلى العميل. تحتوي هذه الطريقة على المعلمات السبعة الموضحة في الأسطر 11-17. تقع جميع الفئات والواجهات الموجودة في المجلد [Responses] ضمن مساحة الأسماء [Application] (السطر 3).
لنعد إلى كود [main.php]:
…
// يتم إضافة المفاتيح [action, état] إلى استجابة وحدة التحكم
$content = ["action" => $action, "état" => $état] + $content;
// يتم إنشاء مثيل للكائن [Response] المكلف بإرسال الرد إلى العميل
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// تم إرسال الرد - يتم تحرير الموارد
$logger->close();
exit;
- السطر 5: يتم إنشاء مثيل للفئة [Response] التي تناسب نوع التطبيق. يتم تعريف هذه الفئات في الملف [config.json] على النحو التالي:
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
- السطر 5: يُسبق اسم الفئة بمساحة الاسم الخاصة بها؛
- السطر 6: يتم إنشاء مثيل للفئة [Response] ويتم استدعاء طريقتها [send] مع المعلمات السبعة التي تتوقعها. هذه المعلمات هي معلمات واجهة [InterfaceResponse] التي تنفذها جميع فئات الاستجابة. يؤدي ذلك إلى إرسال الاستجابة إلى العميل؛
- السطر 9: يتم إغلاق ملف السجلات؛
- السطر 10: انتهى وحدة التحكم الرئيسية من عملها؛
23.9.7. اختبارات [Postman] – 3
سنقوم باختبار حالات خطأ متنوعة للمعلمة [action] التابعة لـ URL.

- في [1]:
- [erreur-101]: حالة المعلمة [action] المفقودة في URL؛
- [erreur-102]: حالة وجود المعلمة [action] في URL ولكنها غير معترف بها؛
- [erreur-103]: حالة المعلمة [action] الموجودة في URL، تم التعرف عليها ولكن دون تحديد نوع الاستجابة المتوقعة [json, xml, html]؛
يتم تنفيذ كل استعلام. ونعرض النتائج التي تم الحصول عليها مباشرةً:
أعلاه:
- في [2-4]، طلب بدون المعلمة [action] في URL [4]؛
- في [5-7]، النتيجة هي jSON؛

أعلاه:
- في [5-9]، طلب يحتوي على معلمة [action] غير صالحة؛
- في [10-13]، الرد jSON؛

أعلاه:
- في [14-19]، إجراء معترف به ولكن لم يتم تحديد النوع (json، xml، html) بعد؛
- في [20-23]، الرد jSON من الخادم؛
23.10. وحدات التحكم الثانوية
يتم تنفيذ كل إجراء بواسطة أحد وحدات التحكم في المجلد [Controllers]:


في البنية العامة للتطبيق المذكور أعلاه، تعمل وحدات التحكم الثانوية وفقًا لـ [2a].
تقوم كل وحدة تحكم بتنفيذ الواجهة التالية: [InterfaceController]:
<?php
namespace Application;
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceController {
// $config هي تهيئة التطبيق
// معالجة طلب Request
// يستخدم الجلسة (Session) ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos=NULL): array;
}
تعليقات
- يتم تنفيذ جميع وحدات التحكم الثانوية عبر الطريقة [execute] في السطر 17. يتم تمرير المعلومات المعروفة عن وحدة التحكم الرئيسية إلى هذه الطريقة:
- السطر 18: [array $config] التي تحتوي على تكوين التطبيق؛
- السطر 19: [Request $request]، وهي الطلب HTTP قيد المعالجة؛
- السطر 20: [Session $session]، وهي الجلسة الحالية لتطبيق الويب؛
- السطر 21: [array $infos=NULL]، وهو مصفوفة إضافية من المعلومات الموجهة إلى وحدة التحكم في حالة عدم كفاية المعلمات الثلاثة الأولى للطريقة. في هذا التطبيق، لم يتم استخدام هذا المعلم مطلقًا. وقد أُدرج كإجراء احترازي؛
- السطر 21: الطريقة [execute] تُرجع المصفوفة [$statusCode, $état, $content, $headers]
- [int $statusCode]: رمز حالة الاستجابة HTTP؛
- [int $état]: الحالة التي يكون عليها التطبيق عند انتهاء التنفيذ؛
- [array $content]: مصفوفة ترابطية [réponse=>résultat] حيث [résultat] من أي نوع: هذه هي النتيجة التي أنتجها وحدة التحكم والتي سيتم إرسالها إلى العميل، بمجرد تسلسل هذه النتيجة في شكل سلسلة أحرف؛
- [array $headers]: قائمة الرؤوس HTTP التي سيتم تضمينها في استجابة الخادم HTTP؛
يتم استدعاء كل وحدة تحكم ثانوية بواسطة الكود التالي من وحدة التحكم الرئيسية:
// يتم تنفيذ الإجراء باستخدام وحدة التحكم الخاصة به
$controller = __NAMESPACE__ . $config["actions"][$action];
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
في السطر 3، نلاحظ أن المعلمة الرابعة [array $infos=NULL] للطريقة [execute] غير مستخدمة.
23.11. الإجراءات
نستعرض الآن الإجراءات المختلفة الممكنة لخدمة الويب:
الإجراء | الدور | سياق التنفيذ |
init-session | تُستخدم لتحديد نوع (json، xml، html) الردود المطلوبة | الطلب GET main.php?action=init-session&type=x يمكن إرسالها في أي وقت |
authentifier-utilisateur | يسمح أو يمنع المستخدم من تسجيل الدخول | الطلب POST main.php?action=authentifier-utilisateur يجب أن تحتوي الطلب على معلمتين مرسلتين [user, password] لا يمكن إرسالها إلا إذا كان نوع الجلسة (json، xml، html) معروفًا |
حساب-الضريبة | يقوم بمحاكاة حساب الضريبة | الطلب POST main.php?action=calculer-impot يجب أن تحتوي الطلب على ثلاثة معلمات مرسلة عبر البوست [marié, enfants, salaire] لا يمكن إرسالها إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وكان المستخدم قد تمت مصادقته |
lister-simulations | طلب عرض قائمة المحاكاة التي تم إجراؤها منذ بداية الجلسة | الطلب GET main.php?action=lister-simulations لا يقبل الطلب أي معلمات أخرى لا يمكن إرسالها إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وكان المستخدم قد تم توثيقه |
حذف-محاكاة | يحذف محاكاة من قائمة المحاكاة | الطلب GET main.php?action=lister-simulations&numéro=x لا يقبل الطلب أي معلمات أخرى لا يمكن إرسالها إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وكان المستخدم قد تم توثيقه |
fin-session | ينهي جلسة المحاكاة. | من الناحية الفنية، يتم حذف الجلسة القديمة على الويب وإنشاء جلسة جديدة لا يمكن إصدارها إلا إذا كان نوع الجلسة (json، xml، html) معروفًا وكان المستخدم قد تم توثيقه |
تعمل جميع وحدات التحكم الثانوية بنفس الطريقة:
- تتحقق من معلماتها. وتوجد هذه المعلمات في الكائن [Request→query] بالنسبة للمعلمات الموجودة في URL وفي الكائن [Request→request] بالنسبة لتلك التي تم إرسالها (الطلب POST)؛
- يشبه «الكونترولر» دالة أو طريقة تتحقق من صحة معلماتها. لكن الأمر أكثر تعقيدًا بعض الشيء بالنسبة للكونترولر:
- قد تكون المعلمات المتوقعة غائبة؛
- قد تكون جميع المعلمات المتوقعة سلاسل أحرف، في حين أن الدالة يمكنها تحديد نوع معلماتها. إذا كانت المعلمة المتوقعة عددًا، فيجب التحقق من أن سلسلة المعلمة تمثل عددًا بالفعل؛
- وبمجرد التحقق من وجود المعلمات المتوقعة وصحة صياغتها، يجب التحقق من صحتها في سياق التنفيذ الحالي. ويوجد هذا السياق في الجلسة. يُعد مثال المصادقة مثالاً على سياق التنفيذ. لا يجب معالجة بعض الإجراءات إلا بعد مصادقة العميل. عادةً، تشير مفتاح في الجلسة إلى ما إذا كانت هذه المصادقة قد تمت أم لا؛
- وبمجرد إتمام عمليات التحقق السابقة، يمكن للوحدة الثانوية أن تبدأ العمل. وتعتبر عملية التحقق من المعلمات هذه بالغة الأهمية. فلا يمكننا قبول أن يرسل لنا العميل أي شيء في أي لحظة من دورة حياة التطبيق. بل يجب أن نتحكم بشكل كامل في دورة حياة التطبيق؛
- بمجرد الانتهاء من مهمته، يقوم وحدة التحكم الثانوية بإرجاع المصفوفة [$statusCode, $état, $content, $headers] التي تنتظرها وحدة التحكم الرئيسية التي استدعتها؛
سنستعرض الآن الوحدات المتحكمة المختلفة، أو ما يعادلها من الإجراءات المختلفة التي تحدد إيقاع عمل تطبيق الويب.
23.11.1. الإجراء [init-session]
تتم معالجة الإجراء [init-session] بواسطة وحدة التحكم [InitSessionController] التالية:
<?php
namespace Application;
// التبعيات الخاصة بـ Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
class InitSessionController implements InterfaceController {
// $config هي تكوين التطبيق
// معالجة طلب Request
// يستخدم الجلسة Session ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// يجب أن يكون لدينا GET ومعلمة واحدة بخلاف [action]
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
if ($erreur) {
$état = 701;
$message = "méthode GET exigée avec paramètres [action, type] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// نسترد المعلمات من GET
$erreur = FALSE;
// النوع
if (!$request->query->has("type")) {
$erreur = TRUE;
$état = 702;
$message = "paramètre [type] manquant";
} else {
$type = strtolower($request->query->get("type"));
}
// التحقق من النوع
if (!$erreur && !array_key_exists($type, $config["types"])) {
$erreur = TRUE;
$état = 703;
$message = "paramètre type [$type] invalide";
}
// خطأ؟
if ($erreur) {
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// يتم تعيين نوع الجلسة في الجلسة
$session->set("type", $type);
// رسالة نجاح
$message = "session démarrée avec type [$type]";
$état = 700;
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
تعليقات
- ننتظر طلب [GET main.php?action=init-session&type=xxx]
- السطور 25-26: يتم التحقق من أن الطلب هو طلب GET مع معلمتين في URL؛
- الأسطر 27-31: إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ وإرسال نتيجة [$statusCode, $état, $content, $headers] إلى وحدة التحكم الرئيسية؛
- الأسطر 35-39: يتم التحقق من وجود المعلمة [type] في URL. إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ؛
- السطر 40: يتم تسجيل نوع الجلسة؛
- الأسطر 43-47: يتم التحقق من أن نوع الجلسة هو أحد المصطلحات (json، xml، html). وإذا لم يكن الأمر كذلك، يتم تسجيل الخطأ؛
- الأسطر 49-51: إذا كان هناك خطأ، نرسل نتيجة [$statusCode, $état, $content, $headers] إلى وحدة التحكم الرئيسية؛
- السطر 53: يتم وضع نوع الجلسة في جلسة تطبيق الويب؛
- الأسطر 55-57: انتهى وحدة التحكم من عملها. يتم إرسال نتيجة النجاح [$statusCode, $état, $content, $headers] إلى وحدة التحكم الرئيسية؛
لنتذكر ما يفعله وحدة التحكم الرئيسية استجابةً لردود وحدات التحكم الثانوية:
// أخطاء؟
if ($erreurs) {
// يتم إعداد الرد دون إرساله
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// يتم تنفيذ الإجراء باستخدام وحدة التحكم الخاصة به
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- إرسال الرد
// حالة الخطأ الفادح HTTP_INTERNAL_SERVER_ERROR
// يتم إرسال بريد إلكتروني إلى المسؤول إن أمكن
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// يعتمد الرد على نوع الجلسة
if ($session->has("type")) {
// يوجد نوع الجلسة في الجلسة
$type = $session->get("type");
} else {
// إذا لم يكن هناك نوع في الجلسة، فسيكون الرد افتراضيًا على النحو التالي: jSON
$type = "json";
}
// نضيف المفاتيح [action, état] إلى استجابة وحدة التحكم
$content = ["action" => $action, "état" => $état] + $content;
// يتم إنشاء مثيل للكائن [Response] المكلف بإرسال الرد إلى العميل
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// تم إرسال الرد - يتم تحرير الموارد
$logger->close();
exit;
- السطر 12: يستلم وحدة التحكم الرئيسية النتيجة من وحدة التحكم الثانوية؛
- السطران 35-36: بعد إجراء بعض التحقق، يرسل الرد عن طريق إنشاء مثيل لإحدى الفئات [JsonResponse, XmlResponse, HtmlResponse] وفقًا لنوع (json، xml، html) الجلسة الحالية؛
فيما يلي، سنجري اختبارات [Postman] في إطار جلسة محاكاة باستخدام النوع [json]. وقد تم عرض طريقة عمل الفئة [JsonResponse] في الفقرة المرتبطة.
23.11.2. اختبارات [Postman]

أعلاه:
- في [2]، ثلاثة اختبارات جديدة؛
- في [3-7]، الإجراء [init-session] مع المعلمة [type] المفقودة؛
- في [8-11]، استجابة الخادم jSON؛

أعلاه:
- في [1-7]، الإجراء [init-session] مع معلمة [type] غير صحيحة؛
- في [8-11]، الرد jSON من الخادم؛

أعلاه:
- في [1-8]، الإجراء [init-session] من النوع jSON؛
- في [9-12]، الاستجابة jSON من الخادم؛
23.11.3. الإجراء [authentifier-utilisateur]
يتم تنفيذ الإجراء [authentifier-utilisateur] بواسطة وحدة التحكم [AuthentifierUtilisateurController] التالية:
<?php
namespace Application;
// التبعيات في Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class AuthentifierUtilisateurController implements InterfaceController {
// $config هي تهيئة التطبيق
// معالجة طلب Request
// تستخدم الجلسة Session ويمكنها تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// يجب أن يكون لدينا POST ومعلمة واحدة GET
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
$état = 201;
$message = "méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user,password]";
// يتم إرجاع النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// نسترد المعلمات من POST
$erreurs = [];
// المستخدم
$état = 210;
if (!$request->request->has("user")) {
$état += 2;
$erreurs[] = "paramètre [user] manquant";
} else {
$user = $request->request->get("user");
}
// كلمة المرور
if (!$request->request->has("password")) {
$état += 4;
$erreurs[] = "paramètre [password] manquant";
} else {
$password = trim($request->request->get("password"));
}
// خطأ؟
if ($erreurs) {
// إرسال النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// التحقق من بيانات تعريف المستخدم
// هل المستخدم موجود؟
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
$i++;
}
// تم العثور عليه؟
if (!$trouvé) {
// رسالة خطأ
$message = "Echec de l'authentification [$user, $password]";
$état = 221;
// إرسال النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
} else {
// تسجيل المصادقة على المستخدم في الجلسة
$session->set("user", TRUE);
// رسالة نجاح
$message = "Authentification réussie [$user, $password]";
$état = 200;
// إرجاع النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
}
تعليقات
- ننتظر طلبًا [POST main.php?action=authentifier-utilisateur] مع معلمتين مرسلتين [user, password]؛
- السطران 24-25: يتم التحقق من وجود طلب POST مع معلمة واحدة في URL؛
- الأسطر 26-31: في حالة وجود خطأ، يتم تسجيله وإرجاع نتيجة [$statusCode, $état, $content, $headers] إلى وحدة التحكم الرئيسية؛
- الأسطر 36-39: يتم التحقق من وجود المعلمة [user] في القيم المرسلة. إذا لم تكن موجودة، يتم تسجيل الخطأ؛
- الأسطر 43-45: يتم التحقق من وجود المعلمة [password] في القيم المرسلة. إذا لم تكن موجودة، يتم تسجيل الخطأ؛
- الأسطر 50-53: إذا كانت إحدى القيم المنشورة مفقودة، يتم إرجاع نتيجة [$statusCode, $état, $content, $headers] إلى وحدة التحكم الرئيسية؛
- الأسطر 56-62: يتم التحقق من وجود الزوج [$user,$password] الذي تم استرداده في الجدول [$config[‘users’]] في ملف التكوين؛
- الأسطر 64-69: إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ. يتم تغيير رمز الحالة HTTP إلى [Response::HTTP_UNAUTHORIZED] ويتم إرجاع النتيجة [$statusCode, $état, $content, $headers] إلى وحدة التحكم الرئيسية؛
- السطر 72: نجحت المصادقة. يتم تسجيل ذلك في الجلسة عن طريق وضع المفتاح [user] فيها. إن وجود هذا المفتاح هو ما يشير إلى نجاح المصادقة؛
- الأسطر 73-77: يتم إرجاع نتيجة [$statusCode, $état, $content, $headers] للنجاح إلى وحدة التحكم الرئيسية؛
23.11.4. اختبارات [Postman]
نقوم بإجراء اختبارات [Postman] لوحدة التحكم [AuthentifierUtilisateurController] في الوضع jSON؛

أعلاه:
- في [1-6]، الإجراء [authentifier-utilisateur] مع GET [2]، في حين أنه يلزم وجود POST؛
- في [7-10]، الرد jSON من الخادم؛
لنستبدل GET بـ POST و [2] دون إدراج أي معلمات في نص الرد [7]:

أعلاه:
- في [1-7]، POST بدون معلمات مرسلة في [7]؛
- في [8-11]، الرد jSON من الخادم؛
لنضف الآن معلمة [password] في نص (body) الطلب [4]:

أعلاه:
- في [1-6]، طلب POST و [2] مع معلمة [password] تم إرسالها إلى [4-6]. يجب إضافة المعلمات المرسلة إلى نص (body) الطلب [4]. هناك عدة طرق لإرسال القيم إلى الخادم. نختار الطريقة [x-www-form-urlencoded] [5]؛
- في [8-10]، استجابة الخادم jSON؛
الآن لنحدد المعلمة [user] بدون المعلمة [password]:

أعلاه:
- في [1-7]، طلب POST بدون المعلمة [password] [4-7]؛
- في [8-11]، الرد jSON من الخادم؛
الآن لنحدد المعلمتين المرسلتين [user, password] ولكن بقيم تؤدي إلى فشل المصادقة:

أعلاه:
- في [1-9]، طلب POST مع معلمات مرسلة [user, password] غير صحيحة؛
- في [10-13]، الرد jSON من الخادم. لاحظ رمز الحالة [401 Unauthorized] [10] في الرد؛
الآن طلب POST بمعرفات صالحة:

أعلاه:
- في [1-9]، الطلب POST [2] مع معرفات صالحة [6-9]؛
- في [10-13]، الرد jSON من الخادم. تجدر الإشارة إلى رمز الحالة HTTP [200 OK] في [10]؛
23.11.5. الإجراء [calculer-impot]
يتم معالجة الإجراء [calculer-impot] بواسطة وحدة التحكم [CalculerImpotController] التالية:
<?php
namespace Application;
// التبعيات في Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// اسم مستعار للطبقة [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class CalculerImpotController implements InterfaceController {
// $config هي تهيئة التطبيق
// معالجة طلب Request
// يستخدم الجلسة Session ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// يجب أن يكون هناك معلمة واحدة GET وثلاث معلمات POST
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
// يتم تسجيل الخطأ
$message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
$état = 301;
// إرجاع النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// يتم استرداد معلمات POST
$erreurs = [];
$état = 310;
// الحالة الاجتماعية
if (!$request->request->has("marié")) {
$état += 2;
$erreurs[] = "paramètre [marié] manquant";
} else {
$marié = trim(strtolower($request->request->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
if ($erreur) {
$état += 4;
$erreurs[] = "valeur [$marié] invalide pour le paramètre [marié]";
}
}
// استرداد عدد الأبناء
if (!$request->request->has("enfants")) {
$état += 8;
$erreurs[] = "paramètre [enfants] manquant";
} else {
$enfants = trim($request->request->get("enfants"));
$erreur = !preg_match("/^\d+$/", $enfants);
if ($erreur) {
$état += 9;
$erreurs[] = "valeur [$enfants] invalide pour le paramètre [enfants]";
}
}
// استرداد الراتب السنوي
if (!$request->request->has("salaire")) {
$erreurs[] = "paramètre [salaire] manquant";
$état += 16;
} else {
$salaire = trim($request->request->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
if ($erreur) {
$état += 17;
$erreurs[] = "valeur [$salaire] invalide pour le paramètre [salaire]";
}
}
// خطأ؟
if ($erreurs) {
// إرجاع النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// لدينا كل ما نحتاجه للعمل
// Redis
\Predis\Autoloader::register();
try {
// العميل [predis]
$redis = new \Predis\Client();
// نقوم بالاتصال بالخادم للتحقق من وجوده
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// لم تسر الأمور على ما يرام
// إرجاع النتيجة مع خطأ إلى وحدة التحكم الرئيسية
$état = 350;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// لدينا معلمات صالحة
// إنشاء الطبقة [dao]
if (!$redis->get("taxAdminData")) {
try {
// نقوم بالبحث عن البيانات الضريبية في قاعدة البيانات
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// يتم تخزين البيانات المسترجعة في Redis
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// حدث خطأ
// إرجاع النتيجة مع خطأ إلى وحدة التحكم الرئيسية
$état = 340;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// يتم أخذ البيانات الضريبية من ذاكرة النطاق [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// إنشاء مثيل للطبقة [dao]
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
}
// إنشاء الطبقة [métier]
$métier = new ServerMetier($dao);
// لدينا كل ما نحتاجه للعمل - حساب الضريبة
$résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// نضيف إلى الجلسة المحاكاة التي تم إجراؤها للتو
$simulation = new Simulation();
$résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
$simulation->setFromArrayOfAttributes($résultat);
// هل توجد قائمة بالمحاكاة في الجلسة؟
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// إضافة المحاكاة إلى قائمة المحاكاة
$simulations[] = $simulation;
// إعادة المحاكاة إلى الجلسة
$session->set("simulations", $simulations);
// إرجاع النتيجة إلى وحدة التحكم الرئيسية
$état = 300;
return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
}
}
تعليقات
- الطلب المتوقع هو [POST main.php?action=calculer-impot] مع ثلاثة معلمات مرسلة [marié, enfants, salaire]:
- يجب أن تكون قيمة [marié] موجودة في [oui, non]؛
- يجب أن تكون قيم [enfants, salaire] أعدادًا صحيحة موجبة أو صفرًا؛
- السطران 26-27: يتم التحقق من وجود POST مع معلمة واحدة في URL؛
- الأسطر 28-34: إذا لم يكن الأمر كذلك، يتم إرسال نتيجة خطأ إلى وحدة التحكم الرئيسية؛
- السطر 36: سنقوم بتجميع رسائل الخطأ في الجدول [$erreurs]؛
- الأسطر 39-41: يتم التحقق من وجود المعلمة [marié]. إذا لم تكن موجودة، يتم تسجيل الخطأ؛
- الأسطر 43-49: يتم التحقق من أن قيمة [marié] موجودة في [oui, non]. إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ؛
- الأسطر 51-54: يتم التحقق من وجود المعلمة [enfants]. إذا لم تكن موجودة، يتم تسجيل الخطأ؛
- الأسطر 55-61: يتم التحقق من أن قيمة المعلمة [enfants] هي عدد موجب أو صفر. إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ؛
- الأسطر 63-66: يتم التحقق من وجود المعلمة [salaire]. إذا لم تكن موجودة، يتم تسجيل الخطأ؛
- الأسطر 67-72: يتم التحقق من أن قيمة المعلمة [salaire] هي عدد موجب أو صفر. إذا لم يكن الأمر كذلك، يتم تسجيل الخطأ؛
- الأسطر 75-78: إذا لم يكن المصفوف [$erreurs] فارغًا، فهذا يعني وجود أخطاء. يتم إدراج مصفوفة الأخطاء في الرد وإرجاع النتيجة إلى وحدة التحكم الرئيسية؛
- السطر 80: لدينا معلمات صالحة. يمكننا حساب الضريبة. ولهذا الغرض، يجب إنشاء الطبقتين [dao] و [métier] اللتين تقومان بهذا الحساب؛
- الأسطر 82-94: نقوم بإنشاء عميل [Redis]؛
- الأسطر 88-94: إذا تعذر الاتصال بالخادم [Redis]، يتم إرسال رمز [500 Internal Server Error] إلى العميل؛
- السطر 98: يتم التحقق مما إذا كان الخادم [Redis] يحتوي على المفتاح [taxAdminData]. يمثل هذا المفتاح بيانات إدارة الضرائب. إذا لم يكن المفتاح موجودًا، فيجب البحث عن البيانات الضريبية في قاعدة البيانات؛
- السطر 101: إنشاء الطبقة [dao] عندما يتعين أخذ البيانات الضريبية من قاعدة البيانات. تم وصف الفئة [ServerDaoWithRedis] في الفقرة الرابط؛
- السطر 103: يتم تخزين البيانات المسترجعة من قاعدة البيانات في الذاكرة [Redis] باستخدام المفتاح [taxAdminData]؛
- الأسطر 104-110: إذا فشل البحث في قاعدة البيانات، يتم تسجيل الخطأ الذي أرسلته الطبقة [dao] وإدراجه في النتيجة المرسلة إلى وحدة التحكم الرئيسية؛
- السطر 109: يتم ترميز رسالة الخطأ التي أرسلتها الطبقة [PDO] في [iso-8859-1]. يتم ترميزها في [utf-8]؛
- الأسطر 111-117: إذا كان المفتاح [taxAdminData] موجودًا في ذاكرة [Redis]، فإن البيانات الضريبية تُمرر مباشرةً إلى مُنشئ الطبقة [dao]؛
- السطر 119: يتم إنشاء الطبقة [métier]. وقد تم وصف الفئة [ServerMetier] في الفقرة «الرابط»؛
- الأسطر 124-126: مع حساب مبلغ الضريبة، يتم إنشاء كائن [Simulation]. وتغلف الفئة [Simulation] بيانات المحاكاة وقد تم وصفها في الفقرة «الرابط»؛
- الأسطر 128-132: يجب إضافة المحاكاة التي تم إنشاؤها للتو إلى قائمة المحاكاة التي تم حسابها بالفعل. توجد هذه القائمة في الجلسة ما لم تكن قد أجريت أي محاكاة بعد؛
- الأسطر 133-136: تُضاف المحاكاة إلى قائمة المحاكاة، وتُعاد هذه القائمة إلى الجلسة؛
- الأسطر 137-139: يتم إرجاع النتيجة إلى وحدة التحكم الرئيسية؛
23.11.6. اختبارات [Postman]
نجري اختبارات [Postman] لوحدة التحكم [CalculerImpotController] في الوضع jSON؛

أعلاه:
- في [1-7]، يتم إرسال طلب [GET] بدلاً من [POST]؛
- في [8-11]، الرد jSON من الخادم؛
الآن، دعونا نستخدم طريقة [POST]، مع أو بدون معلمات مرسلة، وكذلك مع معلمات مرسلة غير صالحة:

أعلاه:
- يتم إرسال طلب [POST] [2] مع معلمات مرسلة [6-11] [marié, enfants, salaire] غير صالحة. يمكنك عدم إرسال أحد هذه المعلمات عن طريق إلغاء تحديد المربع الخاص بها في [16]. سيسمح لك ذلك باختبار حالات مختلفة. في لقطة الشاشة أعلاه، المعلمات الثلاثة موجودة وجميعها غير صالحة؛
- في [12-15]، رد الخادم jSON؛
الآن، لنقم بإلغاء تحديد اثنين من المعلمات الثلاثة المرسلة:

أعلاه،
- في [5-8]، يتم إرسال المعلمة [salaire] وحدها، علاوة على أنها غير صالحة؛
- في [9-11]، النتيجة jSON من الخادم؛
الآن لنقم بحساب الضريبة باستخدام معلمات صالحة:

أعلاه:
- في [1118]، طلب بمعلمات صالحة [6-8]؛
- في [12-14]، الرد jSON من الخادم؛
23.11.7. الإجراء [lister-simulations]
يتم معالجة الإجراء [lister-simulations] بواسطة وحدة التحكم الثانوية [ListerSimulationsController] التالية:
<?php
namespace Application;
// التبعيات في Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class ListerSimulationsController {
// $config هي تهيئة التطبيق
// معالجة طلب Request
// يستخدم الجلسة (Session) ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// يجب أن يكون هناك معلمة واحدة فقط GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
$état = 501;
$message = "GET requis, avec l'unique paramètre [action] dans l'URL";
// يتم إرجاع نتيجة بها خطأ إلى وحدة التحكم الرئيسية
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// يتم استرداد قائمة عمليات المحاكاة في الجلسة
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// يتم إرجاع نتيجة ناجحة إلى وحدة التحكم الرئيسية
$état = 500;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
تعليقات
- الطلب [GET main.php?action=lister-simulations]؛
- السطران 24-25: يتم التحقق من وجود طلب GET بمعلمة واحدة؛
- الأسطر 26-31: إذا لم يكن الأمر كذلك، يتم إرجاع نتيجة تحتوي على خطأ إلى وحدة التحكم الرئيسية؛
- الأسطر 33-37: يتم استرداد قائمة عمليات المحاكاة من الجلسة إن وجدت (السطر 36)، وإلا تكون هذه القائمة فارغة (السطر 34)؛
- الأسطر 39-40: يتم إرجاع قائمة عمليات المحاكاة إلى وحدة التحكم الرئيسية؛
23.11.8. اختبارات [Postman]
سنقوم بإنشاء اختبارين، أحدهما للخطأ والآخر للنجاح.

أعلاه:
- في [1-8]، يتم إجراء استعلام [GET] مع وجود معلمة [param1] زائدة في URL [3, 7-8]؛
- في [9-12]، الرد jSON من الخادم؛
الآن لنقم بإجراء استعلام صحيح:

أعلاه:
- إلى [1-5]، وهو طلب صالح؛
ونتيجة الطلب هي كما يلي:

- في [3-6]، الرد jSON من الخادم. قبل هذا الاختبار، تم تنفيذ الاختبار [Postman] [calculer-impot-300] عدة مرات لإنشاء محاكاة في جلسة الويب الخاصة بالخادم؛
23.11.9. الإجراء [supprimer-simulation]
يتم معالجة الإجراء [supprimer-simulation] بواسطة وحدة التحكم الثانوية [SupprimerSessionController] التالية:
<?php
namespace Application;
// التبعيات الخاصة بـ Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class SupprimerSimulationController {
/// $config هي تكوين التطبيق
// معالجة طلب Request
// يستخدم الجلسة (Session) ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// يجب أن يكون هناك معلمتان GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
$état = 600;
if ($erreur) {
$état += 2;
$message = "GET requis, avec les paramètres [action, numéro]";
}
// يجب أن يكون المعامل [numéro] موجودًا
if (!$erreur) {
$état += 4;
$erreur = !$request->query->has("numéro");
if ($erreur) {
$message = "paramètre [numéro] manquant";
}
}
// يجب أن تكون المعلمة [numéro] صالحة
if (!$erreur) {
$état += 8;
$numéro = $request->query->get("numéro");
$erreur = !preg_match("/^\d+$/", $numéro);
if ($erreur) {
$message = "paramètre [$numéro] invalide";
}
}
// يجب أن يكون المعامل [numéro] ضمن النطاق [0,n-1]
// إذا كان n هو عدد عمليات المحاكاة
if (!$erreur) {
$numéro = (int) $numéro;
$erreur = !$session->has("simulations");
if (!$erreur) {
$simulations = $session->get("simulations");
$erreur = $numéro < 0 || $numéro >= count($simulations);
}
if ($erreur) {
$état += 16;
$message = "la simulation n° [$numéro] n'existe pas";
}
}
// خطأ؟
if ($erreur) {
// يتم إرجاع النتيجة إلى وحدة التحكم الرئيسية
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// يتم حذف المحاكاة $numéro
unset($simulations[$numéro]);
$simulations = array_values($simulations);
// إعادة المحاكاة إلى الجلسة
$session->set("simulations", $simulations);
// إرجاع قائمة عمليات المحاكاة إلى العميل
$état = 600;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
تعليقات
- الاستعلام [GET main.php?action=supprimer-simulation&numéro=x]؛
- الأسطر 24-30: نتحقق من وجود استعلام GET مع معلمتين؛
- الأسطر 32-38: نتحقق من وجود المعلمة [numéro] ضمن معلمات URL؛
- الأسطر 40-47: نتحقق من أن قيمة المعلمة [numéro] صحيحة من الناحية النحوية؛
- الأسطر 50-61: يتم التحقق من وجود المحاكاة رقم [numéro]. هناك حالتان للخطأ:
- لا يمكن العثور على قائمة المحاكاة في الجلسة (السطر 52)؛
- رقم المحاكاة المراد حذفها [numéro] غير موجود في قائمة المحاكاة؛
- السطور 63-66: في حالة حدوث خطأ، يتم إرسال نتيجة مع خطأ إلى وحدة التحكم الرئيسية؛
- السطر 68: يتم حذف المحاكاة رقم [numéro]؛
- السطر 69: لا تغير العملية [unset] الفهارس [0, n-1] في القائمة. لتحديثها، يتم طلب قيم الجدول [$simulations] لإزالة المحاكاة المفقودة؛
- السطر 71: يتم إعادة إدراج الجدول الجديد للمحاكاة في الجلسة؛
- السطران 73-74: يتم إرجاع قائمة المحاكاة الجديدة إلى وحدة التحكم الرئيسية؛
23.11.10. اختبارات [Postman]
سنقوم بإجراء اختبارات للخطأ والنجاح:

أعلاه:
- في [1-6]، طلب GET بدون المعلمة [numéro]؛
- في [7-10]، الرد jSON من الخادم؛
الآن طلب برقم غير صحيح من الناحية النحوية:

أعلاه:
- في [1-5]، طلب GET مع معلمة [numéro] غير صالحة [3, 5]؛
- في [6-9]، الرد jSON من الخادم؛
الآن طلب برقم محاكاة غير موجود:

أعلاه:
- في [1-5]، طلب برقم محاكاة يساوي 100 غير موجود في قائمة المحاكاة؛
- في [6-9]، الرد jSON من الخادم؛
الآن، سنقوم بحذف المحاكاة رقم 0 من القائمة، أي المحاكاة الأولى. أولاً، نطلب هذه القائمة مرة أخرى باستخدام الطلب [lister-simulations-500]:

- في [1]، يوجد حاليًا محاكاة واحدة؛
نحذف المحاكاة الأولى (رقم 0):

أعلاه:
- في [1-5]، يتم حذف المحاكاة رقم 0 [5]؛
- في [6-9]، نرى استجابة الخادم jSON. نلاحظ أن المحاكاة رقم 0 قد تم حذفها؛
لنكرر هذه العملية:

أعلاه:
- في [1]، لم تعد هناك أي محاكاة في جلسة الويب الخاصة بالخادم؛
23.11.11. الإجراء [fin-session]
يتم معالجة الإجراء [fin-session] بواسطة وحدة التحكم الثانوية [FinSessionController] التالية:
<?php
namespace Application;
// تبعيات Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class FinSessionController implements InterfaceController {
// $config هي تهيئة التطبيق
// معالجة طلب Request
// يستخدم الجلسة Session ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// يجب أن يكون هناك معلمة واحدة فقط GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
// خطأ؟
if ($erreur) {
$état = 401;
// النتيجة إلى وحدة التحكم الرئيسية
$message = "GET requis avec le seul paramètre [action] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// يتم حفظ نوع الجلسة
$type = $session->get("type");
// إبطال صلاحية الجلسة الحالية
$session->invalidate();
// إعادة تعيين النوع في الجلسة الجديدة
$session->set("type", $type);
// إرسال الرد
$état = 400;
// إرسال النتيجة إلى وحدة التحكم الرئيسية
$content = ["réponse" => "session supprimée"];
return [Response::HTTP_OK, $état, $content, []];
}
}
تعليقات
- الاستعلام [GET main.php?action=fin-session]؛
- الأسطر 25-33: يتم التحقق من أن الإجراء هو GET مع المعلمة الوحيدة [fin-action]؛
- السطر 38: يتم إبطال صلاحية الجلسة الحالية. يؤدي ذلك إلى حذف البيانات المسجلة فيها وبدء جلسة جديدة؛
- السطر 36: قبل انتهاء الجلسة، يتم حفظ نوعها [json, xml, html]؛
- السطر 40: يتم إعادة تعيين نوع الجلسة السابقة في الجلسة الجديدة. وأخيرًا، نبدأ جلسة جديدة بمفتاح فريد هو [type]؛
- السطران 44-45: يتم إرجاع النتيجة إلى وحدة التحكم الرئيسية؛
23.11.12. اختبارات [Postman]
سنقوم بإجراء اختبار للخطأ واختبار للنجاح:

أعلاه:
- في [1-5]، نطلب إنهاء الجلسة [5] باستخدام POST [2] بدلاً من GET المتوقع؛
- في [6-9]، الرد jSON من الخادم؛
الآن مثال على النجاح. لننظر أولاً إلى ملف تعريف الارتباط للجلسة الذي تم تبادله بين العميل [Postman] والخادم خلال الاختبار الأخير الذي تم إجراؤه:

أعلاه:
- في [3]، ملف تعريف الارتباط الخاص بالجلسة الذي أرسله العميل [Postman] إلى الخادم؛
لنلقِ نظرة الآن على رؤوس HTTP التي أرسلها الخادم في رده:

أعلاه:
- في [3-4]، لا يظهر ملف تعريف الارتباط الخاص بالجلسة في رد الخادم. وهذا أمر طبيعي. فالخادم لا يرسله إلا مرة واحدة: في بداية جلسة ويب جديدة؛
الآن لنقم بتنفيذ إجراء [fin-session] صالح:

أعلاه:
- في [1-3]، إجراء [fin-session] صالح؛
- في [4-7]، استجابة jSON من الخادم؛
لنلقِ نظرة على رؤوس HTTP المرسلة في استجابة الخادم:

- في [3]، يرسل الخادم الرأس [Set-Cookie]، مما يشير إلى بدء جلسة ويب جديدة؛
23.12. أنواع استجابات الخادم
23.12.1. مقدمة
لنعد إلى البنية العامة للتطبيق:

سنعرض أنواع الاستجابات الممكنة [3a]. وقد تم تجميعها في المجلد [Responses] الخاص بالمشروع:

لقد قدمنا بالفعل الفئة [JsonResponse] في الفقرة الخاصة بالروابط. وهي تنفذ الواجهة [InterfaceResponse] وتوسع نطاق الفئة [ParentResponse]. وينطبق الأمر نفسه على الفئتين الأخريين [XmlResponse] و [HtmlResponse].
لنتذكر تعريف واجهة [InterfaceResponse]:
<?php
namespace Application;
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// الطلب $request: الطلب قيد المعالجة
// جلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// array $headers: الرؤوس HTTP المراد إضافتها إلى الرد
// مسجل $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- الأسطر 19-27: تحتوي الواجهة [InterfaceResponse] على طريقة واحدة هي [send] لإرسال الاستجابة إلى العميل؛
- الأسطر 11-17: معنى المعلمات المختلفة للطريقة [send]؛
- الأسطر 23-25: المعلمات [$statusCode, $content, $headers] هي الاستجابة القياسية لوحدات التحكم الثانوية في التطبيق. ومع ذلك، قد تحتاج الاستجابة إلى معلومات أخرى. ولذلك، يتم تزويدها بالمعلمات الثلاثة الأولى (الأسطر 20-22) التي تتيح لها الوصول إلى جميع المعلومات المتعلقة بالطلب والجلسة والتكوين؛
- السطر 26: تحتاج الاستجابة إلى المعلمة [Logger] لأنها ستقوم بتسجيل الاستجابة المرسلة إلى العميل؛
لنستذكر الآن كود الفئة [ParentResponse]، وهي الفئة الأم للأنواع الثلاثة من الردود التي تجمع بين القواسم المشتركة بينها: الإرسال الفعلي لرد نصي إلى العميل:
<?php
namespace Application;
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: رمز حالة الرد HTTP
// سلسلة $content: نص الاستجابة المراد إرسالها
// حسب الحالة، قد تكون سلسلة jSON أو XML أو HTML
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الرد
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// إعداد نص الرد من الخادم
$response = new Response();
$response->setCharset("utf-8");
// رمز الحالة
$response->setStatusCode($statusCode);
// رؤوس
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// إرسال الرد
$response->setContent($content);
$response->send();
}
}
تعليقات
- الأسطر 10-13: معنى المعلمات الثلاثة لطريقة [send]؛
- السطر 17: تجدر الإشارة إلى أن نص الرد من النوع [string] وبالتالي جاهز للإرسال (السطر 30)؛
- السطر 22: ستحتوي الاستجابة على أحرف UTF-8؛
- السطر 24: رمز حالة الرد HTTP؛
- السطور 26-28: إضافة الرؤوس HTTP المحددة بواسطة رمز المتصل؛
- السطران 30-31: إرسال الرد إلى العميل؛
وأخيرًا، نذكر رمز وحدة التحكم الرئيسية التي تطلب إرسال الرد إلى العميل:
// إضافة المفاتيح [action, état] إلى استجابة وحدة التحكم
$content = ["action" => $action, "état" => $état] + $content;
// يتم إنشاء مثيل للكائن [Response] المكلف بإرسال الرد إلى العميل
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// تم إرسال الرد - يتم تحرير الموارد
$logger->close();
exit;
- السطر 4: نحدد اسم الفئة [Response] المراد إنشاء مثيل لها؛
- السطر 5: يتم إنشاء مثيل لها وإرسال الرد إلى العميل باستخدام الطريقة [send($request, $session, $config, $statusCode, $content, $headers, $logger)]. ونظرًا لأنها تنفذ نفس الواجهة [InterfaceResponse]، فإن الطرق [send] الخاصة بأنواع الردود المختلفة لها جميعًا نفس التوقيع؛
23.12.2. الفئة [JsonResponse]
تم عرضها بالفعل في الفقرة السابقة. ومع ذلك، نعيد عرض كودها لتسليط الضوء بشكل أفضل على تجانس فئات الردود الثلاث:
تقوم الفئة [JsonResponse] بتنفيذ الواجهة [InterfaceResponse] بالطريقة التالية:
<?php
namespace Application;
// تبعيات Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// الطلب $request: الطلب قيد المعالجة
// الجلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الرد
// مسجل $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// تحضير مُسلسل Symfony
$serializer = new Serializer(
[
// ضروري لتسلسل الكائنات
new ObjectNormalizer()],
// المشفّر jSON
// للخيارات، قم بعمل OU بين الخيارات المختلفة
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// التسلسل jSON
$json = $serializer->serialize($content, 'json');
// رؤوس الرسائل
$headers = array_merge($headers, ["content-type" => "application/json"]);
// إرسال الرد
parent::sendResponse($statusCode, $json, $headers);
// السجل
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
تعليقات
- السطر 13: تُنفذ الفئة الواجهة [InterfaceResponse]؛
- السطر 13: الفئة تمتد من الفئة [ParentResponse]. جميع أنواع [Response] تمتد من هذه الفئة. وهذه الفئة الأم هي التي ترسل الرد إلى العميل (السطر 46). ولأن هذا الكود كان مشتركًا بين جميع أنواع [Response]، فقد تم تجميعه في فئة أم؛
- الأسطر 33-40: إنشاء مثيل لمُسلسل [Symfony] الذي سيقوم بترجمة استجابة الخادم [$content] إلى سلسلة jSON (السطر 42)؛
- الأسطر 34-36: المعلمة الأولى لمُنشئ [Serializer] هي مصفوفة. يتم وضع مثيل لفئة [ObjectNormalizer] في هذا المصفوف، وهو ضروري لتسلسل الكائنات. تظهر هذه الحالة في هذا التطبيق مع قائمة من عمليات المحاكاة حيث تمثل كل عملية محاكاة مثيلًا لفئة [Simulation]؛
- السطر 39: المعلمة الثانية لمُنشئ [Serializer] هي أيضًا مصفوفة: يتم وضع جميع المشفرات المستخدمة في التسلسل (XML، jSON، CSV...) فيه؛
- السطر 39: لن يكون هناك سوى مُشفّر واحد هنا، من النوع [JsonEncoder]. كان من الممكن أن تكون مُنشِئة بدون معلمات كافية. هنا، قمنا بتمرير معلمة [JsonEncode] إلى المنشئ، فقط لتمرير خيارات الترميز jSON؛
- السطر 39: المعلمة الخاصة بالمنشئ [JsonEncode] عبارة عن مصفوفة من الخيارات. هنا نستخدم الخيار [JSON_UNESCAPED_UNICODE] لطلب أن يتم عرض أحرف UTF-8 من السلسلة jSON بشكل أصلي وليس "مهربًا"؛
- السطر 42: يتم تسلسل نص الرد HHTP إلى jSON بفضل أداة التسلسل السابقة؛
- السطر 44: يتم إضافة الرأس HTTP الذي يخبر العميل بأننا سنرسل له jSON؛
- السطر 46: يُطلب من الفئة الأم إرسال الرد إلى العميل؛
- الأسطر 48-50: يتم تسجيل الرد jSON؛
23.12.3. الفئة [XmlResponse]
تقوم الفئة [XmlResponse] بتنفيذ الواجهة [InterfaceResponse] بالطريقة التالية:
<?php
namespace Application;
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class XmlResponse extends ParentResponse implements InterfaceResponse {
// الطلب $request: الطلب قيد المعالجة
// جلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// array $content: استجابة الخادم
// مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الاستجابة
// مسجل $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// تحضير مُسلسل Symfony
$serializer = new Serializer(
// ضروري لتسلسل الكائنات
[new ObjectNormalizer()],
[
// التسلسل XML
new XmlEncoder(
[
XmlEncoder::ROOT_NODE_NAME => 'root',
XmlEncoder::ENCODING => 'utf-8'
]
),
// التسلسل jSON
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// التسلسل XML
$xml = $serializer->serialize($content, 'xml');
// رؤوس
$headers = array_merge($headers, ["content-type" => "application/xml"]);
// إرسال الرد
parent::sendResponse($statusCode, $xml, $headers);
// السجل
if ($logger !== NULL) {
// السجل بتنسيق jSON
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
تعليقات
- الأسطر 34-48: إنشاء مثيل لمُسلسل Symfony. يقبل المنشئ معلمتين من نوع المصفوفة؛
- السطر 36: المصفوفة الأولى تتضمن مثيلًا من النوع [ObjectNormalizer] الذي يشارك في تسلسل الكائنات؛
- الأسطر 37-47: المصفوفة الثانية تحتوي على المُشفِّرات المستخدمة في التسلسل. يمكن توقع أنواع مختلفة من التسلسل باستخدام نفس مُسلسل البيانات؛
- الأسطر 38-44: المُشفّر XML؛
- السطر 41: يتم تحديد جذر الرمز XML الذي تم إنشاؤه. وسيكون هذا الرمز على الشكل <root>[autres balises XML]</root>؛
- السطر 42: سيستخدم الترميز أحرف UTF-8؛
- السطر 46؛ المُشفّر jSON. سيُستخدم هذا المُشفّر لتسجيل الاستجابة في الملف [logs.txt] الذي تم إنشاؤه بتنسيق jSON؛
- السطر 50: يتم تسلسل نص الرد المرسل إلى العميل بتنسيق XML؛
- السطر 52: تُضاف إلى الرؤوس المستلمة كمعلمة (السطر 30) الرأس HTTP الذي يُشير إلى العميل بأن مستند XML يُرسل إليه؛
- السطر 54: إرسال الرد فعليًّا إلى العميل بواسطة الفئة الأم؛
- الأسطر 56-60: تسجيل الرد في ملف jSON؛
23.12.4. اختبارات [Postman]
لقد أجرينا بالفعل جميع اختبارات الأخطاء المحتملة في jSON. لا يوجد ما يمكن فعله أكثر من ذلك في XML. نعرض مثالين على الردود في XML:

أعلاه:
- في [1-3]، طلب بدء الجلسة XML؛
- في [4-7]، الاستجابة XML من الخادم؛
من الآن فصاعدًا، ستكون جميع ردود الخادم بتنسيق XML. يمكننا إعادة استخدام جميع الطلبات التي سبق استخدامها في [Postman] دون تغييرها، وسنحصل على رد XML لكل منها. لنفترض على سبيل المثال أن عملية المصادقة نجحت:

أعلاه:
- في [1-3]، طلب مصادقة صالح؛
- في [4-7]، الرد XML من الخادم؛
23.12.5. الاستجابة [HtmlResponse]
عندما يكون نوع الجلسة هو [html]، يتم إنشاء مثيل لكائن من النوع [HtmlResponse] لإرسال الرد إلى العميل. وسيقوم هذا الكائن بإرسال تدفق HTML إلى العميل، والذي يعتمد على رمز الحالة الذي أعاده وحدة التحكم الثانوية التي عالجت الإجراء. يتم تسجيل هذه المطابقة [état=>vue] في ملف التكوين [config.json] على النحو التالي:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
يُقرأ هذا التكوين على النحو التالي: [‘nom de la vue’ => ‘états associés à cette vue’]
- السطر 2: إذا قدمت وحدة التحكم الثانوية حالة من الجدول [700, 221, 400]، فيجب عرض العرض [vue-authentification.php]؛
- السطر 3: إذا أرجعت وحدة التحكم الثانوية حالة من الجدول [200, 300, 341, 350, 800]، فيجب عرض العرض [vue-calcul-impot.php]؛
- السطر 4: إذا أرجع وحدة التحكم الثانوية حالة من الجدول [500, 600]، فيجب عرض العرض [vue-liste-simulations.php]؛
- السطر 6: إذا أرجع وحدة التحكم الثانوية حالة غير موجودة في أي من الجداول السابقة، فيجب عرض العرض [vue-erreurs.php]؛
يتم تجميع طرق العرض في المجلد [Views] الخاص بالمشروع:

رمز الفصل [HtmlResponse] هو كما يلي:
<?php
namespace Application;
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class HtmlResponse extends ParentResponse implements InterfaceResponse {
// طلب $request: الطلب قيد المعالجة
// جلسة $session: جلسة تطبيق الويب
// مصفوفة $config: تكوين التطبيق
// int statusCode: رمز حالة الاستجابة HTTP
// مصفوفة $content: استجابة الخادم
// array $headers: الرؤوس HTTP المراد إضافتها إلى الرد
// Logger $logger: المسجل لكتابة السجلات
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// تحضير مُسلسل Symfony
$serializer = new Serializer(
[
// لتسلسل الكائنات
new ObjectNormalizer()],
[
// لتسلسل سجل استجابة jSON
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// الاستجابة HTML تعتمد على رمز الحالة الذي يقدمه وحدة التحكم
$état = $content["état"];
// لكل حالة، هناك عرض واحد - يتم البحث عنه في تكوين التطبيق
// قائمة العروض
$vues = array_keys($config["vues"]);
$trouvé = false;
$i = 0;
// يتم تصفح قائمة العروض
while (!$trouvé && $i < count($vues)) {
// الحالات المرتبطة بالعرض رقم i
$états = $config["vues"][$vues[$i]];
// هل توجد الحالة المطلوبة ضمن الحالات المرتبطة بالعرض رقم I؟
if (in_array($état, $états)) {
// سيكون العرض المعروض هو العرض رقم i
$vueRéponse = $vues[$i];
$trouvé = true;
}
// العرض التالي
$i++;
}
// هل تم العثور عليه؟
if (!$trouvé) {
// إذا لم تكن هناك أي عرض للحالة الحالية للتطبيق
// يتم عرض شاشة الأخطاء
$vueRéponse = $config["vue-erreurs"];
}
// يتم استرداد العرض HTML لعرضه في سلسلة أحرف
ob_start();
require __DIR__ . "/../Views/$vueRéponse";
$html = ob_get_clean();
// يتم الإشارة في الرؤوس إلى أنه سيتم إرسال HTML
$headers = array_merge($headers, ["content-type" => "text/html"]);
// تتولى الفئة الأم عملية إرسال الرد فعليًا
parent::sendResponse($statusCode, $html, $headers);
// تسجيل في jSON للرد بدون HTML
if ($logger !== NULL) {
// تسجيل في jSON للرد من وحدة التحكم الثانوية التي عالجت الإجراء
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
تعليقات
- الأسطر 32-41: يتم إنشاء مثيل لمُسلسل Symfony. وهذا ضروري لتسجيل jSON لاستجابة وحدة التحكم التي عالجت الإجراء (الأسطر 72-82)؛
- الأسطر 42-57: يتم البحث في تكوين التطبيق عن العرض الذي يجب عرضه. ويعتمد هذا العرض على رمز الحالة الذي أرجعته وحدة التحكم التي عالجت الإجراء. ويوجد هذا الرمز في [$content[‘état’]] (السطر 43)؛
- الأسطر 42-61: يتم البحث عن العرض الذي يتوافق مع هذه الحالة؛
- الأسطر 62-67: إذا لم يتم العثور على أي عرض، فهذا يعني أننا في حالة رمز حالة غير طبيعية للتطبيق HTML. سنشرح مفهوم الحالات غير الطبيعية هذا لاحقًا. في هذه الحالة، يتم عرض شاشة خطأ؛
- الأسطر 68-70: يتم تفسير الرمز PHP الخاص بالعرض المحدد وتخزين النتيجة في المتغير [$html] (السطر 71)؛
- يستحق هذا الرمز بعض التوضيح. لنفترض أن العرض المحدد هو [vue-authentification.php] الذي يعرض نموذج مصادقة على الويب:
- السطر 69: تبدأ الدالة [ob_start] ما تسميه الوثائق «تأخير الإخراج». كل ما يُكتب بواسطة عمليات print و require… وما يُرسل عادةً على الفور إلى العميل، يذهب إلى مخزن إخراج مؤقت (ob=output buffer) دون إرساله إلى العميل؛
- السطر 70: يتم تحميل العرض [vue-authentification.php]، وهو عرض ديناميكي HTML يحتوي على كود PHP. ثم يحدث أمران:
- يتم تحميل وتفسير كود PHP الخاص بالعرض [vue-authentification.php]. والنتيجة هي عرض سنسميه [vue-authentification.html] لا يحتوي إلا على كود HTML، أو حتى CSS وجافا سكريبت، ولكنه لا يحتوي على PHP؛
- يتم إرسال هذا الكود HTML عادةً إلى العميل. وهذا هو الحال في الواقع بالنسبة لأي نص يصادفه المترجم PHP ولا يكون كود PHP. بسبب تأخير الإخراج، يتم وضع هذا الرمز HTML في مخزن الإخراج المؤقت دون إرساله إلى العميل؛
- السطر 71: تقوم الدالة [ob_get_clean] بأمرين:
- تضع في المتغير [$html] محتوى مخزن الإخراج المؤقت، أي الصفحة [vue-authentification.html] التي تم وضعها فيه؛
- تقوم بإفراغ المخزن المؤقت للإخراج. وبالنسبة لهذا الأخير، يبدو الأمر كما لو أن شيئًا لم يحدث. من ناحية أخرى، لم يتلق العميل أي شيء حتى الآن؛
- السطر 70: نحن هنا في طور تنفيذ الفئة [HtmlResponse] الموجودة في المجلد [Responses]. للعثور على العرض، يجب الرجوع مستوىً واحدًا إلى الأعلى إلى [..] ثم الانتقال إلى المجلد [Views]. [__DIR__] هو الاسم المطلق للمجلد الذي يوجد فيه البرنامج النصي قيد التنفيذ، وفي مثالنا هو المجلد [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses]؛
- السطر 73: نضيف إلى الرؤوس HTTP المستلمة كمعلمة (السطر 29)، الرأس الذي يُعلم العميل بأننا سنرسل له HTML؛
- السطر 75: نطلب من الفئة الأم المضي قدمًا في إرسال الرد فعليًّا إلى العميل؛
- الأسطر 77-81: يتم تسجيل الرد [$content] المقدم من وحدة التحكم الثانوية التي عالجت الإجراء الجاري في jSON؛
23.12.6. اختبارات [Postman]
لاختبار وضع HTML للجلسة بشكل فعلي، يتعين علينا مراجعة جميع العروض. سنقوم بذلك لاحقًا. سنجري الاختبار التالي:
لنلقِ نظرة على قائمة طرق العرض في ملف التكوين:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
يمكننا العثور على السياق الذي ينتج بعض رموز الحالة المذكورة أعلاه من خلال الاطلاع على الاختبارات [Postman] التي تم إجراؤها:

نلاحظ أن رمز الحالة [700] هو رمز لعملية [init-session] التي نجحت في تنفيذ العملية [2]. فيما سبق، لدينا استجابة jSON، لكنها قد تكون من النوع XML أو HTML. وسيتم اختبار الحالة الأخيرة. وفقًا لملف التكوين، فإن العرض [vue-authentification.php] هو الذي يشكل الاستجابة HTML. دعونا نتحقق من ذلك.

أعلاه:
- في [1-3]، يتم تهيئة جلسة HTML. لذا نتوقع استجابة HTML؛
- في [4-8]، رد HTML من الخادم؛
- تتيح علامة التبويب [8] معاينة الكود HTML الذي تم استلامه؛

- في [8-9]، معاينة للعرض HTML؛
23.13. تطبيق الويب HTML
23.13.1. عرض الصفحات
سيستخدم تطبيق الويب HTML أربع طرق عرض:
طريقة عرض المصادقة:

طريقة عرض حساب الضريبة:

طريقة عرض قائمة عمليات المحاكاة:

عرض الأخطاء غير المتوقعة:

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

يتكون العرض من عنصرين سنسميهما «شظايا»:
- يتم إنشاء الجزء [1] بواسطة البرنامج النصي [v-bandeau.php]؛
- يتم إنشاء الجزء [2] بواسطة البرنامج النصي [v-authentification.php]؛
يتم إنشاء صفحة المصادقة بواسطة الصفحة التالية [vue-authentification.php]:
<?php
// بيانات اختبار الصفحة
// يتم تغليف بيانات الصفحة في $page
…
?>
<!doctype html>
<html lang="fr">
<head>
<!-- علامات meta المطلوبة -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- شريط علوي مكون من سطر واحد و12 عمودًا -->
<?php require "v-bandeau.php"; ?>
<!-- نموذج المصادقة المكون من 9 أعمدة -->
<div class="row">
<div class="col-md-9">
<?php require "v-authentification.php" ?>
</div>
</div>
<?php
// في حالة وجود خطأ - يتم عرض تنبيه خطأ
if ($modèle->error) {
print <<<EOT
<div class="row">
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
Les erreurs suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT;
}
?>
</div>
</body>
</html>
تعليقات
- السطر 7: يبدأ المستند HTML بهذا السطر؛
- الأسطر 8-44: الصفحة HTML مغلفة بعلامتي <html> </html>؛
- الأسطر 9-16: رأس (head) المستند HTML؛
- السطر 11: تشير العلامة <meta charset> إلى أن المستند مكتوب بترميز UTF-8؛
- السطر 12: تحدد العلامة <meta name=’viewport’> العرض الأولي للمنظر: على كامل عرض الشاشة التي تعرضه (width) بحجمه الأصلي (initial-scale) دون تغيير الحجم للتكيف مع شاشة أصغر (shrink-to-fit)؛
- السطر 14: تحدد العلامة <link rel=’stylesheet’> الملف CSS الذي يتحكم في مظهر العرض. نستخدم هنا إطار العمل CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ؛
- السطر 15: تحدد العلامة <title> عنوان الصفحة:

- الأسطر 17-43: يتم تغليف نص الصفحة الإلكترونية بين العلامتين <body></body>؛
- الأسطر 18-42: تحدد العلامة <div> قسمًا من الصفحة المعروضة. تشير جميع السمات [class] المستخدمة في العرض إلى إطار العمل CSS Bootstrap. تحدد العلامة <div class=’container’> حاوية Bootstrap؛
- السطر 20: يتم تضمين البرنامج النصي [v-bandeau.php]. يقوم هذا البرنامج النصي بإنشاء شريط رأس الصفحة [1]. سنقوم بوصفه قريبًا؛
- الأسطر 22-26: تحدد العلامة <div class=’row’> صفًا في Bootstrap. تتكون هذه الصفوف من 12 عمودًا؛
- السطر 23: تحدد العلامة <div class=’col-md-9’> قسمًا مكونًا من 9 أعمدة؛
- السطر 24: يتم تضمين البرنامج النصي [v-authentification.php] الذي يعرض نموذج المصادقة [2] في الصفحة. سنشرح ذلك قريبًا؛
- السطر 27: العلامة <?php تُدرج كود PHP داخل الصفحة HTML. يتم تنفيذ هذا الكود قبل عرض الصفحة HTML ويمكنه تعديلها؛
- السطر 29: سيتم تغليف جميع البيانات الديناميكية للطريقة المعروضة في كائن [$modèle] من النوع [stdClass]. وهذا اختيار تعسفي. كان من الممكن اختيار مصفوفة مرتبطة بدلاً من ذلك للحصول على نفس النتيجة؛
- السطر 29: تفشل عملية المصادقة إذا أدخل المستخدم بيانات اعتماد غير صحيحة. في هذه الحالة، يتم إعادة عرض شاشة المصادقة مع رسالة خطأ. يشير السمة [$modèle→error] إلى ما إذا كان يجب عرض رسالة الخطأ هذه؛
- الأسطر 30-39: تكتب هذه الصيغة كل النص الموجود بين الرمزين PHP <<<EOT (السطر 30 – يمكن وضع ما تريد بدلاً من EOT=End Of Text) والرمز EOT في السطر 39 (يجب أن يكون مطابقًا للرمز المستخدم في السطر 30). يجب كتابة الرمز في العمود الأول من السطر 39. يتم تفسير المتغيرات PHP الموجودة في النص بين الرمزين EOT؛
- السطور 33-36: تحدد منطقة ذات خلفية وردية (class="alert alert-danger") (السطر 33)؛

- السطر 34: نص؛
- السطر 35: العلامة HTML <ul> (قائمة غير مرتبة) تعرض قائمة بنقاط. يجب أن يكون لكل عنصر في القائمة الصيغة <li>عنصر</li>؛
دعونا نستخلص من هذا الكود العناصر الديناميكية التي يجب تعريفها:
- [$modèle→error]: لعرض رسالة خطأ؛
- [$modèle→erreurs]: قائمة (بالمعنى المقصود في HTML) من رسائل الخطأ؛
23.13.2.2. المقطع [v-bandeau.php]
يعرض المقطع [v-bandeau.php] الشريط العلوي لجميع طرق عرض تطبيق الويب:

كود الجزء [v-bandeau.php] هو كما يلي:
<!-- جومبوترون Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
تعليقات
- الأسطر 2-13: الشريط العلوي مغلف في قسم Bootstrap من نوع Jumbotron [<div class="jumbotron">]. تعمل فئة Bootstrap هذه على تصميم المحتوى المعروض بطريقة خاصة لإبرازه؛
- الأسطر 3-12: سطر Bootstrap؛
- الأسطر 4-6: صورة [img] موضوعة في الأعمدة الأربعة الأولى من السطر؛
- السطر 5: بناء الجملة [<?= $logo ?>] مكافئ لبناء الجملة [<?php print $logo ?>]. بعبارة أخرى، ستكون قيمة السمة [src] هي قيمة المتغير PHP [$logo]؛
- الأسطر 7-11: ستُستخدم الأعمدة الثمانية الأخرى في السطر (نذكر أن العدد الإجمالي هو 12 عمودًا) لوضع نص (السطر 9) بخط كبير (<h1>، الأسطر 8-10)؛
العناصر الديناميكية:
- [$logo]: URL للصورة المعروضة في الشريط؛
23.13.2.3. المقطع [v-authentification.php]
يعرض الجزء [v-authentification .php] نموذج المصادقة لتطبيق الويب:

رمز الجزء [v-authentification.php] هو التالي:
<!-- نموذج HTML - يتم إرسال قيمه باستخدام الإجراء [authentifier-utilisateur] -->
<form method="post" action="main.php?action=authentifier-utilisateur">
<!-- العنوان -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
<!-- نموذج Bootstrap -->
<fieldset class="form-group">
<!-- السطر الأول -->
<div class="form-group row">
<!-- التسمية -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- منطقة إدخال النص -->
<input type="text" class="form-control" id="user" name="user"
placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
</div>
</div>
<!-- السطر الثاني -->
<div class="form-group row">
<!-- العنوان -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- منطقة إدخال النص -->
<div class="col-md-4">
<input type="password" class="form-control" id="password" name="password"
placeholder="Mot de passe">
</div>
</div>
<!-- زر من النوع [submit] في السطر الثالث-->
<div class="form-group row">
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
تعليقات
- السطور 2-39: تحدد علامة <form> نموذج HTML. ويتميز هذا النموذج عمومًا بالخصائص التالية:
- يحدد حقول الإدخال (علامات <input> في السطرين 17 و27)؛
- يحتوي على زر من النوع [submit] (السطر 34) الذي يرسل القيم المدخلة إلى URL المحدد في السمة [action] لعلامة [form] (السطر 2). يتم تحديد الطريقة HTTP المستخدمة لاستدعاء هذا العنصر URL في السمة [method] لعلامة [form] (السطر 2)؛
- وهنا، عندما ينقر المستخدم على الزر [Valider] (السطر 34)، سيقوم المتصفح بإرسال (السطر 2) القيم التي تم إدخالها في النموذج إلى URL [main.php?action=authentifier-utilisateur] (السطر 2)؛
- القيم التي سيتم ترحيلها هي القيم التي أدخلها المستخدم في حقول الإدخال بالسطرين 17 و27. وسيتم ترحيلها بالصيغة [user=xx&password=yy]. أسماء المعلمات [user, password] هي أسماء سمات [name] الخاصة بحقول الإدخال في السطرين 17 و27؛
- السطر 5-7: قسم Bootstrap لعرض عنوان على خلفية زرقاء:

- السطور 10-37: نموذج Bootstrap. سيتم عندئذٍ تصميم جميع عناصر النموذج بطريقة معينة؛
- الأسطر 12-20: تحدد السطر الأول من النموذج:
![]()
- يحدد السطر 14 العنوان [1] على ثلاثة أعمدة. يربط السمة [for] للعلامة [label] العنوان بالسمة [id] لمنطقة الإدخال في السطر 17؛
- السطور 15-19: تضع حقل الإدخال في مجموعة مكونة من أربعة أعمدة؛
- السطر 17: العلامة HTML [input] تصف حقل إدخال. ولها عدة معلمات:
- [type=’text’]: هي منطقة إدخال نصية. يمكن كتابة أي شيء فيها؛
- [class=’form-control’]: نمط Bootstrap لمنطقة الإدخال؛
- [id=’user’]: معرّف حقل الإدخال. يُستخدم هذا المعرّف عادةً بواسطة CSS ورمز جافا سكريبت؛
- [name=’user’]: اسم حقل الإدخال. سيتم إرسال القيمة التي أدخلها المستخدم عبر المتصفح تحت هذا الاسم [user=xx]؛
- [placeholder=’invite’]: النص المعروض في حقل الإدخال عندما لا يكون المستخدم قد أدخل أي شيء بعد؛
![]()
- [value=’valeur’]: سيُعرض النص «القيمة» في حقل الإدخال فور ظهوره، أي قبل أن يقوم المستخدم بإدخال أي شيء آخر. تُستخدم هذه الآلية في حالة حدوث خطأ لعرض الإدخال الذي تسبب في الخطأ. وهنا ستكون هذه القيمة هي قيمة المتغير PHP [$modèle→login]؛
- الأسطر 21-30: كود مشابه لإدخال كلمة المرور؛
- السطر 27: [type=’password’] يؤدي إلى ظهور حقل إدخال نص (يمكن كتابة أي شيء فيه) لكن الأحرف المكتوبة تظل مخفية:
![]()
- الأسطر 32-36: سطر ثالث للزر [Valider]؛
- السطر 34: نظرًا لوجود السمة [type=submit]، فإن النقر على هذا الزر يؤدي إلى إرسال القيم التي تم إدخالها إلى الخادم عبر المتصفح كما تم شرحه سابقًا. يعرض السمة CSS [class="btn btn-primary"] زرًا أزرق:

يبقى لنا شرح أمر أخير. في السطر 2، يحدد السمة [action="main.php?action=authentifier-utilisateur"] قيمة URL غير مكتملة (لا تبدأ بـ http://machine:port/chemin). في مثالنا، جميع عناصر URL في التطبيق تأتي بالصيغة [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. سيتم الحصول على عرض المصادقة باستخدام عناصر URL متنوعة:
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html]؛
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur]
تشير هذه القيم URL إلى مستند [main.php] الموجود في المسار [http://localhost/php7/scripts-web/impots/version-12]. وينطبق ذلك على جميع قيم URL الخاصة بهذا التطبيق. سيتم إضافة هذا المسار كبادئة للمعلمة [action="main.php?action=authentifier-utilisateur"] عند إرسال القيم المدخلة. وبالتالي، سيتم إرسال هذه القيم إلى URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur].
23.13.2.4. الاختبارات المرئية
يمكن إجراء اختبارات للعروض قبل دمجها في التطبيق بوقت طويل. يتعلق الأمر هنا باختبار مظهرها البصري. سنجمع جميع عروض الاختبار في مجلد [Tests] الخاص بالمشروع:

لاختبار العرض [vue-authentification.php]، يتعين علينا إنشاء نموذج البيانات الذي سيعرضه:
<?php
// بيانات اختبار الصفحة
//
// يتم حساب نموذج العرض
$modèle = getModelForThisView();
function getModelForThisView(): object {
// يتم تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// معرف المستخدم
$modèle->login = "albert";
// قائمة الأخطاء
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// يتم إنشاء قائمة HTML بالأخطاء
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// صورة الشريط العلوي
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// يتم عرض النموذج
return $modèle;
}
?>
<!-- المستند HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- علامات meta المطلوبة -->
…
</head>
<body>
….
</body>
</html>
تعليقات
- الأسطر 1-5: تحتوي طريقة عرض المصادقة على أجزاء ديناميكية يتحكم فيها الكائن [$modèle]. يُطلق على هذا الكائن اسم نموذج العرض. وفقًا لأحد التعريفين المقدمين للاختصار MVC، فإننا نجد هنا الحرف M من MVC؛
- السطر 5: يتم حساب نموذج العرض بواسطة الدالة [getModelForThisView]؛
- السطر 9: سيتم تغليف نموذج العرض في نوع [stdClass]؛
- الأسطر 10-22: يتم تحديد قيم الاختبار للعناصر الديناميكية في واجهة المصادقة؛
يمكن إجراء الاختبار المرئي من خلال Netbeans:

نواصل هذه الاختبارات البصرية حتى نكون راضين عن النتيجة.
23.13.2.5. حساب نموذج العرض
بمجرد تحديد الشكل المرئي للواجهة، يمكننا المضي قدمًا في حساب نموذج الواجهة في الظروف الفعلية. دعونا نستعرض رموز الحالة التي تؤدي إلى هذه الواجهة. يمكن العثور عليها في ملف التكوين:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
إذن، رموز الحالة [700, 221, 400] هي التي تعرض شاشة المصادقة. لمعرفة معنى هذه الرموز، يمكن الاستعانة باختبارات [Postman] التي أجريت على التطبيق jSON:
- [init-session-json-700]: 700 هو رمز الحالة بعد نجاح إجراء [init-session]: وعندها يتم عرض نموذج المصادقة فارغًا؛
- [authentifier-utilisateur-221]: 221 هو رمز الحالة عند فشل إجراء [authentifier-utilisateur] (بيانات اعتماد غير معترف بها): وعندها يتم عرض نموذج المصادقة لتصحيحه؛
- [fin-session-400]: 400 هو رمز الحالة بعد نجاح العملية [fin-session]: عندها يتم عرض نموذج المصادقة فارغًا؛
والآن بعد أن عرفنا متى يجب عرض نموذج المصادقة، يمكننا حساب نموذجه في [vue-authentification.php]:

فيما يلي كود حساب قالب العرض [vue-authentification.php]:
<?php
// يتم توريث المتغيرات التالية
// الطلب $request: الطلب الحالي
// الجلسة $session: جلسة التطبيق
// مصفوفة $config: تكوين التطبيق
// مصفوفة $content: استجابة وحدة التحكم
//
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// يتم حساب نموذج العرض
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new stdClass();
// حالة التطبيق
$état = $content["état"];
// يعتمد النموذج على الحالة
switch ($état) {
case 700:
case 400:
// حالة عرض النموذج فارغًا
$modèle->login = "";
// لا توجد أخطاء لعرضها
$modèle->error = FALSE;
break;
case 221:
// مصادقة خاطئة
// يتم إعادة عرض المستخدم الذي تم إدخاله في البداية
$modèle->login = $request->request->get("user");
// هناك خطأ يجب عرضه
$modèle->error = TRUE;
// قائمة HTML برسائل الخطأ - هنا رسالة واحدة فقط
$modèle->erreurs = "<li>Echec de l'authentification</li>";
}
// نتيجة
return $modèle;
}
?>
<!-- المستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 3-6: يتم استدعاء المتغيرات الموروثة من الفئة [HtmlResponse] التي تعمل على عرض العرض [vue-authentification.php] بواسطة [require]؛
- الأسطر 9-10: فئات Symfony المستخدمة في كود العرض؛
- الأسطر 15-40: تتولى الدالة [getModelForThisView] حساب نموذج العرض؛
- السطر 19: يتم استرداد رمز الحالة الذي أعاده وحدة التحكم التي عالجت الإجراء الجاري؛
- الأسطر 21-37: يعتمد النموذج على رمز الحالة هذا؛
- الأسطر 22-28: الحالة التي يتعين فيها عرض نموذج مصادقة فارغ؛
- الأسطر 29-37: حالة المصادقة الخاطئة: يتم عرض المعرف الذي أدخله المستخدم وعرض رسالة خطأ. يمكن للمستخدم عندئذٍ إعادة المحاولة بإدخال معرف آخر؛
تمت كتابة نموذج خاص للشريط [v-bandeau.php]:
<?php
// شعار
$scheme = $request->server->get('REQUEST_SCHEME'); // http
$host = $request->server->get('SERVER_NAME'); // localhost
$port = $request->server->get('SERVER_PORT'); // 80
$uri = $request->server->get('REQUEST_URI'); // /php7/scripts-web/impots/version-12/main.php?action=xxx
$champs = [];
preg_match("/(.+)\/.+?$/", $uri, $champs);
$root = $champs[1]; // /php7/scripts-web/impots/version-12
$modèle->logo = "$scheme://$host:$port$root/Views/logo.jpg"; // http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg
?>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $modèle->logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
تعليقات
- يستخدم السطر 16 المتغير [$modèle→logo] الذي يمثل URL الخاص بشعار الشريط. وبدلاً من حساب هذا المتغير أربع مرات لكل من العروض الأربعة للتطبيق، تم تحليل هذا الحساب إلى العوامل في الجزء [v-bandeau.php]؛
- توضح الأسطر 1-11 كيفية إنشاء المتغير URL و[http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] استنادًا إلى المعلومات الموجودة في بيئة الخادم [$request→server]؛
23.13.2.6. اختبارات [Postman]
لقد أنشأنا بالفعل استعلامات تنتج الرموز [700, 221, 400] التي تعرض شاشة المصادقة. دعونا نستعرضها:
- [init-session-html-700]: 700 هو رمز الحالة عند انتهاء إجراء [init-session] بنجاح: وعندها يتم عرض نموذج المصادقة فارغًا؛
- [authentifier-utilisateur-221]: 221 هو رمز الحالة عند فشل إجراء [authentifier-utilisateur] (بيانات اعتماد غير معترف بها): عندها يتم عرض نموذج المصادقة لتصحيحه؛
- [fin-session-400]: 400 هو رمز الحالة بعد نجاح الإجراء [fin-session]: عندها يتم عرض نموذج المصادقة فارغًا؛
يكفي إعادة استخدامها والتحقق مما إذا كانت تعرض شاشة المصادقة بشكل صحيح. لن نعرض هنا سوى اختبارين:
- [init-session-html-700]: بدء جلسة عمل HTML؛

- [authentifier-utilisateur-221]: مصادقة المستخدم [x, x]؛

أعلاه:
- أرسل الطلب السلسلة [user=x&password=x]؛
- في [4]، تظهر رسالة خطأ؛
- في [3]، تم إعادة عرض المستخدم الخاطئ؛
23.13.2.7. Conclusion
تمكنا من اختبار العرض [vue-authentification.php] دون كتابة العروض الأخرى. وقد كان ذلك ممكنًا لأن:
- تمت كتابة جميع وحدات التحكم؛
- تسمح لنا [Postman] بإرسال طلبات إلى الخادم دون الحاجة إلى طرق العرض. عند كتابة وحدات التحكم، يجب أن ندرك أن أي شخص يمكنه القيام بذلك. لذلك يجب أن نكون مستعدين لمعالجة الطلبات التي لا تسمح بها أي طريقة عرض. يتم إنشاء هذه الطلبات يدويًّا في [Postman]. يجب ألا نقول مسبقًا «هذا الطلب مستحيل». يجب التحقق؛
23.13.3. عرض حساب الضريبة
23.13.3.1. عرض الصفحة
عرض حساب الضريبة هو كما يلي:

تتكون طريقة العرض من ثلاثة أجزاء:
- 1: الشريط العلوي الذي تم إنشاؤه بواسطة المقطع [v-bandeau.php] الذي سبق عرضه؛
- 2: نموذج حساب الضريبة الذي تم إنشاؤه بواسطة المقطع [v-calcul-impot.php]؛
- 3: قائمة تحتوي على رابطين، تم إنشاؤها بواسطة المقطع [v-menu.php]؛
يتم إنشاء عرض حساب الضريبة بواسطة البرنامج النصي [vue-calcul-impot.php] التالي:

<?php
// يتم توريث المتغيرات التالية
// الطلب $request: الطلب الحالي
// الجلسة $session: جلسة التطبيق
// array $config: إعدادات التطبيق
// array $content: استجابة وحدة التحكم التي عالجت الإجراء
//
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// يتم حساب نموذج العرض
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// يتم تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
…
// يتم عرض النموذج
return $modèle;
}
?>
<!-- مستند HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- علامات الميتا المطلوبة -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- شريط -->
<?php require "v-bandeau.php"; ?>
<!-- صف من عمودين -->
<div class="row">
<!-- القائمة -->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- نموذج الحساب -->
<div class="col-md-9">
<?php require "v-calcul-impot.php" ?>
</div>
</div>
<!-- حالة النجاح -->
<?php
if ($modèle->success) {
// يتم عرض تنبيه بالنجاح
print <<<EOT1
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-success" role="alert">
$modèle->impôt</br>
$modèle->décôte</br>\n
$modèle->réduction</br>\n
$modèle->surcôte</br>\n
$modèle->taux</br>\n
</div>
</div>
</div>
EOT1;
}
?>
<?php
if ($modèle->error) {
// قائمة الأخطاء المكونة من 9 أعمدة
print <<<EOT2
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
L'erreur suivante s'est produite :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT2;
}
?>
</div>
</body>
</html>
تعليقات
- نكتفي بالتعليق على المستجدات التي لم نرها من قبل؛
- السطر 37: إدراج الشريط العلوي للعرض في السطر الأول من Bootstrap الخاص بالعرض؛
- الأسطر 41-43: إدراج القائمة التي ستشغل ثلاثة أعمدة من السطر الثاني من Bootstrap في العرض؛
- الأسطر 45-47: إدراج نموذج حساب الضريبة الذي سيشغل تسعة أعمدة من السطر الثاني لـ Bootstrap في العرض؛
- الأسطر 51-69: إذا نجح حساب الضريبة [$modèle→success=TRUE]، فسيتم عرض نتيجة حساب الضريبة في إطار أخضر (الأسطر 59-65). يقع هذا الإطار في السطر الثالث من Bootstrap في العرض (السطر 54) ويشغل تسعة أعمدة (السطر 58) على يمين ثلاثة أعمدة فارغة (الأسطر 55-57). وبالتالي، سيكون هذا الإطار مباشرةً أسفل نموذج حساب الضريبة؛
- الأسطر 71-87: إذا فشل حساب الضريبة [$modèle→error=TRUE]، فسيتم عرض رسالة خطأ في إطار وردي (الأسطر 80-83). يوجد هذا الإطار في السطر الثالث من Bootstrap في العرض (السطر 75) ويشغل تسعة أعمدة (السطر 79) على يمين ثلاثة أعمدة فارغة (الأسطر 76-78). وبالتالي، سيكون هذا الإطار مباشرة أسفل نموذج حساب الضريبة؛
23.13.3.2. المقتطف [v-calcul-impot.php]
يعرض المقتطف [v-calcul-impot.php] نموذج المصادقة لتطبيق الويب:

رمز المقطع [v-calcul-impot.php] هو كما يلي:
<!-- نموذج HTML المرسَل -->
<form method="post" action="main.php?action=calculer-impot">
<!-- رسالة مكونة من 12 عمودًا على خلفية زرقاء -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
<!-- عناصر النموذج -->
<fieldset class="form-group">
<!-- السطر الأول في 9 أعمدة -->
<div class="row">
<!-- نص على 4 أعمدة -->
<legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
<!-- أزرار الاختيار على 5 أعمدة-->
<div class="col-md-5">
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" <?= $modèle->checkedOui ?>>
<label class="form-check-label" for="gridRadios1">
Oui
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" <?= $modèle->checkedNon ?>>
<label class="form-check-label" for="gridRadios2">
Non
</label>
</div>
</div>
</div>
<!-- السطر الثاني المكون من 9 أعمدة -->
<div class="form-group row">
<!-- نص على 4 أعمدة -->
<label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
<!-- حقل إدخال رقمي لعدد الأطفال مكون من 5 أعمدة -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="<?= $modèle->enfants ?>">
</div>
</div>
<!-- السطر الثالث المكون من 9 أعمدة -->
<div class="form-group row">
<!-- عنوان على 4 أعمدة -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
<!-- حقل إدخال رقمي للراتب مكون من 5 أعمدة -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel" aria-describedby="salaireHelp" value="<?= $modèle->salaire ?>">
<small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
</div>
</div>
<!-- السطر الرابع، زر [submit] مكون من 5 أعمدة -->
<div class="form-group row">
<div class="col-md-5">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
تعليقات
- السطر 2: سيتم إرسال النموذج HTML (السمة [method]) إلى URL [main.php?action=calculer-impot] (السمة [action]). ستكون القيم التي يتم إرسالها هي قيم حقول الإدخال:
- قيمة زر الاختيار المحدد بالشكل التالي:
- [marié=oui] إذا تم تحديد زر الاختيار [Oui] (السطور 16-22). [marié] هي قيمة السمة [name] في السطر 18، و[oui] هي قيمة السمة [value] في السطر 18؛
- [marié=non] إذا تم تحديد زر الاختيار [Non] (الأسطر 23-28). [marié] هي قيمة السمة [name] في السطر 24، و [non] هي قيمة السمة [value] في السطر 24؛
- قيمة حقل الإدخال الرقمي في السطر 37 على شكل [enfants=xx]، حيث [enfants] هي قيمة السمة [name] في السطر 37، و [xx] هي القيمة التي أدخلها المستخدم عبر لوحة المفاتيح؛
- قيمة حقل الإدخال الرقمي في السطر 46 بالصيغة [salaire=xx] حيث [salaire] هي قيمة السمة [name] في السطر 46، و [xx] هي القيمة التي أدخلها المستخدم عبر لوحة المفاتيح؛
- قيمة زر الاختيار المحدد بالشكل التالي:
وأخيرًا، ستكون القيمة التي سيتم إرسالها على الشكل [marié=xx&enfants=yy&salaire=zz].
- سيتم إرسال القيم التي تم إدخالها عندما ينقر المستخدم على الزر من النوع [submit] في السطر 53؛
- الأسطر 16-30: زرادا الاختيار:
![]()
يعد زرا الراديو جزءًا من نفس مجموعة زرا الراديو لأنهما يحملان نفس السمة [name] (السطران 18 و24). يتأكد المتصفح من أنه في مجموعة زرا الراديو، يتم تحديد زر واحد فقط في أي وقت. لذا فإن النقر على أحدهما يؤدي إلى إلغاء تحديد الزر الذي كان محددًا من قبل؛
- وهما زران من نوع «زر الاختيار» بسبب السمة [type="radio"] (السطران 18 و24)؛
- عند عرض النموذج (قبل الإدخال)، يجب تحديد أحد أزرار الاختيار: ويكفي لذلك إضافة السمة [checked=’checked’] إلى العلامة <input type="radio"> المعنية. ويتم ذلك باستخدام متغيرات ديناميكية:
- [<?= $modèle->checkedOui ?>] في السطر 18؛
- [<?= $modèle->checkedNon ?>] في السطر 24؛
وستكون هذه المتغيرات جزءًا من قالب العرض.
- السطر 37: حقل إدخال رقمي [type="number"] بقيمة دنيا تساوي 0 [min="0"]. في المتصفحات الحديثة، يعني هذا أن المستخدم لن يتمكن من إدخال سوى رقم >=0. في هذه المتصفحات الحديثة نفسها، يمكن الإدخال باستخدام شريط تمرير يمكن النقر عليه لأعلى أو لأسفل. تشير السمة [step="1"] في السطر 37 إلى أن شريط التمرير سيعمل بخطوات مقدارها 1 وحدة. ونتيجة لذلك، لن يقبل شريط التمرير سوى الأعداد الصحيحة التي تتراوح من 0 إلى n بخطوة مقدارها 1. بالنسبة للإدخال اليدوي، يعني ذلك أن الأرقام التي تحتوي على فاصلة عشرية لن تُقبل؛
![]()
- السطر 37: في بعض حالات العرض، يجب أن تكون منطقة إدخال بيانات الأطفال مملوءة مسبقًا بآخر قيمة تم إدخالها في هذه المنطقة. ولذلك، يتم استخدام السمة [value] التي تحدد القيمة المراد عرضها في منطقة الإدخال. وستكون هذه القيمة ديناميكية ويتم إنشاؤها بواسطة المتغير [$modèle→enfants]؛
- السطر 46: نفس التوضيحات المتعلقة بإدخال الراتب تنطبق على إدخال بيانات الأبناء؛
- السطر 53: الزر من النوع [submit] الذي يقوم بتشغيل POST للقيم التي تم إدخالها في URL [main.php?action=calculer-impot]؛

23.13.3.3. المقتطف [v-menu.php]
يعرض هذا الجزء قائمة على يسار نموذج حساب الضريبة:

رمز هذا المقتطف هو التالي:
<!-- قائمة Bootstrap -->
<nav class="nav flex-column">
<?php
// عرض قائمة الروابط HTML
foreach($modèle->optionsMenu as $texte=>$url){
print <<<EOT3
<a class="nav-link" href="$url">$texte</a>
EOT3;
}
?>
</nav>
تعليقات
- الأسطر 2-11: تحيط العلامة HTML [nav] بجزء من المستند HTML الذي يعرض روابط تنقل إلى مستندات أخرى؛
- السطر 7: العلامة HTML [a] تُدرج رابطًا للتنقل:
- [$url]: هي الرابط URL الذي يتم الانتقال إليه عند النقر على الرابط [$texte]. وعندئذٍ يقوم المتصفح بتنفيذ عملية [GET $url]. وإذا كان الرابط [$url] رابطًا نسبيًا لـ URL، فإنه يُسبق ببادئة جذر الرابط URL المعروض حاليًا في عنوان المتصفح. وبالتالي، للحصول على الرابط [1]، في حين أن الرابط الحالي URL في المتصفح هو من النوع [http://chemin/main.php?paramètres]، فسيتم إنشاء الرابط التالي:
- السطر 5: سيكون النموذج [$modèle→optionsMenu] للجزء عبارة عن مصفوفة بالشكل التالي:
[‘ Liste des simulations’=>’main.php?action=liste-simulations’,
‘ Fin de session’=>’main.php?action=fin-session’]
- السطران 2 و7: الفئتان CSS و[nav, flex-column, nav-link] هما فئتان من Bootstrap تحددان مظهر القائمة؛
23.13.3.4. اختبار مرئي
نجمع هذه العناصر المختلفة في المجلد [Tests] وننشئ نموذج اختبار للعرض [vue-calcul-impot.php]:

سيكون نموذج بيانات العرض [vue-calcul-impot] كما يلي:
<?php
// بيانات اختبار الصفحة
//
// يتم حساب قالب العرض
$modèle = getModelForThisView();
function getModelForThisView(): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// نموذج
$modèle->checkedOui = "";
$modèle->checkedNon = 'checked="checked"';
$modèle->enfants = 2;
$modèle->salaire = 300000;
// رسالة النجاح
$modèle->success = TRUE;
$modèle->impôt = "Montant de l'impôt : 1000 euros";
$modèle->décôte = "Décôte : 15 euros";
$modèle->réduction = "Réduction : 20 euros";
$modèle->surcôte = "Surcôte : 0 euros";
$modèle->taux = "Taux d'imposition : 14 %";
// رسالة خطأ
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// يتم إنشاء قائمة HTML بالأخطاء
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// القائمة
$modèle->optionsMenu = [
'قائمة عمليات المحاكاة' => 'main.php?action=قائمة-عمليات-المحاكاة',
'إنهاء الجلسة' => 'main.php?action=fin-session'];
// صورة الشريط العلوي
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// عرض النموذج
return $modèle;
}
?>
<!-- المستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 7-39: نقوم بتهيئة جميع الأجزاء الديناميكية للعرض [vue-calcul-impot.php] والأجزاء [v-calcul-impot.php] و [v-menu.php]؛
يتم اختبار العرض [vue-calcul-impot.php]:

ونحصل على النتيجة التالية:

نعمل على هذه العرض حتى نحصل على النتيجة المرئية التي نريدها. يمكننا بعد ذلك الانتقال إلى دمج العرض في تطبيق الويب الذي نعمل على كتابته.
23.13.3.5. حساب نموذج العرض

بمجرد تحديد الشكل المرئي للواجهة، يمكننا المضي قدمًا في حساب نموذج الواجهة في الظروف الفعلية. دعونا نستعرض رموز الحالة التي تؤدي إلى هذه الواجهة. نجدها في ملف التكوين:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
إذن، رموز الحالة [200, 300, 341, 350, 800] هي التي تعرض شاشة المصادقة. لمعرفة معنى هذه الرموز، يمكن الاستعانة باختبارات [Postman] التي أُجريت على التطبيق jSON:
- [authentifier-utilisateur-200]: 200 هو رمز الحالة عند انتهاء إجراء [authentifier-itilisateur] بنجاح: عندها يتم عرض نموذج حساب الضريبة فارغًا؛
- [calculer-impot-300]: 300 هو رمز الحالة بعد نجاح إجراء [calculer-impot]. عندها يتم عرض نموذج الحساب مع البيانات التي تم إدخالها فيه ومبلغ الضريبة. يمكن للمستخدم عندها إجراء حساب آخر؛
- [fin-session-400]: 400 هو رمز الحالة عند نجاح العملية [fin-session]: وعندها يتم عرض نموذج المصادقة فارغًا؛
- رمز الحالة [341] هو الرمز الذي يتم الحصول عليه في حالة حساب ضريبة صحيح، لكن عدم الاتصال بـ SGBD يؤدي إلى حدوث خطأ؛
- رمز الحالة [350] هو الرمز الذي يتم الحصول عليه عند إجراء حساب ضريبي صحيح، لكن عدم الاتصال بخادم [Redis] يؤدي إلى حدوث خطأ؛
- سيتم عرض رمز الحالة [800] لاحقًا. لم نواجهه بعد؛
- لقد افترضنا هنا أن المستخدم يستخدم متصفحًا حديثًا. وبالتالي، مع النموذج قيد الدراسة، لا يمكن إدخال أرقام سالبة أو سلاسل أحرف غير رقمية أو أرقام عشرية في حقول الإدخال [enfants, salaire]. أما مع المتصفحات الأقدم، فسيكون ذلك ممكنًا. سنعامل هذه الأخطاء على أنها أخطاء غير متوقعة، وسنعرض عندئذٍ العرض [vue-erreurs]؛
الآن بعد أن عرفنا متى يجب عرض نموذج حساب الضريبة، يمكننا حساب نموذجه في [vue-calcul-impot.php]:
<?php
// يتم توريث المتغيرات التالية
// الطلب $request: الطلب الحالي
// الجلسة $session: جلسة التطبيق
// مصفوفة $config: تكوين التطبيق
// مصفوفة $content: استجابة وحدة التحكم التي عالجت الإجراء
//
// التبعيات في Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// يتم حساب نموذج العرض
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// حالة التطبيق
$état = $content["état"];
// يعتمد النموذج على الحالة
switch ($état) {
case 200 :
case 800:
// العرض الأولي لنموذج فارغ
$modèle->success = FALSE; $modèle->errror = FALSE;
$modèle->checkedNon = 'checked="checked"';
$modèle->checkedOui = "";
$modèle->enfants = "";
$modèle->salaire = "";
break;
case 300:
// نجاح الحساب - عرض النتيجة
$modèle->success = TRUE;
$modèle->error = FALSE;
$modèle->impôt = "Montant de l'impôt : {$content["réponse"]["impôt"]} euros";
$modèle->décôte = "Décôte : {$content["réponse"]["décôte"]} euros";
$modèle->réduction = "Réduction : {$content["réponse"]["réduction"]} euros";
$modèle->surcôte = "Surcôte : {$content["réponse"]["surcôte"]} euros";
$modèle->taux = "Taux d'imposition : " . ($content["réponse"]["taux"] * 100) . " %";
// تم استعادة النموذج بالقيم التي تم إدخالها
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
break;
case 341:
// قاعدة البيانات HS
case 350:
// خادم Redis HS
// تم استعادة النموذج بالقيم التي تم إدخالها
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
// خطأ
$modèle->success = FALSE;
$modèle->error = TRUE;
$modèle->erreurs = "<li>{$content["réponse"]}</li>";
break;
}
//قائمة
$modèle->optionsMenu = [
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session"];
// يتم عرض النموذج
return $modèle;
}
?>
<!-- المستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
<title>Application impots</title>
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 22-30: عرض نموذج فارغ؛
- الأسطر 31-45: حالة نجاح حساب الضريبة. يتم إعادة عرض القيم التي تم إدخالها بالإضافة إلى مبلغ الضريبة؛
- الأسطر 46-59: حالة فشل حساب الضريبة بسبب عدم توفر أحد الخادمين [Redis] أو [MySQL]؛
- السطور 62-64: حساب الخيارين في القائمة؛
23.13.3.6. اختبارات [Postman]
يتيح لنا الاختبار [calculer-impot-300] الحصول على رمز الحالة 300. وهو ما يعني نجاح حساب الضريبة:

- في [3]، القيم التي أدت إلى النتيجة [2]؛
لنحاول حالة خطأ: الخطأ [350] بسبب عدم توفر الخادم [Redis]:

23.13.4. عرض قائمة عمليات المحاكاة
23.13.4.1. عرض القائمة
العرض الذي يعرض قائمة المحاكاة هو التالي:

تتكون طريقة العرض التي تم إنشاؤها بواسطة البرنامج النصي [vue-liste-simulations] من ثلاثة أجزاء:
- 1: الشريط العلوي الذي تم إنشاؤه بواسطة المقطع [v-bandeau.php] الذي سبق عرضه؛
- 2: جدول عمليات المحاكاة الذي تم إنشاؤه بواسطة المقطع [v-liste-simulations.php]؛
- 3: قائمة تحتوي على رابطين، تم إنشاؤها بواسطة المقطع [v-menu.php]؛
يتم إنشاء عرض عمليات المحاكاة بواسطة البرنامج النصي [vue-liste-simulations.php] التالي:

<?php
// يتم حساب نموذج العرض
$modèle = getModelForThisView();
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// يتم تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
…
// يتم عرض النموذج
return $modèle;
}
?>
<!-- مستند HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- علامات الميتا المطلوبة -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- شريط رأس الصفحة -->
<?php require "v-bandeau.php"; ?>
<!-- تصميم بعمودين -->
<div class="row">
<!-- قائمة من ثلاثة أعمدة-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- قائمة المحاكاة المكونة من 9 أعمدة-->
<div class="col-md-9">
<?php require "v-liste-simulations.php" ?>
</div>
</div>
</div>
</body>
</html>
تعليقات
- السطر 28: تضمين شريط التطبيق [1]؛
- السطر 33: إدراج قائمة [2]. سيتم عرضها في ثلاثة أعمدة أسفل الشريط العلوي؛
- السطر 37: إدراج جدول المحاكاة [3]. سيتم عرضه في تسعة أعمدة أسفل الشريط وأمين القائمة؛
لقد قمنا بالفعل بتعليق اثنين من الأجزاء الثلاثة لهذه الصفحة:
أما الجزء [v-liste-simulations.php] فهو كما يلي:
<!-- رسالة على خلفية زرقاء -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- جدول عمليات المحاكاة -->
<table class="table table-sm table-hover table-striped">
<!-- عناوين الأعمدة الستة في الجدول -->
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Marié</th>
<th scope="col">Nombre d'enfants</th>
<th scope="col">Salaire annuel</th>
<th scope="col">Montant impôt</th>
<th scope="col">Surcôte</th>
<th scope="col">Décôte</th>
<th scope="col">Réduction</th>
<th scope="col">Taux</th>
<th scope="col"></th>
</tr>
</thead>
<!-- نص الجدول (البيانات المعروضة) -->
<tbody>
<?php
$i = 0;
// يتم عرض كل محاكاة بالتنقل عبر جدول المحاكاة
foreach ($modèle->simulations as $simulation) {
// عرض سطر من الجدول المكون من 6 أعمدة - العلامة <tr>
// العمود 1: عنوان الصف (رقم المحاكاة) - العلامة <th scope='row'>
// العمود 2: قيمة المعلمة [marié] - العلامة <td>
// العمود 3: قيمة المعلمة [enfants] - العلامة <td>
// العمود 4: قيمة المعلمة [salaire] - العلامة <td>
// العمود 5: قيمة المعلمة [impôt] (للضريبة) - العلامة <td>
// العمود 6: قيمة المعلمة [surcôte] - العلامة <td>
// العمود 7: قيمة المعلمة [décôte] - العلامة <td>
// العمود 8: قيمة المعلمة [réduction] - العلامة <td>
// العمود 9: قيمة المعلمة [taux] (للضريبة) - العلامة <td>
// العمود 10: رابط حذف المحاكاة - العلامة <td>
print <<<EOT
<tr>
<th scope="row">$i</th>
<td>{$simulation["marié"]}</td>
<td>{$simulation["enfants"]}</td>
<td>{$simulation["salaire"]}</td>
<td>{$simulation["impôt"]}</td>
<td>{$simulation["surcôte"]}</td>
<td>{$simulation["décôte"]}</td>
<td>{$simulation["réduction"]}</td>
<td>{$simulation["taux"]}</td>
<td><a href="main.php?action=supprimer-simulation&numéro=$i">Supprimer</a></td>
</tr>
EOT;
$i++;
}
?>
</tr>
</tbody>
</table>
تعليقات
- تم إنشاء جدول HTML باستخدام العلامة <table> (السطران 6 و58)؛
- يتم إنشاء عناوين أعمدة الجدول داخل علامة <thead> (رأس الجدول، السطران 8 و21). تحدد علامة <tr> (صف الجدول، السطران 9 و20) حدود الصف. في الأسطر 10-15، تحدد علامة <th> (رأس الجدول) عنوان العمود. وبالتالي، هناك عشرة منها. تشير [scope="col"] إلى أن العنوان ينطبق على العمود. تشير [scope="row"] إلى أن العنوان ينطبق على الصف؛
- الأسطر 23-57: العلامة <tbody> تحدد البيانات المعروضة في الجدول؛
- السطور 40-51: العلامة <tr> تحدد حدود سطر من الجدول؛
- السطر 41: العلامة <th scope=’row’> تحدد رأس السطر؛
- الأسطر 42-50: تحدد كل علامة <td> عمودًا في السطر؛
- السطر 27: توجد قائمة عمليات المحاكاة في النموذج [$modèle→simulations] وهو جدول ترابطي؛
- السطر 50: رابط لحذف المحاكاة. يستمد النموذج URL الرقم المعروض في العمود الأول من الجدول (السطر 41)؛
23.13.4.2. اختبار بصري
نجمع هذه العناصر المختلفة في المجلد [Tests] وننشئ نموذج اختبار للعرض [vue-liste-simulations.php]:

سيكون نموذج بيانات العرض [vue-liste-simulations] كما يلي:
<?php
// يتم حساب نموذج العرض
$modèle = getModelForThisView();
function getModelForThisView(): object {
// يتم تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// يتم وضع المحاكاة في التنسيق المطلوب للصفحة
$modèle->simulations = [
[
"marié" => "oui",
"enfants" => 2,
"salaire" => 60000,
"impôt" => 448,
"décôte" => 100,
"réduction" => 20,
"surcôte" => 0,
"taux" => 0.14
],
[
"marié" => "non",
"enfants" => 2,
"salaire" => 200000,
"impôt" => 25600,
"décôte" => 0,
"réduction" => 0,
"surcôte" => 8400,
"taux" => 0.45
]
];
// خيارات القائمة
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// صورة الشريط العلوي
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// يتم عرض النموذج
return $modèle;
}
?>
<!-- مستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 9-30: جدول المحاكاة المعروض بواسطة الجدول HTML؛
- الأسطر 32-34: جدول خيارات القائمة؛
دعونا نعرض هذه العرضة:

نحصل على النتيجة التالية:

نواصل العمل على هذه الواجهة حتى نصل إلى النتيجة المرئية التي نرضى عنها. يمكننا بعد ذلك الانتقال إلى دمج الواجهة في تطبيق الويب الذي نقوم بكتابته حالياً.
23.13.4.3. حساب نموذج العرض

بمجرد تحديد الشكل المرئي للواجهة، يمكننا المضي قدمًا في حساب نموذج الواجهة في الظروف الفعلية. دعونا نستعرض رموز الحالة التي تؤدي إلى هذه الواجهة. نجدها في ملف التكوين:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
إذن، رموز الحالة [500, 600] هي التي تعرض عرض المحاكاة. لمعرفة معنى هذه الرموز، يمكن الاستعانة باختبارات [Postman] التي أجريت على التطبيق jSON:
- [lister-simulations-500]: 500 هو رمز الحالة بعد نجاح إجراء [lister-simulations]: يتم عندئذ عرض قائمة المحاكاة التي أجراها المستخدم؛
- [supprimer-simulation-600]: 600 هو رمز الحالة عند نجاح إجراء [supprimer-simulation]. وعندها يتم عرض القائمة الجديدة للمحاكاة التي تم الحصول عليها بعد عملية الحذف هذه؛
والآن بعد أن عرفنا متى يجب عرض قائمة عمليات المحاكاة، يمكننا حساب نموذجها في [vue-liste-simulations.php]:
<?php
// يتم توريث المتغيرات التالية
// الطلب $request: الطلب الحالي
// الجلسة $session: جلسة التطبيق
// مصفوفة $config: تكوين التطبيق
// مصفوفة $content: استجابة وحدة التحكم
// لا توجد أخطاء محتملة
// array $content: استجابة وحدة التحكم
//
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// يتم حساب نموذج العرض
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// يتم وضع المحاكاة في التنسيق الذي تتوقعه الصفحة
// توجد هذه البيانات في استجابة وحدة التحكم التي نفذت الإجراء
// في شكل مصفوفة من الكائنات من النوع [Simulation]
$objetsSimulation = $content["réponse"];
// سيتم تحويل كل كائن من نوع [Simulation] إلى مصفوفة ترابطية
$modèle->simulations = [];
foreach ($objetsSimulation as $objetSimulation) {
$modèle->simulations[] = [
"marié" => $objetSimulation->getMarié(),
"enfants" => $objetSimulation->getEnfants(),
"salaire" => $objetSimulation->getSalaire(),
"impôt" => $objetSimulation->getImpôt(),
"surcôte" => $objetSimulation->getSurcôte(),
"décôte" => $objetSimulation->getdécôte(),
"réduction" => $objetSimulation->getRéduction(),
"taux" => $objetSimulation->getTaux()
];
}
// خيارات القائمة
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// نقوم بتحويل النموذج
return $modèle;
}
?>
<!-- مستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 26-36: حساب النموذج [$modèle→simulations] المستخدم من قبل الجزء [v-liste-simulations.php]؛
- الأسطر 39-41: حساب النموذج [$modèle→optionsMenu] المستخدم من قبل الجزء [v-menu.php]؛
23.13.4.4. اختبارات [Postman]
يتيح لنا الاختبار [lister-simulations-500] الحصول على رمز الحالة 500. وهو يتوافق مع طلب لعرض المحاكاة:

يتيح لنا الاختبار [supprimer-simulation-600] الحصول على رمز الحالة 600. وهو يتوافق مع الحذف الناجح للمحاكاة رقم 0. والنتيجة التي يتم إرجاعها هي قائمة بالمحاكاة بعد حذف محاكاة واحدة منها:

23.13.5. عرض الأخطاء غير المتوقعة
نسمي هنا بالخطأ غير المتوقع أي خطأ لم يكن من المفترض أن يحدث في إطار الاستخدام العادي للتطبيق على الويب.
لنأخذ على سبيل المثال الاختبار [Postman] [calculer-impot-3xx] المُعرَّف على النحو التالي:

- في [1-3]، طلب POST مع الإجراء [calculer-impot]؛
- إلى [4-6]: هنا يمكننا تعريف ما نريد للمعلمات الثلاثة لـ POST:
- [4]: المعلمة [marié] مفقودة؛
- [5-6]: المعلمات [enfants, salaire] موجودة ولكنها غير صالحة؛
- في النموذج [9]، يتم الإبلاغ عن هذه الأخطاء الثلاثة برمز الحالة 338؛
ولكن في النموذج HTML الخاص بالتطبيق الويب، لا يمكن أن تحدث هذه الحالة:
- فجميع المعلمات موجودة؛
- المعلمة [marié] التي تأخذ قيمتها من سمات [value] الخاصة بزرين اختيار، لا بد أن تكون إحدى القيمتين [oui] أو [non]؛
- باستخدام متصفح حديث، تضمن سمات <input type=’number’ min=’0’ step=’1’ …> أن تكون القيم المدخلة للأطفال والراتب أعدادًا صحيحة >=0؛
ومع ذلك، لا شيء يمنع المستخدم من إدخال [Postman] وإرسال الاختبار [calcul-impot-3xx] المذكور أعلاه إلى خادمنا. وقد رأينا أن تطبيق الويب الخاص بنا كان قادرًا على الاستجابة بشكل صحيح لهذا الطلب. سنطلق على «الخطأ غير المتوقع» اسم الخطأ الذي لا ينبغي أن يحدث في سياق تطبيق HTML. إذا حدث ذلك، فهذا يعني على الأرجح أن شخصًا ما يحاول «اختراق» التطبيق. ولأغراض تعليمية، قررنا عرض صفحة أخطاء لهذه الحالات. في الواقع، يمكننا إعادة عرض آخر صفحة تم إرسالها إلى العميل. ويكفي لذلك تسجيل آخر استجابة HTML التي تم إرسالها في جلسة العمل. وفي حالة حدوث خطأ غير متوقع، نقوم بإعادة إرسال هذه الاستجابة. وبذلك سيشعر المستخدم بأن الخادم لا يستجيب لأخطائه، حيث إن الصفحة المعروضة لا تتغير.
23.13.5.1. عرض الصفحة
الطريقة التي تعرض الأخطاء غير المتوقعة هي كما يلي:

تتكون الصفحة التي تم إنشاؤها بواسطة البرنامج النصي [vue-erreurs.php] من ثلاثة أجزاء:
- 1: يتم إنشاء الشريط العلوي بواسطة الجزء [v-bandeau.php] الذي تم عرضه سابقًا؛
- 2: الخطأ أو الأخطاء غير المتوقعة؛
- 3: قائمة تحتوي على ثلاثة روابط، تم إنشاؤها بواسطة المقطع [v-menu.php]؛
يتم إنشاء عرض الأخطاء غير المتوقعة بواسطة البرنامج النصي [vue-erreurs.php] التالي:

<?php
// يتم حساب نموذج العرض
$modèle = getModelForThisView();
function getModelForThisView(): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
…
// يتم إرجاع النموذج
return $modèle;
}
?>
<!-- المستند HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- علامات الميتا المطلوبة -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- شريط علوي مكون من 12 عمودًا -->
<?php require "v-bandeau.php"; ?>
<!-- سطر من عمودين -->
<div class="row">
<!-- قائمة مكونة من 3 أعمدة-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- قائمة الأخطاء -->
<div class="col-md-9">
<?php
print <<<EOT
<div class="alert alert-danger" role="alert">
Les erreurs inattendues suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
EOT;
?>
</div>
</div>
</div>
</body>
</html>
تعليقات
- السطر 27: تضمين شريط رأس التطبيق [1]؛
- السطر 32: تضمين قائمة [2]. سيتم عرضها في ثلاثة أعمدة أسفل الشريط العلوي؛
- الأسطر 34-44: عرض منطقة الأخطاء على تسعة أعمدة؛
- الأسطر 37-44: العملية [print] التي تعرض الأخطاء غير المتوقعة؛
- السطر 38: سيتم هذا العرض في إطار Bootstrap بخلفية وردية؛
- السطر 39: نص تعريفي؛
- السطر 40: العلامة <ul> تحيط بقائمة نقطية. يتم توفير هذه القائمة النقطية بواسطة النموذج [$modèle->erreurs]؛
لقد قمنا بالفعل بتعليق جزأين من هذه الصفحة:
- [v-bandeau.php]: في الفقرة التي تحتوي على الرابط؛
- [v-menu.php]: في الفقرة التي تحتوي على الرابط؛
23.13.5.2. اختبار بصري
نجمع هذه العناصر المختلفة في المجلد [Tests] وننشئ نموذج اختبار للعرض [vue-erreurs.php]:

سيكون نموذج بيانات العرض [vue-erreurs.php] كما يلي:
<?php
// يتم حساب نموذج العرض
$modèle = getModelForThisView();
function getModelForThisView(): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// جدول الأخطاء غير المتوقعة
$erreurs = ["erreur1", "erreur2"];
// يتم إنشاء قائمة HTML بالأخطاء
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// خيارات القائمة
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// صورة الشريط العلوي
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// إرجاع النموذج
return $modèle;
}
?>
<!-- المستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 9-15: إنشاء قائمة HTML بالأخطاء؛
- الأسطر 17-20: جدول خيارات القائمة؛
دعونا نعرض هذه العرض:

ونحصل على النتيجة التالية:

نعمل على هذه الواجهة حتى نصل إلى النتيجة المرئية التي ترضينا. يمكننا بعد ذلك الانتقال إلى دمج الواجهة في تطبيق الويب الذي نقوم بكتابته حالياً.
23.13.5.3. حساب نموذج العرض

بمجرد تحديد الشكل المرئي للمنظر، يمكن المضي قدمًا في حساب نموذج المنظر في الظروف الفعلية. دعونا نستعرض رموز الحالة التي تؤدي إلى هذا المنظر. يمكن العثور عليها في ملف التكوين:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
إذن، رموز الحالة التي لا توجد في رموز الأسطر [2-4] هي التي تعرض عرض الأخطاء غير المتوقعة.
رمز حساب نموذج العرض [vue-erreurs.php] هو التالي:
<?php
// يتم توريث المتغيرات التالية
// الطلب $request: الطلب الحالي
// الجلسة $session: جلسة التطبيق
// مصفوفة $config: تكوين التطبيق
// مصفوفة $content: استجابة وحدة التحكم
//
// تبعيات Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// يتم حساب نموذج العرض
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// تغليف بيانات الصفحة في $modèle
$modèle = new \stdClass();
// استرداد الأخطاء من استجابة وحدة التحكم
$réponse = $content["réponse"];
if (!is_array($réponse)) {
// رسالة خطأ واحدة
$erreurs = [$réponse];
} else {
// عدة رسائل خطأ
$erreurs = $réponse;
}
// يتم إنشاء قائمة HTML بالأخطاء
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// خيارات القائمة
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// إرجاع النموذج
return $modèle;
}
?>
<!-- مستند HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
تعليقات
- الأسطر 19-32: حساب النموذج [$modèle→erreurs] المستخدم في عرض [vue-erreurs.php]؛
- الأسطر 34-37: حساب النموذج [$modèle→optionsMenu] المستخدم من قبل الجزء [v-menu.php]؛
23.13.5.4. اختبارات [Postman]
يتيح لنا الاختبار [calculer-impot-3xx] الحصول على رمز الحالة 338، وهو رمز حالة غير متوقع. وتكون الاستجابة HTML كما يلي:

23.13.6. تنفيذ إجراءات قائمة التطبيق
سنتناول هنا تنفيذ إجراءات القائمة. دعونا نستعرض معنى الروابط التي صادفناها
عرض | الرابط | الهدف | الدور |
حساب الضريبة | [Liste des simulations] | [main.php?action=lister-simulations] | طلب قائمة عمليات المحاكاة |
[Fin de session] | |||
قائمة عمليات المحاكاة | [Calcul de l’impôt] | [main.php?action=afficher-calcul-impot] | عرض طريقة حساب الضريبة |
[Fin de session] | |||
أخطاء غير متوقعة | [Calcul de l’impôt] | [main.php?action=afficher-calcul-impot] | عرض طريقة حساب الضريبة |
[Liste des simulations] | |||
[Fin de session] |
تجدر الإشارة إلى أن النقر على رابط يؤدي إلى GET نحو هدف الرابط. تم تنفيذ الإجراءات [lister-simulations, fin-session] باستخدام عملية GET، مما يسمح لنا بتعيينها كأهداف للروابط. وعندما يتم تنفيذ الإجراء عبر POST، لا يصبح استخدام الرابط ممكنًا إلا إذا تم ربطه بجافا سكريبت.
من الإجراءات المذكورة أعلاه، يتضح أن الإجراء [afficher-calcul-impot] لم يتم تنفيذه بعد. إنها عملية تنقل بين عرضين: لا يوجد سبب يدعو الخادم jSON أو XML لتنفيذها لأنهما لا يتعاملان مع مفهوم العرض. الخادم HTML هو الذي يقدم هذا المفهوم.
لذلك، يتعين علينا تنفيذ الإجراء [afficher-calcul-impot]. سيسمح لنا ذلك بمراجعة طريقة تنفيذ إجراء ما داخل الخادم.
أولاً، يتعين علينا إضافة وحدة تحكم ثانوية جديدة. سنسميها [AfficherCalculImpotController]:

يجب إضافة وحدة التحكم هذه إلى ملف التكوين [config.json]:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
…
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
…
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
…
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
- السطر 15: وحدة التحكم الجديدة؛
- السطر 30: الإجراء الجديد ووحدة التحكم الخاصة به؛
- السطر 35: ستُرجع وحدة التحكم الجديدة رمز الحالة 800. عند تغيير العرض، لا يمكن أن يحدث أي خطأ؛
سيكون وحدة التحكم [AfficherCalculImpotController.php] كما يلي:
<?php
namespace Application;
// التبعيات الخاصة بـ Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Response;
class AfficherCalculImpotController implements InterfaceController {
// $config هي تهيئة التطبيق
// معالجة طلب Request
// يستخدم الجلسة (Session) ويمكنه تعديلها
// $infos هي معلومات إضافية خاصة بكل وحدة تحكم
// تُرجع مصفوفة [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// تغيير العرض - مجرد رمز حالة يجب تعيينه
return [Response::HTTP_OK, 800, ["réponse" => ""], []];
}
}
تعليقات
- السطر 10: مثل وحدات التحكم الثانوية الأخرى، تنفذ وحدة التحكم الجديدة الواجهة [InterfaceController]؛
- تغييرات العرض سهلة التنفيذ: يكفي جعل رمز الحالة مرتبطًا بالعرض المستهدف، وهنا الرمز 800 كما رأينا أعلاه؛
23.13.7. الاختبارات في ظروف حقيقية
تمت كتابة الكود واختبار كل إجراء باستخدام [Postman]. ولم يتبق لنا سوى اختبار تسلسل الصفحات في ظروف حقيقية. نحتاج إلى وسيلة لتهيئة الجلسة HTML. نعلم أنه يتعين إرسال المعلمات [action=init-session&type=html] إلى الخادم. لتجنب الحاجة إلى كتابتها في حقل العنوان بالمتصفح، سنقوم بإضافة البرنامج النصي [index.php] إلى تطبيقنا:

سيكون البرنامج النصي [index.php] كما يلي:
<?php
// إعادة التوجيه إلى [main.php] في وضع [html]
header('Location: main.php?action=init-session&type=html');
- السطر 4: [header] هي دالة PHP تضيف رأسًا HTTP إلى الاستجابة. يطلب الرأس HTTP [Location: main.php?action=init-session&type=html] من متصفح العميل إعادة التوجيه إلى الهدف URL المحدد في [Location]. يتم طلب البرنامج النصي [index.php] مع URL و[http://localhost/php7/scripts-web/impots/version-12/index.php]. عندما يتلقى متصفح العميل إعادة التوجيه إلى URL النسبي [main.php?action=init-session&type=html]، سيطلب URL المطلق [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html] وستبدأ جلسة HTML؛
يمكن تبسيط عنوان البدء URL إلى [http://localhost/php7/scripts-web/impots/version-12/]. في حالة عدم تحديد أي صفحة في URL، يتم استخدام صفحات [index.html, index.php] بشكل افتراضي. هنا سيتم استخدام البرنامج النصي [index.php]؛
لنبدأ: نقدم الآن بعض تسلسلات العروض.
في متصفحنا، نقوم بتنشيط تتبع الطلبات (F12 في Firefox) ونطلب URL لبدء تشغيل [https://localhost/php7/scripts-web/impots/version-12/]:

- في [4]، يكون الرد الأول من الخادم هو إعادة توجيه 302:
- في [5]، يتم إرسال طلب جديد إلى URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html]؛
لنلقِ نظرة عن كثب على إعادة التوجيه 302:

- في [8]، الرمز HTTP [302] هو رمز إعادة توجيه: يُبلغ متصفح العميل أن URL المطلوب قد تم نقله. يتم تحديد URL الجديد في [9]. سيتبع المتصفح هذا التوجيه بطلب جديد GET:

- إلى [12-13]، وهو الطلب الجديد الذي أرسله المتصفح؛
لنملأ النموذج الذي تلقيناه؛

ثم نقوم ببعض عمليات المحاكاة:


لنطلب قائمة المحاكاة:

لنحذف المحاكاة الأولى:

لننهي الجلسة:

يُرجى من القارئ إجراء اختبارات أخرى.
23.14. عميل خدمة الويب jSON
23.14.1. بنية العميل/الخادم

ننتقل الآن إلى العميل jSON [A] لخدمة الويب [B]. يتميز العميل [A]، مثل خدمة الويب [B]، بهيكل متعدد الطبقات:

وتنعكس هذه البنية في تنظيم الكود التالي:

تمت مناقشة معظم الفئات وشرحها سابقًا:
فقرة رابط. | |
رابط الفقرة. | |
رابط الفقرة. | |
رابط الفقرة. | |
رابط الفقرة. | |
رابط الفقرة. |
23.14.2. الطبقة [dao]

23.14.2.1. Interface
ستكون واجهة الطبقة [dao] كما يلي [InterfaceClientDao.php]:
<?php
// مساحة الأسماء
namespace Application;
interface InterfaceClientDao {
// قراءة بيانات دافعي الضرائب
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// حساب الضرائب المستحقة على أحد المكلفين
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// تسجيل النتائج
public function saveResults(string $resultsFilename, array $simulations): void;
// التوثيق
public function authentifierUtilisateur(String $user, string $password): void;
// قائمة عمليات المحاكاة
public function listerSimulations(): array;
// حذف محاكاة
public function supprimerSimulation(int $numéro): array;
// بدء الجلسة
public function initSession(string $type = 'json'): void;
// إنهاء الجلسة
public function finSession(): void;
}
تعليقات
- السطر 9: تتيح الطريقة [getTaxPayersData] استخراج ملف jSON الذي يحتوي على بيانات دافعي الضرائب. يتم تنفيذ هذه الطريقة بواسطة السمة [TraitDao] التي سبق التعليق عليها (الفقرة «الرابط»);
- السطر 15: تسمح الطريقة [saveResults] بحفظ نتائج عدة حسابات ضريبية في ملف jSON. وهنا أيضًا، يتم تنفيذ هذه الطريقة بواسطة السمة [TraitDao] التي سبق التعليق عليها (الفقرة الرابط)؛
- السطور 12 و18 و21 و27 و30: تم إنشاء طريقة لكل إجراء من الإجراءات التي يقبلها خدمة الويب؛
23.14.2.2. Implémentation
يتم تنفيذ الواجهة [InterfaceClientDao] بواسطة الفئة [ClientDao] التالية:
<?php
namespace Application;
// التبعيات
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;
class ClientDao implements InterfaceClientDao {
// استخدام سمة
use TraitDao;
// السمات
private $urlServer;
private $sessionCookie;
private $verbose;
// المنشئ
public function __construct(string $urlServer, bool $verbose = TRUE) {
$this->urlServer = $urlServer;
$this->verbose = $verbose;
}
…
}
تعليقات
- الأسطر 18-21: يتلقى المنشئ معلمتين:
- URL [$urlServer] من خدمة الويب jSON؛
- قيمة منطقية [$verbose] تشير إلى أن الفئة يجب أن تعرض استجابات الخادم على وحدة التحكم؛
- السطر 14: ملف تعريف الارتباط للجلسة. تم وصف دوره في الإصدار 09 من العميل (الفقرة الرابط)؛
- السطر 11: تستخدم الفئة السمة [TraitDao] التي تنفذ طريقتين من الواجهة:
- [getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array]؛
- [function calculerImpot(string $marié, int $enfants, int $salaire): Simulation]؛
23.14.2.2.1. Méthode [initSession]
يتم تنفيذ الطريقة [initSession] على النحو التالي:
public function initSession(string $type = 'json'): void {
// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create();
// إرسال الطلب إلى الخادم دون مصادقة
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "init-session",
"type" => $type
],
"verify_peer" => false
]);
// يتم استرداد الرد
$this->getResponse($response);
// استرداد ملف تعريف الارتباط الخاص بالجلسة
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// ملف تعريف ارتباط الجلسة؟
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
نظرًا لأن الإجراء [init-session] يجب أن يكون الإجراء الأول المطلوب من خدمة الويب، فإن الطريقة [initSession] ستكون أول طريقة في الطبقة [dao] يتم استدعاؤها.
تعليقات
- السطر 1: يتم تمرير نوع الجلسة المطلوبة كمعلمة. في حالة عدم وجود معلمة، سيتم بدء جلسة jSON؛
- الأسطر 5-11: يتم إرسال طلب GET إلى خدمة الويب؛
- السطران 7-8: المعلمتان الخاصتان بـ GET؛
- السطر 10: في حالة التبادلات الآمنة (نموذج https)، لن يتم التحقق من شهادة الأمان المرسلة من خدمة الويب؛
- السطر 13: تسترد الطريقة [getResponse] الاستجابة من الخادم. وتُرجعها في شكل مصفوفة. هنا، لا يتم استخدام نتيجة هذه الطريقة. تطلق الطريقة [getResponse] استثناءً إذا كان الرمز HTTP في استجابة خدمة الويب مختلفًا عن 200 OK؛
- الأسطر 14-25: نظرًا لأن الطريقة [initSession] هي أول طريقة في الطبقة [dao] يتم تنفيذها، يتم استرداد ملف تعريف الارتباط الخاص بالجلسة حتى تتمكن الطرق التالية من إعادته إلى خدمة الويب. وقد تم توضيح هذا الرمز بالفعل في الإصدار 09؛
23.14.2.2.2. الطريقة [getResponse]
تتولى الطريقة [getResponse] مهمة استغلال استجابة خدمة الويب:
private function getResponse(CurlResponse $response) {
// يتم استرداد الرد
$json = $response->getContent(false);
// السجلات
if ($this->verbose) {
print "$json\n";
}
// يتم استرداد حالة الرد
$statusCode = $response->getStatusCode();
// خطأ؟
if ($statusCode !== 200) {
// هناك خطأ
throw new ExceptionImpots($json);
}
// إرسال الرد
$array = json_decode($json, true);
return $array["réponse"];
}
تعليقات
- السطر 1: الطريقة خاصة؛
- السطر 1: معلمة الطريقة هي استجابة خدمة الويب من النوع [Symfony\Component\HttpClient\Response\CurlResponse]، وهو نوع استجابة Symfony، عندما يتم تنفيذ [HttpClient] بواسطة [CurlClient]، أي بواسطة المكتبة [curl]؛
- السطر 3: يتم استرداد الاستجابة jSON من الخادم. تجدر الإشارة إلى أن المعلمة [false] موجودة لمنع Symfony من إثارة استثناء عندما يكون حالة استجابة الخادم HTTP ضمن النطاق [3xx, 4xx, 5xx]؛
- الأسطر 5-7: إذا كنا في الوضع [$verbose]، فإننا نعرض استجابة الخادم على وحدة التحكم؛
- الأسطر 9-14: إذا كان حالة استجابة الخادم HTTP مختلفة عن 200، يتم إثارة استثناء مع رسالة الخطأ التي تمثل استجابة الخادم jSON؛
- السطر 16: يتم فك تشفير السلسلة jSON في مصفوفة؛
- السطر 17: المعلومات المفيدة موجودة في [$array["réponse"]]؛
23.14.2.2.3. الطريقة [authentifierUtilisateur]
طريقة [authentifierUtilisateur] هي كما يلي:
public function authentifierUtilisateur(string $user, string $password): void {
// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create();
// إرسال الطلب إلى الخادم مع المصادقة
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "authentifier-utilisateur"
],
"body" => [
"user" => $user,
"password" => $password
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// يتم استرداد الرد
$this->getResponse($response);
}
تعليقات
- السطر 5: طلب العميل هو POST؛
- الأسطر 6-8: المعلمات في URL؛
- الأسطر 9-12: معلمات POST؛
- السطر 14: ملف تعريف الارتباط الخاص بالجلسة؛
- السطر 17: نقرأ الرد. نعلم أنه في حالة حدوث خطأ (رمز HTTP مختلف عن 200)، فإن الطريقة [getResponse] تطلق استثناءً بنفسها؛
23.14.2.2.4. الطريقة [calculerImpot]
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create();
// يتم إرسال الطلب إلى الخادم بدون مصادقة ولكن باستخدام ملف تعريف ارتباط الجلسة
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "calculer-impot"],
"body" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// يتم استرداد الرد
$array = $this->getResponse($response);
return (new Simulation())->setFromArrayOfAttributes($array);
}
تعليقات
- السطران 6-7: المعلمة الوحيدة لـ URL؛
- السطور 8-12: المعلمات الثلاثة للطريقة POST (السطر 5)؛
- السطر 17: يتم معالجة الاستجابة؛
- السطر 18: إذا وصلنا إلى هنا، فهذا يعني أن الأسلوب [getResponse] لم يطلق استثناءً. نُرجع كائن [Simulation] مُهيأً بالمصفوفة التي أرجعها [getResponse]؛
23.14.2.2.5. الطريقة [listerSimulations]
public function listerSimulations(): array {
// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create();
// يتم إرسال الطلب إلى الخادم دون مصادقة ولكن باستخدام ملف تعريف ارتباط الجلسة
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "lister-simulations"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// يتم استرداد الرد
return $this->getSimulations($response);
}
تعليقات
- السطر 5: الطريقة GET؛
- الأسطر 6-8: المعلمة الوحيدة لـ GET؛
- السطر 13: يتم إسناد استرداد عمليات المحاكاة إلى الطريقة الخاصة [getSimulations]؛
23.14.2.2.6. الطريقة [getSimulations]
private function getSimulations(CurlResponse $response): array {
// يتم استرداد الرد JSON
$array = $this->getResponse($response);
// لدينا مصفوفة من الكائنات الترابطية
// سنحولها إلى مصفوفة من كائنات Simulation
$simulations = [];
foreach ($array as $simulation) {
$simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
}
// نقوم بعرض قائمة كائنات المحاكاة
return $simulations;
}
تعليقات
- السطر 3: يتم استرداد المصفوفة الناتجة عن الاستجابة. وهي عبارة عن مصفوفة من المصفوفات، حيث تحتوي كل مصفوفة منها على جميع سمات كائن [Simulation]؛
- السطر 6: إذا وصلنا إلى هنا، فهذا يعني أن الطريقة [getResponse] لم تطلق استثناءً؛
- الأسطر 6-9: نستغل الرد لإنشاء مصفوفة من كائنات [Simulation]؛
- السطر 11: يتم إرجاع هذا المصفوف؛
23.14.2.2.7. الطريقة [SupprimerSimulation]
public function supprimerSimulation(int $numéro): array {
// نقوم بإنشاء عميل HTTP
$httpClient = HttpClient::create();
// نرسل الطلب إلى الخادم بدون مصادقة ولكن باستخدام ملف تعريف الارتباط الخاص بالجلسة
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "supprimer-simulation",
"numéro" => $numéro
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// استرداد الرد
return $this->getSimulations($response);
}
تعليقات
- السطر 5: يتم إجراء استعلام GET؛
- الأسطر 6-9: المعلمتان الخاصتان بـ URL؛
- السطر 14: بعد الحذف، يعرض الخادم الجدول الجديد للمحاكاة. يتم عرض هذا الجدول؛
23.14.2.2.8. الطريقة [finSession]
تنتهي جلسة العمل مع خدمة الويب عادةً باستدعاء الطريقة [finSession]:
public function finSession(): void {
// يتم إنشاء عميل HTTP
$httpClient = HttpClient::create();
// يتم إرسال الطلب إلى الخادم دون مصادقة ولكن باستخدام ملف تعريف ارتباط الجلسة
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "fin-session"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// يتم استرداد الرد
$this->getResponse($response);
}
تعليقات
- السطر 5: يتم إجراء استعلام GET؛
- الأسطر 6-8: المعلمة الوحيدة لـ URL؛
- السطر 13: يتم قراءة الرد. سيتم إلقاء استثناء إذا كان رمز الرد HTTP مختلفًا عن 200؛
23.14.3. الطبقة [métier]

23.14.3.1. L’interface
واجهة الطبقة [métier] هي كما يلي [InterfaceClientMetier.php]:
<?php
// مساحة الأسماء
namespace Application;
interface InterfaceClientMetier {
// حساب الضرائب الخاصة بأحد دافعي الضرائب
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// حساب الضرائب في الوضع الدفعي
public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;
// المصادقة
public function authentifierUtilisateur(String $user, string $password): void;
// قائمة عمليات المحاكاة
public function listerSimulations(): array;
// تسجيل النتائج
public function saveResults(string $resultsFilename, array $simulations): void;
// حذف محاكاة
public function supprimerSimulation(int $numéro): array;
// بدء الجلسة
public function initSession(string $type = 'json'): void;
// إنهاء الجلسة
public function finSession(): void;
}
تعليقات
- الطريقة [executeBatchImpots] في السطر 12 هي الوحيدة الخاصة بطبقة [métier]. أما جميع الطرق الأخرى فتنتمي إلى طبقة [dao] التي تقوم بتنفيذها؛
23.14.3.2. الفئة [ClientMetier]
الفئة التي تنفذ الطبقة [métier] هي التالية:
<?php
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// السمة
private $clientDao;
// الشركة المصنعة
public function __construct(InterfaceClientDao $clientDao) {
$this->clientDao = $clientDao;
}
// حساب الضريبة
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// حساب الضرائب في الوضع الدفعي
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// السماح بإعادة توجيه الاستثناءات القادمة من الطبقة [dao]
// استرداد بيانات دافعي الضرائب
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// جدول النتائج
$simulations = [];
// يتم تحليلها
foreach ($taxPayersData as $taxPayerData) {
// يتم حساب الضريبة
$simulations [] = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
}
// تسجيل النتائج
if ($resultsFileName !== NULL) {
$this->clientDao->saveResults($resultsFileName, $simulations);
}
}
public function authentifierUtilisateur(String $user, string $password): void {
$this->clientDao->authentifierUtilisateur($user, $password);
}
public function listerSimulations(): array {
return $this->clientDao->listerSimulations();
}
public function saveResults(string $resultsFilename, array $simulations): void {
$this->clientDao->saveResults($resultsFilename, $simulations);
}
public function supprimerSimulation(int $numéro): array {
return $this->clientDao->supprimerSimulation($numéro);
}
public function finSession(): void {
$this->clientDao->finSession();
}
public function initSession(string $type = 'json'): void {
$this->clientDao->initSession($type);
}
}
تعليقات
- الأسطر 10-12: لكي يتم إنشاء الطبقة [métier]، تحتاج إلى مرجع إلى الطبقة [dao]؛
- الأسطر 20-38: الطريقة [executeBatchImpots] هي الوحيدة الخاصة بالطبقة [métier]. أما تنفيذ الطرق الأخرى فيفوض المهمة المطلوبة إلى الطرق التي تحمل الأسماء نفسها في الطبقة [dao]؛
- السطر 23: يتم اللجوء إلى الطبقة [dao] للحصول على بيانات دافعي الضرائب في مصفوفة من الكائنات من النوع [TaxPayerData]؛
- السطر 25: يتم تجميع مختلف عمليات المحاكاة المحسوبة في المصفوفة [$simulations]؛
- السطور 27-33: يتم حساب الضريبة لكل دافع ضرائب في الجدول [$taxPayersData]؛
- الأسطر 35-37: يتم حفظ النتائج التي تم الحصول عليها في الجدول [$simulations] في ملف jSON؛
ملاحظة: الطبقة [métier] لا تقوم بأي شيء تقريبًا. يمكننا حذفها ودمج كل شيء في الطبقة [dao].
23.14.4. النص البرمجي الرئيسي

يتم تكوين البرنامج النصي الرئيسي بواسطة الملف [config.json] التالي:
{
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12",
"dependencies": [
"/Entities/BaseEntity.php",
"/Entities/TaxPayerData.php",
"/Entities/Simulation.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Utilitaires.php",
"/Model/InterfaceClientDao.php",
"/Model/TraitDao.php",
"/Model/ClientDao.php",
"/Model/InterfaceClientMetier.php",
"/Model/ClientMetier.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php"
],
"user": {
"login": "admin",
"passwd": "admin"
},
"urlServer": "https://localhost:443/php7/scripts-web/impots/version-12/main.php"
}
النص البرمجي الرئيسي [main.php] هو التالي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare(strict_types = 1);
// مساحة الأسماء
namespace Application;
// إدارة الأخطاء بواسطة PHP
// ini_set("display_errors", "0");
//
// مسار ملف التكوين
define("CONFIG_FILENAME", "../Data/config.json");
// يتم استرداد التكوين
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// تعريف الثوابت
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// التبعيات الخاصة بـ Symfony
use Symfony\Component\HttpClient\HttpClient;
// إنشاء الطبقة [dao]
$clientDao = new ClientDao($config["urlServer"]);
// إنشاء الطبقة [métier]
$clientMetier = new ClientMetier($clientDao);
// حساب الضرائب في الوضع الدفعي
try {
// تهيئة الجلسة
$clientMetier->initSession('json');
// المصادقة
$clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
// حساب الضرائب دون حفظ النتائج
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
// قائمة المحاكاة
$clientMetier->listerSimulations();
// حذف محاكاة
$simulations = $clientMetier->supprimerSimulation(1);
// حفظ النتائج
$clientMetier->saveResults(RESULTS_FILENAME, $simulations);
// إنهاء الجلسة
$clientMetier->finSession();
// إجراء دون مصادقة - يجب أن يؤدي إلى تعطل النظام
$clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
// يتم عرض الخطأ
print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// النهاية
print "Terminé\n";
exit();
تعليقات
- الأسطر 12-16: استخدام ملف التكوين [config.json]؛
- الأسطر 18-26: تحميل جميع التبعيات؛
- الأسطر 28-34: تعريف الثوابت والأسماء المستعارة؛
- الأسطر 36-39: إنشاء الطبقات [dao] و [métier]؛
- السطر 44: تهيئة جلسة jSON؛
- السطر 46: المصادقة لدى الخادم؛
- السطر 48: حساب الضريبة لمجموعة من دافعي الضرائب. لا يتم حفظ النتائج (المعلمة الثانية NULL)؛
- السطر 50: طلب نتائج جميع هذه الحسابات؛
- السطر 52: حذف المحاكاة رقم 1 (الثانية في القائمة)؛
- السطر 54: يتم حفظ عمليات المحاكاة المتبقية؛
- السطر 56: إنهاء الجلسة. وهذا يعني إتلاف ملف تعريف الارتباط الخاص بالجلسة؛
- السطر 58: يتم طلب قائمة عمليات المحاكاة. ونظرًا لإلغاء ملف تعريف الارتباط الخاص بالجلسة، يجب إعادة عملية المصادقة. لذا، يجب أن تظهر استثناءً يفيد بعدم المصادقة؛
الملف [taxpayersdata.json] هو كما يلي:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
يوجد 12 دافع ضرائب، أحدهم غير صحيح. وبذلك يكون المجموع 11 محاكاة. سيتم حذف إحداها. يجب أن يتبقى 10.
بعد تنفيذ البرنامج النصي الرئيسي، يكون الملف jSON [results.json] كما يلي:
[
{
"marié": "oui",
"enfants": "2",
"salaire": "55555",
"impôt": 2814,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "oui",
"enfants": "3",
"salaire": "50000",
"impôt": 0,
"surcôte": 0,
"décôte": 720,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "2",
"salaire": "100000",
"impôt": 19884,
"surcôte": 4480,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "non",
"enfants": "3",
"salaire": "100000",
"impôt": 16782,
"surcôte": 7176,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "3",
"salaire": "100000",
"impôt": 9200,
"surcôte": 2180,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"marié": "oui",
"enfants": "5",
"salaire": "100000",
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "0",
"salaire": "100000",
"impôt": 22986,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "2",
"salaire": "30000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
},
{
"marié": "non",
"enfants": "0",
"salaire": "200000",
"impôt": 64210,
"surcôte": 7498,
"décôte": 0,
"réduction": 0,
"taux": 0.45
},
{
"marié": "oui",
"enfants": "3",
"salaire": "20000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
يوجد بالفعل 10 محاكاة.
يحتوي الملف jSON [errors.json] على المحتوى التالي:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
نتائج وحدة التحكم هي كما يلي (في الوضع التفصيلي، يتم عرض استجابات الخادم jSON على وحدة التحكم):
{"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}
{"action":"authentifier-utilisateur","état":200,"réponse":"Authentification réussie [admin, admin]"}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"lister-simulations","état":500,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"supprimer-simulation","état":600,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"fin-session","état":400,"réponse":"session supprimée"}
{"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Une erreur s'est produite : {"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Terminé
23.14.5. اختبارات [Codeception]
كما هو الحال مع العملاء السابقين، يمكن إخضاع عميل الإصدار 12 لاختبارات [Codeception]:

يشبه كود فئة الاختبار الخاصة بطبقة [métier] في العميل كود فئات الاختبار الخاصة بالعملاء السابقين:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// تعريف الثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12");
// مسار ملف التكوين
define("CONFIG_FILENAME", ROOT . "/Data/config.json");
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// التبعيات المطلقة (مكتبات خارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// التبعيات الخاصة بـ Symfony
use Symfony\Component\HttpClient\HttpClient;
// فئة الاختبار
class ClientDaoTest extends \Codeception\Test\Unit {
// طبقة DAO
private $clientDao;
public function __construct() {
parent::__construct();
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// إنشاء طبقة [dao]
$clientDao = new ClientDao($config["urlServer"]);
// إنشاء الطبقة [métier]
$this->métier = new ClientMetier($clientDao);
// تهيئة الجلسة
$this->métier->initSession("json");
// المصادقة
$this->métier->authentifierUtilisateur("admin", "admin");
}
// اختبارات
public function test1() {
$simulation = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $simulation->getImpôt(), 1);
$this->assertEqualsWithDelta(0, $simulation->getSurcôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getDécôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getRéduction(), 1);
$this->assertEquals(0.14, $simulation->getTaux());
}
public function test2() {
….
}
…
public function test11() {
…
}
}
تعليقات
- الأسطر 34-46: تجدر الإشارة إلى أن مُنشئ فئة الاختبار يتم تنفيذه قبل كل اختبار؛
- الأسطر 38-41: إنشاء الطبقات [dao] و [métier]؛
- الأسطر 42-45: تختبر طرق الاختبار [test1…, test11] الطريقة [calculerImpot]. ولتحقيق ذلك، يجب أولاً تهيئة جلسة jSON والمصادقة؛
نتائج الاختبار هي كما يلي:

ينبغي إجراء العديد من الاختبارات الأخرى:
- اختبار الطرق المختلفة للطبقة [dao]؛
- اختبار الحالات التي يعرضها خادم الويب. هذه الحالات مهمة لأن قيمتها تحدد الصفحة HTML التي سيتم عرضها؛