Skip to content

19. تمرين تطبيقي – الإصدار 9

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

  • حاليًا، يتم استرداد بيانات إدارة الضرائب من قاعدة البيانات مع كل طلب. سنستخدم جلسة عمل:
    • أثناء الطلب الأول للمستخدم، يتم استرداد بيانات السلطة الضريبية من قاعدة البيانات وتخزينها في الجلسة؛
    • بالنسبة للطلبات اللاحقة من نفس المستخدم، يتم استرداد بيانات السلطة الضريبية من الجلسة. يمكننا توقع تحسن طفيف في وقت التنفيذ نظرًا لأن استعلامات قاعدة البيانات تستهلك موارد كثيرة؛
  • سيقوم الخادم بتسجيل الأحداث المهمة في ملف نصي:
    • نجاح أو فشل المصادقة؛
    • ما إذا كانت المعلمات المرسلة من العميل صالحة؛
    • نتيجة حساب الضريبة؛
    • حالات الخطأ المختلفة؛
  • في حالة حدوث خطأ فادح، سيتم إرسال بريد إلكتروني إلى مسؤول التطبيق؛

يجب أيضًا تعديل العميل للتعامل مع ملف تعريف الارتباط الخاص بالجلسة الذي سيتم إرساله إليه.

19.1. الخادم

نحن نركز على جانب الخادم من التطبيق.

Image

سيتم تنفيذ هذه البنية من خلال البرامج النصية التالية:

Image

19.1.1. الأدوات المساعدة

Image

19.1.1.1. فئة [Logger]

ستُستخدم فئة [Logger] لكتابة السجلات في ملف نصي:


<?php
 
namespace Application;
 
class Logger {
  // attribute
  private $resource;
 
  // manufacturer
  public function __construct(string $logsFilename) {
    // open file
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // writing a message to the logs
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }
 
  // close log file
  public function close() {
    fclose($this->resource);
  }
 
}

تعليقات

  • السطر 7: مورد ملف السجل؛
  • السطر 10: يتلقى منشئ الفئة اسم ملف السجل كمعلمة؛
  • السطر 12: يتم فتح ملف النص في وضع الإضافة (a+): سيتم فتح الملف، وسيتم الاحتفاظ بمحتوياته. سيتم كتابة البيانات الجديدة بعد المحتوى الحالي؛
  • الأسطر 13-15: إذا تعذر فتح الملف، يتم إصدار استثناء؛
  • الأسطر 19-21: تكتب طريقة [write] الرسالة [$message] في ملف السجل، مسبوقة بالتاريخ والوقت؛
  • الأسطر 24–16: تغلق الطريقة [close] ملف السجل؛

ملاحظة: قد يخدم تطبيق الخادم عدة عملاء في وقت واحد. ومع ذلك، لا يوجد سوى ملف سجل واحد لجميعهم. وبالتالي، هناك خطر حدوث وصول متزامن عند الكتابة إلى الملف. لذلك، يجب مزامنة عمليات الكتابة لمنع اختلاطها. توفر PHP إشارات [https://www.php.net/manual/fr/book.sem.php] لهذا الغرض. سنتجاهل مزامنة الكتابة هنا، ولكن يجب أن نبقى على دراية بهذه المشكلة.

19.1.1.2. فئة [SendAdminMail]

تسمح لك فئة [SendAdminMail] بإرسال بريد إلكتروني إلى مسؤول التطبيق في حالة حدوث عطل:


<?php
 
namespace Application;
 
class SendAdminMail {
  // attributes
  private $config;
  private $logger;
 
  // manufacturer
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }
 
  public function send() {
    // sends $this->config['message'] to smtp server $this->config['smtp-server'] on port $infos[smt-port]
    // if $this->config['tls'] is true, TLS support will be used
    // mail is sent from $this->config['from']
    // for recipient $this->config['to']
    // message has subject $this->config['subject']
    // attachments from $this->config['attachments'] are attached to the mail
    // the result of the method
    try {
      // message creation
      $message = (new \Swift_Message())
        // message subject
        ->setSubject($this->config["subject"])
        // sender
        ->setFrom($this->config["from"])
        // recipients with a dictionary (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // message text
        ->setBody($this->config["message"])
      ;
      // attachments
      foreach ($this->config["attachments"] as $attachment) {
        // path of attachment
        $fileName = __DIR__ . $attachment;
        // check that the file exists
        if (file_exists($fileName)) {
          // attach the document to the message
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // error
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // protocol TLS ?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // no TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // the shipment manager
      $mailer = new \Swift_Mailer($transport);
      // sending the message
      $mailer->send($message);
      // end
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // error
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }
 
}

تعليقات

  • السطر 11: يأخذ المنشئ معلمتين:
    • [$config]: مصفوفة ترابطية تحتوي على جميع المعلومات اللازمة لإرسال البريد الإلكتروني؛
    • [$logger]: مسجل يستخدم لتسجيل اللحظات المهمة أثناء عملية إرسال البريد الإلكتروني؛

سيكون المصفوف الترابطي بالشكل التالي:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • الأسطر 16–76: تُستخدم طريقة [send] لإرسال البريد الإلكتروني. تم عرض هذا الكود ووصفه في القسم المرتبط؛

19.1.2. طبقة [dao]

Image

فيما يلي نص البرنامج النصي [ServeurDaoWithSession.php]:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class ServerDaoWithSession extends ServerDao {
 
  // manufacturer
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // simplest case
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // hand over to parent class
      parent::__construct($databaseFilename);
    }
  }
 
}

تعليقات

  • السطر 7: فئة [ServerDaoWithSession] في الإصدار 09 تمتد من فئة [ServerDao] من الإصدار 08. في الواقع، تعرف فئة [ServerDao] كيفية استخدام قاعدة البيانات. كل ما تبقى هو معالجة الحالة التي تم فيها بالفعل استرداد بيانات إدارة الضرائب:
  • السطر 10: يأخذ المنشئ الآن معلمتين:
    • [string $databaseFilename]: اسم الملف الذي يحتوي على المعلومات اللازمة للاتصال بقاعدة البيانات إذا لم يتم استرداد بيانات إدارة الضرائب بعد، وإلا فإن القيمة هي NULL؛
    • [TaxAdminData $taxAdminData]: بيانات إدارة الضرائب إذا تم استردادها بالفعل، وإلا فإنها تكون NULL؛

عند بدء جلسة الويب، سيتم إنشاء طبقة [dao] باستخدام كائن [$databaseFilename] غير NULL وكائن [taxAdminData] NULL. بعد ذلك، سيتم استرداد بيانات إدارة الضرائب من قاعدة البيانات وتخزينها في الجلسة. بالنسبة للطلبات اللاحقة ضمن نفس الجلسة، سيتم إنشاء طبقة [dao] باستخدام كائن [databaseFilename] NULL وكائن [taxAdminData] تم استرداده من الجلسة (والذي ليس NULL). لذلك، لن يتم إجراء أي بحث في قاعدة البيانات.

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

يتم تكوين البرنامج النصي للخادم [impots-server.php] بواسطة ملف JSON التالي [config-server.json]:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/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": "Data/logs.txt"
}

يتغير البرنامج النصي للخادم [impots-server.php] على النحو التالي:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
// session
$session = new Session();
$session->start();
 
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// log file creation
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // internal server error
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // completed
  exit;
}
 
// 1st log
$logger->write("\n---nouvelle requête\n");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication only the 1st time
if (!$session->has("user")) {
  // log
  $logger->write("Autentification en cours…\n");
  // authentication

  }
  // has the user been found?
  if (!$trouvé) {
    // not found - code 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // completed
    exit;
  } else {
    // we note in the session that we have authenticated the user
    $session->set("user", TRUE);
    // log
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET

 
// mistakes?
if ($erreurs) {
// an error code 400 HTTP_BAD_REQUEST is sent to the customer
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // completed
  exit;
} else {
  // logs
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// we have everything you need to work
// creation of the [dao] layer
if (!$session->has("taxAdminData")) {
  // the data is taken from the database
  $logger->write("données fiscales prises en base de données\n");
  try {
    // construction of the [dao] layer
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // put data in session
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // we note the error
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // completed
    exit;
  }
} else {
  // data are taken from the session
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // logs
  $logger->write("données fiscales prises en session\n");
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// end
exit;
 
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // send an e-mail to the administrator
  // SendAdminMail intercepts all exceptions and logs them itself
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // an error code 500 is sent to the customer
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}
 
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response : answer HTTP
  // $result: results table
  // $statusCode: HTTP response status
  // $headers: HTTP headers to be included in the response
  // $logger: application logger
  //
  // status HTTTP
  $response->setStatusCode($statusCode);
  // body
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // headers
  $response->headers->add($headers);
  // shipping
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

تعليقات

  • السطور 34-35: بدء جلسة؛
  • الأسطر 38–40: إعداد استجابة JSON؛
  • الأسطر 42-50: محاولة إنشاء ملف السجل. في حالة حدوث استثناء، يتم استدعاء الأسلوب [doInternalServer] (الأسطر 132-140)؛
  • السطر 132: تقبل طريقة [doInternalServer] أربعة معلمات:
    • [$message]: الرسالة المراد تسجيلها. يجب ترميزها بـ UTF-8؛
    • [$response]: كائن [Response] الذي يغلف استجابة الخادم لعميله؛
    • [$logger]: كائن [Logger] المستخدم للتسجيل؛
    • [$infos]: المعلومات المستخدمة لإرسال بريد إلكتروني إلى مسؤول التطبيق؛
  • الأسطر 135–137: يتم إرسال بريد إلكتروني إلى مسؤول التطبيق؛
  • السطر 139: يتم إرسال الاستجابة إلى العميل:
    • $response: استجابة HTTP؛
    • $result: يرسل الخادم سلسلة JSON من المصفوفة [‘response’=>["error" => $message]
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR]، الرمز 500؛
    • $headers: [], لا توجد رؤوس HTTP لإضافتها إلى الرد؛
    • $logger: مسجل التطبيق؛
  • السطر 58: بفضل إعداد الجلسة، سنقوم بمصادقة العميل مرة واحدة فقط:
    • بمجرد مصادقة العميل، سنقوم بتعيين مفتاح [user] في الجلسة (السطر 78)؛
    • أثناء الطلب التالي من نفس العميل، يمنع السطر 58 المصادقة غير الضرورية؛
  • السطر 103: بفضل الجلسة التي تم إنشاؤها، سنقوم بالبحث في قاعدة البيانات مرة واحدة فقط:
    • أثناء الطلب الأول، سيتم إجراء البحث في قاعدة البيانات (السطر 108). ثم يتم تخزين البيانات المسترجعة في الجلسة (السطر 110) المرتبطة بمفتاح [taxAdminData]؛
    • بالنسبة للطلبات اللاحقة، سيتم العثور على مفتاح [taxAdminData] في الجلسة (السطر 103)، ثم يتم تمرير البيانات الموجودة على القرص مباشرةً إلى طبقة [dao] (السطر 119)؛
  • الأسطر 111–116: قد يفشل البحث عن البيانات الضريبية في قاعدة البيانات. في هذه الحالة، يتم إرسال رمز [500 Internal Server Error] إلى العميل؛
  • السطر 113: يتم ترميز رسالة الخطأ الناتجة عن استثناء برنامج تشغيل MySQL بـ ISO 8859-1. يتم تحويلها إلى UTF-8 لتسجيلها بشكل صحيح؛
  • باقي الكود مطابق تقريبًا لكود الإصدار السابق؛
  • الأسطر 143–164: ترسل الدالة [sendResponse] جميع الاستجابات إلى العميل؛
  • الأسطر 144–148: معنى المعلمات؛
  • السطر 153: يكون الرد دائمًا سلسلة JSON لمصفوفة [‘result’=>something]؛
  • السطر 156: في بعض الأحيان، توجد رؤوس HTTP لإضافتها إلى الرد. هذا هو الحال في السطر 71؛
  • السطر 158: يتم إرسال الاستجابة؛
  • الأسطر 160–163: يتم تسجيل الرد وإغلاق المسجل؛

19.1.4. [Codeception] الاختبارات

Image

سنقوم باختبار طبقة [dao] فقط، وهي الطبقة الوحيدة التي تم تغييرها.

فيما يلي كود الاختبار [ServerDaoTest]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test -----------------------------------------------------
 
class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    // parent
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {

  }
 
}
  • الأسطر 9–24: نقوم بإنشاء بيئة تشغيل مطابقة لتلك الموجودة في البرنامج النصي للخادم [impots-server]؛
  • السطر 38: لإنشاء طبقة [dao]، نقوم بإنشاء مثيل لفئة [ServerDaoWithSession]؛

نتائج الاختبار هي كما يلي:

Image

19.2. العميل

نحن نركز على جانب العميل من التطبيق.

Image

سيتم تنفيذ هذه البنية من خلال البرامج النصية التالية:

Image

في الإصدار الجديد، التغييرات الوحيدة هي:

  • ملف التكوين [config-client.json]؛
  • طبقة [dao] الخاصة بالعميل؛

19.2.1. طبقة [dao]

تتطور طبقة [Dao] على النحو التالي:


<?php
 
namespace Application;
 
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
 
class ClientDao implements InterfaceClientDao {
  // using a Trait
  use TraitDao;
  // attributes
  private $urlServer;
  private $user;
  private $sessionCookie;
 
  // manufacturer
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }
 
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // session cookie ?
    if (!$this->sessionCookie) {
      // create a HTTP customer
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // make the request to the server without a session cookie
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // make a request to the server with the session cookie
      // create a HTTP customer
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // the answer is retrieved
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // logs
    print "$json=json\n";
    // retrieve response status
    $statusCode = $response->getStatusCode();
    // mistake?
    if ($statusCode !== 200) {
      // we have an error - we throw an exception
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    if (!$this->sessionCookie) {
      // retrieve the session cookie
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // session cookie ?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // we return the answer
    return $réponse;
  }
 
}

تعليقات

يتضمن التعديل الذي أُجري على طبقة [dao] الآن إدارة جلسة عمل:

  • السطر 14: ملف تعريف الارتباط الخاص بالجلسة؛
  • الأسطر 25-39: أثناء الطلب الأول، لا توجد ملفات تعريف الارتباط هذه؛ لذلك نرسل الطلب إلى الخادم عن طريق إرسال معلومات المصادقة (السطر 28)؛
  • الأسطر 40-53: بالنسبة للطلبات اللاحقة، عادة ما يكون لدينا ملف تعريف ارتباط الجلسة. لذلك لا نرسل معلومات المصادقة (الأسطر 42-44)؛
  • الأسطر 69-82: سيتضمن رد الخادم على الطلب الأول ملف تعريف ارتباط الجلسة. نقوم باسترداده. تم استخدام هذا الرمز وشرحه بالفعل في القسم المرتبط؛
  • السطر 78: يتم تخزين ملف تعريف ارتباط الجلسة الذي تم استرداده في سمة الفئة [$sessionCookie]؛

ملاحظة: كان بإمكاننا الاحتفاظ بالإصدار القديم من طبقة [dao] وإجراء المصادقة على كل طلب، حيث أن التكلفة ضئيلة. ولأغراض تعليمية، أردنا توضيح كيفية إدارة عميل HTTP للجلسة.

19.2.2. ملف التكوين

يتطور ملف التكوين JSON على النحو التالي:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/Métier/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-09/impots-server.php"
}

يتغير عنوان URL في السطر 24 فقط.

19.3. بعض الاختبارات

19.3.1. الاختبار 1

أولاً، نقوم بتشغيل العميل في بيئة خالية من الأخطاء. النتائج هي نفسها كما في الإصدارات السابقة. ولكن الآن، على جانب الخادم، لدينا ملف سجل [logs.txt]:


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • الأسطر 3-7: أثناء الطلب الأول، تتم المصادقة ويتم استرداد البيانات من قاعدة البيانات؛
  • الأسطر 9-14: أثناء الطلب التالي، لا تتم المصادقة مرة أخرى ويتم استرداد البيانات من الجلسة. ويتكرر هذا الأمر للطلبات اللاحقة (السطر 15 وما بعده)؛

19.3.2. الاختبار 2

الآن دعونا نغلق قاعدة بيانات MySQL. على جانب العميل، نحصل على إخراج وحدة التحكم التالي:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé

على جانب الخادم، لدينا السجلات التالية [logs.txt]:


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

لاسترداد البريد الإلكتروني الذي تلقّاه مسؤول التطبيق، نستخدم البرنامج النصي [imap-03.php] من القسم المرتبط بملف التكوين التالي [config-imap-01.json]:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

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

Image

يحتوي الملف [message_1.txt] على النص التالي:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
 
SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. الاختبار 3

الآن دعونا نتأكد من أنه لا يمكن إنشاء ملف [logs.txt]. للقيام بذلك، ما عليك سوى إنشاء مجلد [logs.txt]:

Image

بمجرد الانتهاء من ذلك، دعونا نقوم بتشغيل برنامج العميل.

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


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

على جانب الخادم، لا توجد سجلات، لكن المسؤول يتلقى البريد الإلكتروني التالي:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
 
Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. الاختبار 4

هذه المرة، دعونا نقدم بيانات اعتماد غير صحيحة للعميل المتصل في ملف تكوين العميل.

يعرض العميل الناتج التالي على وحدة التحكم:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

على جانب الخادم، تظهر السجلات التالية:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. اختبار 5

دعونا نعيد إدخال اسم المستخدم وكلمة المرور الصحيحين [admin, admin] في ملف تكوين العميل.

الآن دعونا نطلب عنوان URL الخاص بالخادم [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] مباشرةً في المتصفح دون تمرير أي معلمات:

في ملف سجل الخادم [logs.txt]، نرى الأسطر التالية:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. اختبارات [Codeception]

كما فعلنا في الإصدارات السابقة، سنكتب اختبارات [Codeception] للإصدار 09.

Image

19.4.0.1. اختبار طبقة [Business]

اختبار [ClientMetierTest.php] هو كما يلي:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");
 
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// uses
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
 
// test class
class ClientMetierTest extends Unit {
  
}

تعليقات

  • بالمقارنة مع فئة الاختبار في الإصدار 08، التغيير الوحيد هو السطر 10، الذي يحدد الدليل الجذر للعميل المراد اختباره؛

نتائج الاختبار هي كما يلي:

Image

من المفيد التحقق من سجلات الخادم [logs.txt]:


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

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


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 

 
// test class
class ClientMetierTest extends Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // creation of the [business] layer
    $this->métier = new ClientMetier($clientDao);
  }
 
  // tests
  public function test1() {

  }
 
  public function test2() {

  }
 
  public function test3() {

  }
 

 
}

في فئة اختبار [Codeception]، يتم تنفيذ المنشئ لكل اختبار.

  • السطر 21: وبالتالي، يتم إنشاء [ClientDao] جديد لكل اختبار مع ملف تعريف ارتباط جلسة عمل NULL. وهذا يفسر سبب عدم استفادة هذا العميل من أي جلسة عمل؛

يوضح لنا هذا المثال أن الجلسة ليست المكان المناسب لتخزين بيانات إدارة الضرائب. في الواقع، يتم مشاركة هذه البيانات بين جميع مستخدمي التطبيق. ومع ذلك، يتم هنا تكرارها في كل جلسة من جلساتهم.

في برمجة الويب، هناك ثلاثة أنواع من الرؤية للبيانات المشتركة:

  • البيانات المشتركة بين جميع مستخدمي تطبيق الويب. وعادةً ما تكون هذه البيانات للقراءة فقط. ولا يدعم PHP هذا النوع من التخزين بشكل أصلي؛
  • البيانات المشتركة عبر الطلبات من نفس العميل. يتم تخزين هذه البيانات في الجلسة. نشير إلى هذا باسم جلسة العميل للإشارة إلى تخزين العميل. جميع الطلبات من العميل لها حق الوصول إلى هذه الجلسة. يمكنهم تخزين وقراءة المعلومات هناك. في البرامج النصية السابقة، يتم تنفيذ هذه الجلسة بواسطة كائن Symfony [HttpFoundation\Session\Session]؛
  • ذاكرة الطلب، أو سياق الطلب. يمكن معالجة طلب المستخدم من خلال عدة إجراءات متتالية. يسمح سياق الطلب للإجراء 1 بتمرير المعلومات إلى الإجراء 2. في البرامج النصية السابقة، يتم تنفيذ الطلب بواسطة كائن Symfony [HttpFoundation\Request] وذاكرته بواسطة السمة [HttpFoundation\Request::attributes]؛

Image

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