Skip to content

23. Ćwiczenie praktyczne – wersja 12

W tym rozdziale napiszemy aplikację internetową zgodną z architekturą MVC (Model-Widok-Kontroler). Aplikacja będzie mogła generować wyniki w trzech formatach: jSON, XML, HTML. Istnieje znaczna różnica w stopniu złożoności między tym, co zamierzamy teraz zrobić, a tym, co zostało zrobione wcześniej. Wykorzystamy większość omówionych dotychczas koncepcji i szczegółowo omówimy wszystkie etapy prowadzące do powstania ostatecznej aplikacji.

23.1. Architektura MVC

Zaimplementujemy model architektury znany jako MVC (Model – Widok – Kontroler) w następujący sposób:

Image

Przetwarzanie żądania klienta będzie przebiegało w następujący sposób:

  • 1 – żądanie

Żądania URL będą miały postaćhttp://machine:port/contexte/….?action=uneAction&param1=v1&param2=v2&… [Contrôleur principal] wykorzysta plik konfiguracyjny do „przekierowania” żądania do właściwego kontrolera i właściwej akcji w ramach tego kontrolera. W tym celu wykorzysta pole [action] z URL. Pozostała część URL [param1=v1&param2=v2&…] składa się z opcjonalnych parametrów, które zostaną przekazane do akcji. C w MVC to w tym przypadku ciąg znaków [Contrôleur principal, Contrôleur / Action]. Jeśli żaden kontroler nie jest w stanie obsłużyć żądanej akcji, serwer WWW odpowie, że żądana akcja URL nie została znaleziona.

  • 2 – przetwarzanie
    • Wybrana akcja [2a] może wykorzystać parametry parami, które przekazała jej akcja [Contrôleur principal]. Mogą one pochodzić z kilku źródeł:
      • ze ścieżki [/param1/param2/…] należącej do URL,
      • z parametrów [param1=v1&param2=v2] z URL,
      • z parametrów przesłanych przez przeglądarkę wraz z żądaniem;
    • Podczas przetwarzania żądania użytkownika akcja może wymagać wykorzystania warstwy [métier] [2b]. Po przetworzeniu żądania klienta akcja ta może wywołać różne odpowiedzi. Klasycznym przykładem jest:
      • odpowiedź o błędzie, jeśli żądanie nie mogło zostać poprawnie przetworzone;
      • w przeciwnym razie odpowiedź potwierdzająca;
    • [Contrôleur / Action] zwróci swoją odpowiedź [2c] do głównego kontrolera wraz z kodem stanu. Kody te w jednoznaczny sposób odzwierciedlają stan, w jakim znajduje się aplikacja. Będą to albo kody sukcesu, albo kody błędów;
  • 3 – odpowiedź
    • w zależności od tego, czy klient zażądał odpowiedzi jSON, XML czy HTML, [Contrôleur principal] utworzy instancję odpowiedniego typu odpowiedzi [3a] i poprosi ją o wysłanie odpowiedzi do klienta. [Contrôleur principal] przekaże mu zarówno odpowiedź, jak i kod stanu dostarczone przez uruchomiony obiekt [Contrôleur / Action];
    • jeśli żądana odpowiedź jest typu jSON lub XML, wybrana odpowiedź sformatuje odpowiedź otrzymaną od [Contrôleur / Action] i wyśle ją za pośrednictwem [3c]. Klientem zdolnym do wykorzystania tej odpowiedzi może być skrypt konsolowy o nazwie PHP lub skrypt JavaScript umieszczony na stronie o nazwie HTML;
    • jeśli pożądana odpowiedź ma typ HTML, wybrana odpowiedź wybierze [3b] jeden z widoków HTML [Vuei] przy użyciu podanego kodu stanu. Jest to widok V dla MVC. Jednemu kodowi stanu odpowiada jeden widok. Ten widok V wyświetli odpowiedź z [Contrôleur / Action], który został wykonany. Wykorzystuje ona HTML, CSS oraz JavaScript do sformatowania danych tej odpowiedzi. Dane te nazywamy modelem widoku. To właśnie litera M w MVC. Klientem jest wówczas najczęściej przeglądarka;

Teraz wyjaśnijmy związek między architekturą internetową MVC a architekturą warstwową. W zależności od definicji modelu te dwie koncepcje mogą być ze sobą powiązane lub nie. Weźmy na przykład jednowarstwową aplikację internetową MVC:

Image

W powyższym przykładzie każdy z elementów [Contrôleur / Action] zawiera część warstw [métier] i [dao]. W warstwie [web] rzeczywiście występuje architektura MVC, ale cała aplikacja nie ma architektury warstwowej. Tutaj istnieje tylko jedna warstwa, która wykonuje wszystkie zadania.

Rozważmy teraz wielowarstwową architekturę internetową:

Image

Warstwa [web] może zostać zaimplementowana bez stosowania się do modelu MVC. Mamy zatem architekturę wielowarstwową, ale warstwa internetowa nie implementuje modelu MVC.

Na przykład w środowisku .NET warstwa [web]może być zaimplementowana przy użyciu ASP.NET i MVC, co daje architekturę warstwową z warstwą [web] typu MVC. Po wykonaniu tej czynności można zastąpić tę warstwę ASP.NET MVC klasyczną warstwą ASP.NET (WebForms), zachowując pozostałe elementy (biznesowa, DAO, sterownik) bez zmian. Otrzymujemy wówczas architekturę warstwową z warstwą [web], która nie jest już typu MVC.

W MVC stwierdziliśmy, że model M odpowiada widokowi V, c.a.d, czyli zbiorowi danych wyświetlanych przez widok V. Podano kolejną definicję modelu M dla MVC:

Image

Wielu autorów uważa, że to, co znajduje się po prawej stronie warstwy [web], tworzy model M modelu MVC. Aby uniknąć niejasności, można mówić o:

  • o modelu domeny, gdy mamy na myśli wszystko, co znajduje się po prawej stronie warstwy [web];
  • o modelu widoku, gdy mamy na myśli dane wyświetlane przez widok V;

23.2. Struktura drzewa projektu NetBeans

W projekcie NetBeans zastosujemy architekturę odzwierciedlającą model MVC:

Image

  • [3]: [main.php] jest głównym kontrolerem naszego modelu MVC. Jest to element C w modelu MVC;
  • [4]: folder [Controllers] będzie zawierał kontrolery podrzędne. Każdy z nich obsługuje konkretną akcję. Akcja ta jest określona w pliku URL, na przykład […/main.php?action=authentifier-utilisateur]. W ramach tej akcji plik [Contrôleur principal] oraz [main.php] wybierają plik [Contrôleur secondaire], w tym przypadku [AuthentifierUtilisateurController], w celu przetworzenia żądanej akcji. Kontrolery te należą również do elementu C obiektu MVC;
  • [5]: folder [Model] będzie zawierał warstwy [métier] i [dao] aplikacji. Zgodnie z wcześniej przyjętymi terminami elementy te reprezentują model domeny, a zgodnie z terminologią przyjętą dla litery M mogą reprezentować literę M w MVC;
  • [6]: folder [Responses] zawiera klasy odpowiedzialne za wysyłanie odpowiedzi do klienta. Dla każdego pożądanego typu odpowiedzi istnieje jedna klasa:
    • [JsonResponse]: dla odpowiedzi jSON;
    • [XmlResponse]: dla odpowiedzi XML;
    • [HtmlResponse]: dla odpowiedzi HTML;
  • [7]: folder [Views] zawiera widoki HTML, gdy wymagana jest odpowiedź HTML. Jest to widok V z MVC. Są one aktywowane przez klasę [HtmlResponse], która przekazuje im dane do wyświetlenia. Dane te stanowią szablon widoku. Zgodnie z terminologią przyjętą dla M, dane te mogą stanowić M z MVC;
  • [8]: folder [Utilities] zawiera narzędzia:
    • [Logger]: klasa umożliwiająca zapisywanie logów w pliku tekstowym;
    • [Sendmail]: klasa umożliwiająca wysyłanie wiadomości e-mail;
  • [9]: folder [Logs] zawiera plik logów [logs.txt];
  • [10]: folder [Entities] zawiera klasy wykorzystywane przez różne kontrolery;

Korzystając z tej struktury katalogów, można opisać przebieg przetwarzania akcji żądanej przez klienta:

  • [main.php] [3] odbiera żądanie;
  • po przeprowadzeniu kilku wstępnych weryfikacji (czy akcja należy do akcji akceptowanych?) przekazuje żądanie do kontrolera pomocniczego [4] odpowiedzialnego za przetworzenie tej akcji;
  • kontroler pomocniczy wykonuje swoje zadanie. W trakcie pracy może potrzebować warstw [métier], [dao] i [5], a także elementów z folderu [10]. Przekazuje swoją odpowiedź kontrolerowi głównemu [main.php], który go aktywował;
  • w zależności od typu odpowiedzi [jSON, XML, HTML] żądanego przez klienta, główny kontroler [main.php] aktywuje jedną z odpowiedzi z folderu [Responses] [6];
  • odpowiedzi [JsonResponse, XmlResponse] wysyłają odpowiednio odpowiedź jSON lub XML do klienta;
  • odpowiedź [HtmlResponse] wykorzystuje jeden z widoków z folderu [Views] [7] w celu wysłania odpowiedzi HTML do klienta;
  • różne kontrolery mają dostęp do klasy [Logger] z folderu [8] w celu zapisywania logów w pliku logów w folderze [9]. Rejestrowane są:
    • żądana akcja;
    • odpowiedź jej kontrolera. Jest ona rejestrowana w formacie jSON niezależnie od żądanego typu [jSON, XML, HTML];
  • w przypadku wystąpienia błędu krytycznego (HTTP_INTERNAL_SERVER_ERROR) główny kontroler [main.php] wysyła wiadomość e-mail do administratora przy użyciu klasy [SendMail] z folderu [8];

23.3. Działania aplikacji

Klient przekazuje serwerowi WWW akcję do wykonania w postaci parametru [action] w URL [/main.php?action=xxx]. Dozwolone akcje są wymienione w pliku [config.json], który konfiguruje główny kontroler [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"
},
  • wiersz 1: klucz [actions] ze słownika jSON;
  • wiersze 3–9: słownik [action:contrôleur]. Każdej akcji przypisany jest kontroler pomocniczy odpowiedzialny za jej przetworzenie;
  • wiersz 3: [init-session]: rozpoczyna sesję symulacji obliczeń podatkowych. Ta akcja określa typ oczekiwanych odpowiedzi [jSON, XML, HTML];
  • wiersz 4: po ustaleniu typu sesji klient będzie musiał uwierzytelnić się za pomocą akcji [authentifier-utilisateur]. Dopóki nie zostanie zidentyfikowany, wszystkie inne akcje są zablokowane, z wyjątkiem [init-session];
  • wiersz 5: po uwierzytelnieniu klient będzie mógł przeprowadzić serię obliczeń podatkowych za pomocą akcji [calculer-impot];
  • wiersz 6: w dowolnym momencie klient może wyświetlić listę przeprowadzonych przez siebie symulacji za pomocą akcji [lister-simulations];
  • wiersz 7: będzie mógł usunąć niektóre z nich za pomocą akcji [supprimer-simulation];
  • wiersz 8: klient kończy sesję symulacji za pomocą akcji [fin-session]. Od tego momentu będzie musiał ponownie się uwierzytelnić, jeśli zechce korzystać z aplikacji;
  • wiersz 9: w aplikacji HTML akcja [afficher-calcul-impot] powoduje wyświetlenie formularza służącego do obliczenia podatku;

23.4. Konfiguracja aplikacji internetowej

Aplikacja jest konfigurowana za pomocą następującego pliku 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"
}

Uwagi

  • wiersz 2: nazwa pliku jSON zawierającego konfigurację dostępu do bazy danych;
  • wiersze 3–39: konfiguracja zależności projektu. Wymieniono tu wszystkie skrypty PHP z drzewa projektu;
  • wiersze 40–44: użytkownik uprawniony do korzystania z aplikacji;
  • wiersze 46–54: adresy e-mail administratora aplikacji;
  • wiersz 55: ścieżka do pliku logów;
  • wiersze 56–65: powiązania [action => contrôleur secondaire chargé de la traiter];
  • wiersze 66–70: powiązania [type de réponse => classe Response chargée d’envoyer la réponse au client];
  • wiersze 71–75: powiązania [vue HTML => tableau des codes d’état menant à cette vue];
  • wiersz 76: widok [vue-erreurs] jest wyświetlany w sesji HTML za każdym razem, gdy wystąpi błąd nietypowy:
    • aplikacja jSON lub XML jest zazwyczaj wywoływana za pomocą zaprogramowanego klienta. Klient ten przekazuje serwerowi parametry, które mogą być nieobecne lub błędne. Wszystkie kontrolery obsługują te przypadki i zwracają klientowi kody błędów. Należy uwzględnić wszystkie możliwe przypadki błędów;
    • w przypadku aplikacji HTML sytuacja wygląda nieco inaczej. Przy normalnym użytkowaniu aplikacja internetowa wykorzystuje jedynie część możliwych scenariuszy użycia klientów jSON i XML. Weźmy przykład: akcja [calculer-impot] oczekuje trzech parametrów przesłanych metodą POST (wysłanych przez POST): [marié, enfants, salaire].
      • jeśli mamy klienta jSON, który pozwala na ręczne wpisywanie URL, można wywołać akcję [calculer-impot] z parametrem GET zamiast POST lub z parametrem POST bez żadnych przesłanych parametrów, podczas gdy wymagane są trzy itp. Serwer jSON musi obsłużyć wszystkie te przypadki;
      • W przypadku aplikacji internetowej akcja [calculer-impot] zostanie wywołana z formularza internetowego, w którym żaden z dwóch poprzednich przypadków nie będzie możliwy: akcja [calculer-impot] zostanie wywołana wraz z akcją POST oraz trzema parametrami [marié, enfants, salaire]. Niektóre z tych parametrów mogą mieć nieprawidłową wartość, ale będą obecne. Użytkownik może jednak samodzielnie wywołać pewne błędy, wpisując kody URL w przeglądarce. Ze względów bezpieczeństwa należy uwzględnić ten przypadek;
      • widok [vue-erreurs] będzie wyświetlany za każdym razem, gdy kontroler pomocniczy zwróci kod stanu niezgodny z aplikacją internetową, tj. kod stanu nieobecny w wierszach 72–74 pliku konfiguracyjnego. Decydujemy się na to rozwiązanie ze względów dydaktycznych. Inną możliwą opcją byłoby niepodejmowanie żadnych działań i po prostu ponowne wyświetlenie widoku aktualnie wyświetlanego w przeglądarce klienta, tak aby użytkownik miał wrażenie, że serwer nie odpowiada na jego ręcznie utworzone zapytania URL;

23.5. Instalacja narzędzi i bibliotek

23.5.1. Postman

[Postman] to narzędzie, które pozwoli nam wysyłać zapytania do różnych URL naszej aplikacji internetowej. Umożliwia nam ono:

  • korzystać z dowolnych URL: są one tworzone ręcznie;
  • wysyłać zapytania do serwera internetowego za pomocą plików GET, POST, PUT, OPTIONS…;
  • określić parametry GET lub POST;
  • ustalić nagłówki HTTP żądania;
  • otrzymania odpowiedzi w formacie jSON, XML, HTML,
  • uzyskanie dostępu do nagłówków odpowiedzi HTTP. W ten sposób uzyskujemy dostęp do pełnej odpowiedzi serwera HTTP;

Ponieważ ręcznie tworzymy zapytania o numerach URL, będziemy mogli przetestować wszystkie możliwe przypadki błędów i sprawdzić, jak reaguje serwer.

[Postman] jest dostępne pod adresem URL [https://www.getpostman.com/downloads/]. Wersja dostępna w czerwcu 2019 r. to 7.2. Wersja ta wykazuje pewną anomalię: podczas wysyłania kolejnych żądań do serwera WWW klient [Postman 7.2] nie odsyła automatycznie plików cookie, które serwer mu przesyła, w szczególności pliku cookie sesji. Aby utrzymać sesję, należy zatem ręcznie skopiować plik cookie sesji do nagłówków HTTP kolejnych żądań. Nie jest to zbyt skomplikowane, ale nie jest to praktyczne. Jest to błąd, który nie występował w poprzednich wersjach. Zdając sobie sprawę z tego błędu, zespół [Postman] naprawił go w wersji alfa (możliwe, że niestabilnej) o nazwie [Postman Canary], dostępnej na stronie URL [https://www.getpostman.com/downloads/canary]. To właśnie ta wersja jest tutaj używana. Opiszemy proces jej instalacji. Jeśli dostępna jest stabilna wersja [Postman 7.3] lub nowsza, można ją pobrać: błąd prawdopodobnie został już naprawiony.

Przeprowadź instalację swojej wersji [Postman]. Podczas instalacji pojawi się prośba o utworzenie konta: w tym przypadku nie będzie ono potrzebne. Konto [Postman] służy do synchronizacji różnych urządzeń, tak aby konfiguracja jednego z nich została skopiowana na inne. W tym przypadku żadna z tych opcji nie jest potrzebna.

Po zainstalowaniu aplikacja [Postman] wyświetla następujący interfejs:

Image

  • w [2-3] mamy dostęp do ustawień produktu;

Image

  • w [6], wersji używanej w tym dokumencie;
  • jeśli utworzyli Państwo konto, następuje synchronizacja między Państwa komputerem a zdalnym serwerem [Postman]. Symbolizuje to obracające się koło [7], które pojawia się za każdym razem, gdy wprowadzają Państwo zmiany w projekcie [Postman]. Aby zatrzymać tę zbędną synchronizację, wyloguj się z [8-9];

23.5.2. Biblioteka Symfony / Serializer

Aby serializować obiekty w jSON i XML, wykorzystamy bibliotekę [Symfony / Serializer]. Ma ona tutaj dwie zalety:

  • jest spójna w użyciu zarówno przy serializacji do formatu jSON, jak i XML: dzięki temu nie trzeba uczyć się obsługi dwóch różnych interfejsów programowania aplikacji (API);
  • z natury potrafi serializować obiekty w formatach jSON lub XML, nawet jeśli ich atrybuty są prywatne. Przypomnijmy, że w jSON, aby zserializować obiekt, klasa tego obiektu musiała implementować interfejs [\JsonSerializable]. Wynikiem tego był łańcuch jSON reprezentujący tablicę asocjacyjną, w której klucze stanowiły atrybuty klasy. Podczas deserializacji tego łańcucha jSON uzyskiwano pierwotną tablicę asocjacyjną, którą następnie należało przekształcić w obiekt klasy, która została zserializowana. W przypadku ciągu [Symfony / Serializer] deserializacja od razu zwraca obiekt klasy zserializowanej. Jest to prostsze;

Dokumentacja biblioteki [Symfony / Serializer] jest dostępna pod adresem URL: [https://symfony.com/doc/current/components/serializer.html] (czerwiec 2019 r.).

Aby zainstalować tę bibliotekę, otwórz terminal Laragon (patrz link w akapicie) i wpisz następujące polecenie:

Image

  • w [1], polecenie instalacji biblioteki [symfony/serializer];
  • w [2], kolejną bibliotekę niezbędną dla naszego projektu: umożliwia serializację obiektów;

Image

23.6. Elementy aplikacji

Image

Elementy [BaseEntity, Database, ExceptionImpots, TaxAdminData] zostały wykorzystane już w wersji 08 serwisu internetowego (patrz akapit „link”).

Klasa [Simulation] posłuży do enkapsulacji elementów symulacji obliczeń podatkowych:


<?php

namespace Application;

class Simulation extends BaseEntity {
  // atrybuty symulacji obliczeń podatkowych
  protected $marié;
  protected $enfants;
  protected $salaire;
  protected $impôt;
  protected $surcôte;
  protected $décôte;
  protected $réduction;
  protected $taux;

  // metody pobierające
  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;
  }

}

Komentarze

  • wiersz 5: klasa [Simulation] rozszerza klasę [BaseEntity] i w związku z tym dziedziczy następujące metody:
    • [setFromArrayOfAttributes($arrayOfAttributes)]: która pozwala zainicjować atrybuty klasy;
    • [__toString]: która zwraca ciąg znaków jSON obiektu;
  • wiersze 7–14: atrybuty symulacji;
  • wiersze 16–47: metody pobierające klasy;

23.7. Narzędzia aplikacji

Image

Klasa [Logger] umożliwia rejestrowanie zdarzeń w pliku tekstowym. Klasa ta została opisana w akapicie link.

Klasa [SendAdminMail] umożliwia wysłanie wiadomości e-mail do administratora aplikacji. Klasa ta została opisana w akapicie [link].

23.8. Warstwy [métier] i [dao]

Image

Image

Klasy i interfejsy warstw [métier] oraz [dao] znajdują się w folderze [Model]. Wszystkie zostały zdefiniowane i wykorzystane w poprzednich wersjach:

ExceptionImpots
Klasa wyjątków rzucanych przez warstwę [dao]. Zdefiniowana w sekcji „link”.
InterfaceServerDao
Interfejs zaimplementowany przez warstwę serwera [dao]. Zdefiniowany w akapicie „link”.
ServerDao
Implementacja interfejsu [InterfaceServerDao]. Implementuje warstwę serwera [dao]. Zdefiniowana w akapicie „link”.
ServerDaoWithSession
Implementacja interfejsu [InterfaceServerDao]. Implementuje warstwę serwera [dao]. Zdefiniowana w akapicie „link”.
InterfaceServerMetier
Interfejs zaimplementowany przez warstwę [métier] serwera. Zdefiniowany w sekcji „link”.
ServerMetier
Implementacja interfejsu [InterfaceMetier]. Implementuje warstwę serwera [metier]. Zdefiniowana w akapicie „link”.

Tworzona aplikacja wykorzystuje wiele elementów, które zostały już przedstawione i zastosowane:

  • warstwy [métier] i [dao];
  • narzędzia [Logger] i [SendAdminMail];
  • entytety [ExceptionImpots, TaxAdminData, Database];

Skupimy się na warstwie [web] aplikacji:

Image

23.9. Główny kontroler [main.php]

23.9.1. Wprowadzenie

Image

  • [1-2]: główny kontroler [main.php] [1] jest konfigurowany przez plik [config.json] [2];

Przypomnijmy pozycję kontrolera głównego w naszej architekturze MVC:

Image

W [1] główny kontroler [main.php] jest pierwszym elementem architektury MVC, który przetwarza żądanie klienta. Pełni on kilka ról:

  • najpierw przeprowadza podstawowe weryfikacje:
    • czy jego plik konfiguracyjny istnieje i jest poprawny;
    • ładuje wszystkie zależności projektu. Oznacza to załadowanie wszystkich elementów architektury MVC;
    • czy żądana akcja została określona? Jeśli tak, to czy jest prawidłowa?
    • jeśli żądana akcja jest prawidłowa, wybiera [2a] kontroler pomocniczy, który ją przetworzy, i przekazuje mu niezbędne informacje: żądanie HTTP, sesję, konfigurację aplikacji;
    • pobierz [2c] odpowiedź kontrolera pomocniczego. W zależności od typu (jSON, XML, HTML) aplikacji żądanej przez klienta, należy wybrać odpowiedź (JsonResponse, XmlResponse, HtmlResponse) odpowiedzialną za wysłanie odpowiedzi do klienta i przekazać jej wszystkie potrzebne informacje (żądanie HTTP, sesję, konfigurację aplikacji, odpowiedź kontrolera pomocniczego);
    • po wysłaniu tej odpowiedzi ([3c]) należy zwolnić zasoby, które mogły zostać wykorzystane do przetworzenia żądania;

23.9.2. [main.php] - 1

Kod kontrolera głównego [main.php] jest następujący:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;

// obsługa błędów przez PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// pobieranie konfiguracji
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// błąd?
if (!$fileContents) {
  // odnotowujemy błąd
  $état = 131;
  $erreur = TRUE;
  $message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
  // pobieramy kod JSON z pliku konfiguracyjnego do tablicy asocjacyjnej
  $config = \json_decode($fileContents, true);
  // błąd?
  if (!$config) {
    // odnotowuje się błąd
    $erreur = TRUE;
    $état = 132;
    $message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
  }
}
// błąd?
if ($erreur) {
  // przygotowywanie odpowiedzi JSON z serwera
  // nie można skorzystać z pliku konfiguracyjnego
  // zależności symfony
  require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
  // przygotowywanie odpowiedzi
  $response = new Response();
  $response->headers->set("content-type", "application/json");
  $response->setCharset("utf-8");
  // kod statusu
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  // treść
  $response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
  // wysyłanie
  $response->send();
  // koniec
  exit;
}

Uwagi

  • wiersze 10–12: główny kontroler wykorzystuje następujące obiekty Symfony:
    • [Request]: przetwarzane zapytanie HTTP;
    • [Session]: sesja aplikacji internetowej;
    • [Response]: odpowiedź HTTP dla klienta;
  • wiersz 15: przez cały czas trwania prac programistycznych ten wiersz pozostanie skomentowany: błędy PHP są wówczas włączane do strumienia tekstowego wysyłanego do klienta. Jeśli klientem jest przeglądarka, pozwala to na wyświetlenie błędów napotkanych przez serwer. Stanowi to pomoc w debugowaniu;
  • wiersz 16: zgłaszane są wszystkie błędy (E_ALL) z wyjątkiem ostrzeżeń (! E_WARNING) i komunikatów o charakterze niekrytycznym (! E_NOTICE). Na przykład, jeśli nie można otworzyć pliku, PHP generuje błąd typu [E_NOTICE]. Jeśli w wierszu 15 włączono wyświetlanie błędów, błąd otwarcia pliku pojawi się w przeglądarce klienta. To dobrze, jeśli zapomnieliście przetestować wynik otwarcia pliku, gorzej jednak, jeśli zaplanowaliście ten test: wówczas wiersz o kodzie [notice] zakłóca odpowiedź serwera dla klienta. Na etapie rozwoju wiersz 16 również powinien zostać skomentowany: nie chcecie przecież przegapić żadnego błędu;
  • wiersz 19: odczytywany jest plik konfiguracyjny;
  • wiersze 22–27: jeśli odczyt nie powiódł się, odnotowujemy błąd (wiersz 25), ustawiamy aplikację w stanie [131] i przygotowujemy komunikat o błędzie;
  • wiersz 30: dekodowany jest ciąg znaków jSON z pliku konfiguracyjnego;
  • wiersze 32–37: jeśli dekodowanie nie przebiegnie pomyślnie, odnotowuje się błąd (wiersz 34), przełącza się aplikację do stanu [132] i przygotowuje komunikat o błędzie;
  • wiersze 40–57: w przypadku błędu odczytu pliku konfiguracyjnego nie można kontynuować działania. Przygotowuje się wówczas odpowiedź jSON dla klienta:
  • wiersz 44: ponieważ plik konfiguracyjny nie został odczytany, należy ręcznie zaimportować plik [autoload] niezbędny dla [Symfony];
  • wiersze 46–47: przygotowujemy odpowiedź o kodzie jSON;
  • wiersz 50: kod HTTP odpowiedzi będzie wynosił 500 INTERNAL_SERVER_ERROR;
  • wiersz 52: ustalamy treść odpowiedzi jSON. Wszystkie odpowiedzi generowane przez analizowaną aplikację internetową będą zawierały trzy klucze:
      • [action]: akcja żądana przez klienta;
      • [état]: stan aplikacji po wykonaniu tej akcji;
      • [réponse]: odpowiedź serwera internetowego;
  • wiersz 54: odpowiedź jSON jest wysyłana do klienta;

23.9.3. Testy [Postman] – 1

Sprawdzimy zachowanie serwera w przypadku braku lub nieprawidłowości pliku konfiguracyjnego:

Image

Zgromadzimy różne żądania, które nasz klient [Postman] wyśle do serwera podatkowego, w kolekcjach.

  • W [1] utwórz nową kolekcję;
  • w [2] nadaj jej nazwę;
  • w [3] opis jest opcjonalny;

Image

  • w kolekcjach [4] pojawia się teraz kolekcja o nazwie [impots-server-tests-version12] [5];
  • w [6] można dodać nowe zapytanie do kolekcji;

Image

  • w [7] nadaje się nazwę zapytaniu;
  • w [8] opis jest opcjonalny;

Image

  • w [9-11] zapytanie dodano do kolekcji;
  • w [12] wybiera się typ zapytania, w tym przypadku zapytanie [GET]. W [19] dostępne są różne typy zapytań;
  • w [13] wpisuje się tutaj identyfikator serwera URL;
  • w [14] należy wprowadzić tutaj parametry dodane do URL, które będą zatem parametrami GET. Zaletą umieszczenia ich tutaj, a nie bezpośrednio w pliku URL, jest to, że zostaną one zakodowane w formacie URL przez plik [Postman]. Jeśli umieścisz je samodzielnie w pliku URL, to Ty będziesz musiał je zakodować w formacie URL;
  • w pliku [15] plik [Authorization] służy do określenia użytkownika, który się zaloguje. Nie będziemy musieli korzystać z tej możliwości;
  • w pliku [16] znajdują się nagłówki HTTP, które będą towarzyszyć żądaniu. Niektóre nagłówki są automatycznie dołączane do żądania. W tym miejscu można dodać nowe;
  • W przypadku [17], [Body] oznacza parametry operacji [POST]. Będziemy musieli skorzystać z tej opcji;

Przeprowadzimy następujący test:

  • w operacji [main.php] wskazujemy, że plikiem konfiguracyjnym jest [config2.json], który nie istnieje:

Image

  • należy usunąć komentarz z linii 16 kodu;
  • wiersz 18: błąd dotyczący nazwy pliku konfiguracyjnego;

Przejdźmy do pliku [Postman], [13, 20] oraz URL serwera obliczającego podatek i uruchommy plik [21]:

Image

Odpowiedź zwrócona przez serwer (oczywiście pod warunkiem, że Laragon jest aktywny) wygląda następująco:

Image

  • w [22] serwer zwrócił kod HTTP [500 Internal Server Error];
  • w przypadku [23], [Body] oznacza treść odpowiedzi, tj. dokument wysłany przez serwer za nagłówkami HTTP [28];
  • w [26] widać, że [Postman] otrzymało odpowiedź jSON;
  • w [27] znajduje się sformatowana odpowiedź jSON;
  • w pliku [28] znajduje się nieformatowana, surowa odpowiedź jSON;
  • w [29], tryb [Preview] jest używany, gdy odpowiedź ma postać HTML. Tryb [Preview] wyświetla wówczas otrzymaną stronę;
  • w przypadku [30] – odpowiedź serwera jSON. Jest to właśnie ta, na którą czekaliśmy;

W trybie [25] nagłówki HTTP wysłane w odpowiedzi serwera są następujące:

Image

  • w [32] typ odpowiedzi to jSON;

Ten pierwszy test pozwolił nam stwierdzić, że:

  • można wysłać dowolny typ żądania do testowanego serwera;
  • można ustawić parametry GET lub POST;
  • mamy dostęp do całej odpowiedzi: nagłówki HTTP oraz dokument następujący po tych nagłówkach [Body];

Teraz przeprowadźmy drugi test:

Image

  • w [1-3], plik [config3.json] jest plikiem jSON o nieprawidłowej składni;
  • w pliku [4] plik [main.php] jest skonfigurowany do korzystania z pliku [config3.json];

Dodajemy nowe zapytanie w pliku [Postman]:

Image

  • W [1-3] klikamy prawym przyciskiem myszy na [2] i wybieramy opcję [duplicate], aby zduplikować zapytanie [2];
  • w [4] nowa zapytanie ma domyślną nazwę, którą zmieniamy na [5];

Image

  • w [6], nazwa zapytania została zmieniona;
  • na [9-10], wysyłamy to samo zapytanie GET, co poprzednio;

Image

  • w [11], odpowiedź serwera jSON;

W niniejszym artykule pokazaliśmy, w jaki sposób będą testowane różne operacje serwisu internetowego służącego do obliczania podatku.

23.9.4. [main.php] – 2

Wracamy do analizy kodu głównego kontrolera [main.php]:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;

// obsługa błędów przez PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// pobieramy konfigurację
$configFilename = "config.json";

// dołączamy zależności niezbędne dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require_once "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require_once "$dependency";
}

// tworzenie pliku logów
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // nie udało się utworzyć pliku logów – wewnętrzny błąd serwera
  $é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éé"],
    []);
  // zakończono
  exit;
}

Komentarze

  • wiersz 18: mamy plik konfiguracyjny [config.json], który obecnie istnieje i jest poprawny pod względem składniowym. Należy ponadto sprawdzić, czy klucze oczekiwane w tym pliku rzeczywiście się w nim znajdują. Uznamy, że jest to część normalnej pracy programisty związanej z debugowaniem. Takie samo rozumowanie moglibyśmy zastosować w przypadku dwóch poprzednich błędów;
  • wiersze 20–28: dołączamy wszystkie zależności niezbędne dla projektu internetowego. Z tym kodem mieliśmy już do czynienia kilkakrotnie;
  • wiersze 31–43: próbujemy utworzyć obiekt [Logger], który pozwoli nam rejestrować zdarzenia w pliku [$config['logsFilename']]. Tworzenie tego obiektu może się nie powieść;
  • wiersze 33–43: obsługa błędu podczas tworzenia obiektu [Logger];
  • wiersz 35: ustalanie numeru statusu;
  • wiersze 36–40: wysyłanie odpowiedzi jSON;
  • wiersz 42: zatrzymanie skryptu;

Wszystkie odpowiedzi wysyłane do klienta implementują następujący interfejs [InterfaceResponse]:

Image

Kod interfejsu [InterfaceResponse] jest następujący:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

interface InterfaceResponse {

  // Żądanie $request: żądanie jest przetwarzane
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł rejestrujący służący do zapisywania logów
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}
  • wiersze 19–27: interfejs [InterfaceResponse] posiada jedną metodę [send] służącą do wysyłania odpowiedzi do klienta;
  • wiersze 11–17: znaczenie poszczególnych parametrów metody [send];
  • wiersze 23–25: parametry [$statusCode, $content, $headers] znajdują się w standardowym wyniku kontrolerów pomocniczych aplikacji. Jednak odpowiedź może wymagać dodatkowych informacji. Dlatego przekazujemy jej trzy pierwsze parametry (wiersze 20–22), które zapewniają jej dostęp do wszystkich informacji dotyczących żądania, sesji i konfiguracji;
  • wiersz 26: odpowiedź wymaga parametru [Logger], ponieważ będzie rejestrować odpowiedź wysłaną do klienta;

Klasa [JsonResponse] implementuje interfejs [InterfaceResponse] w następujący sposób:


<?php

namespace Application;

// zależności 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 {

  // Żądanie $request: żądanie w trakcie przetwarzania
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł logujący do zapisywania logów

  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {

    // przygotowanie serializatora symfony
    $serializer = new Serializer(
      [
      // niezbędny do serializacji obiektów
      new ObjectNormalizer()],
      // koder jSON
      // w przypadku opcji należy umieścić OU pomiędzy poszczególnymi opcjami
      [new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
    );
    // serializacja jSON
    $json = $serializer->serialize($content, 'json');
    // nagłówki
    $headers = array_merge($headers, ["content-type" => "application/json"]);
    // wysyłanie odpowiedzi
    parent::sendResponse($statusCode, $json, $headers);
    // log
    if ($logger !== NULL) {
      $logger->write("réponse=$json\n");
    }
  }

}

Komentarze

  • wiersz 13: klasa implementuje interfejs [InterfaceResponse];
  • wiersz 13: klasa dziedziczy po klasie [ParentResponse]. Wszystkie typy klasy [Response] dziedziczą po tej klasie. To właśnie ta klasa nadrzędna wysyła odpowiedź do klienta (wiersz 46). Ponieważ kod ten był wspólny dla wszystkich typów klasy [Response], został on wyodrębniony do klasy nadrzędnej;
  • wiersze 33–40: instancjonowanie serializatora [Symfony], który przekształci odpowiedź serwera [$content] w ciąg znaków jSON (wiersz 42);
  • wiersze 34–36: pierwszym parametrem konstruktora klasy [Serializer] jest tablica. W tablicy tej umieszcza się instancję klasy [ObjectNormalizer] niezbędną do serializacji obiektów. W tej aplikacji sytuacja ta występuje w przypadku listy symulacji, gdzie każda symulacja jest instancją klasy [Simulation];
  • wiersz 39: drugi parametr konstruktora klasy [Serializer] jest również tablicą: umieszcza się w nim wszystkie kodery używane podczas serializacji (XML, jSON, CSV…);
  • wiersz 39: w tym przypadku będzie tylko jeden enkoder typu [JsonEncoder]. Wystarczyłby konstruktor bez parametrów. W tym przypadku przekazaliśmy do konstruktora parametr [JsonEncode] wyłącznie w celu przekazania opcji kodowania jSON;
  • wiersz 39: parametr konstruktora [JsonEncode] jest tablicą opcji. W tym przypadku używamy opcji [JSON_UNESCAPED_UNICODE], aby zastrzec, że znaki UTF-8 w ciągu jSON mają być renderowane natywnie, a nie „eskapowane”;
  • wiersz 42: treść odpowiedzi HTTP jest serializowana do postaci jSON za pomocą poprzedniego serializatora;
  • wiersz 44: dodajemy nagłówek HTTP, który informuje klienta, że wyślemy mu plik jSON;
  • wiersz 46: prosimy klasę nadrzędną o wysłanie odpowiedzi do klienta;
  • wiersze 48–50: rejestrujemy odpowiedź jSON;

Kod klasy nadrzędnej [ParentResponse] wygląda następująco:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Response;

class ParentResponse {

  // int $statusCode: kod statusu odpowiedzi HTTP
  // ciąg znaków $content: treść odpowiedzi do wysłania
  // w zależności od przypadku jest to ciąg znaków jSON, XML, HTML
  // tablica $headers: nagłówki HTTP, które należy dodać do odpowiedzi

  public function sendResponse(
    int $statusCode,
    string $content,
    array $headers): void {

    // przygotowanie tekstowej odpowiedzi serwera
    $response = new Response();
    $response->setCharset("utf-8");
    // kod statusu
    $response->setStatusCode($statusCode);
    // nagłówki
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // wysyłanie odpowiedzi
    $response->setContent($content);
    $response->send();
  }
}

Komentarze

  • wiersze 10–13: znaczenie trzech parametrów metody [send];
  • wiersz 17: należy zauważyć, że treść odpowiedzi ma typ [string] i jest zatem gotowa do wysłania (wiersz 30);
  • wiersz 22: odpowiedź będzie zawierała znaki UTF-8;
  • wiersz 24: kod statusu odpowiedzi HTTP;
  • wiersze 26–28: dodanie nagłówków o formacie HTTP podanych przez kod wywołującego;
  • wiersze 30–31: wysłanie odpowiedzi do klienta;

Szczegółowo opisaliśmy cały cykl odpowiedzi jSON. Nie będziemy do tego wracać w dalszej części. Należy jedynie pamiętać o sygnaturze interfejsu [InterfaceResponse]:


interface InterfaceResponse {

  // Żądanie $request: żądanie jest przetwarzane
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł rejestrujący służący do zapisywania logów
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}

Główny kontroler [main.php] będzie musiał zachować tę sygnaturę za każdym razem, gdy zażąda wysłania odpowiedzi do klienta.

23.9.5. Testy [Postman] – 2

Modyfikujemy plik [config.json] w następujący sposób:

Image

  • w pliku [1] wskazujemy, że plikiem logów jest [Logs], który jest folderem o nazwie [2]. Utworzenie pliku [Logs] powinno zatem zakończyć się niepowodzeniem;

Tworzymy nowe żądanie [Postman] [3], o nazwie [erreur-133]:

Image

  • [2-4]: definiujemy to samo zapytanie, co w dwóch poprzednich testach;
  • [5-7]: otrzymujemy oczekiwaną odpowiedź jSON;

23.9.6. [main.php] – 3

Kontynuujmy analizę głównego kontrolera [main.php]:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;

// obsługa błędów przez PHP


// tworzenie pliku logów


// pierwszy wpis w dzienniku
$logger->write("\n---nouvelle requête\n");
// bieżące żądanie
$request = Request::createFromGlobals();

// sesja
$session = new Session();
$session->start();
// lista błędów
$erreurs = [];
$erreur = FALSE;
// obsługa żądanej akcji
if (!$request->query->has("action")) {
  $erreurs[] = "paramètre [action] manquant";
  $erreur = TRUE;
  $état = 101;
  $action = "";
} else {
  // zapisywanie akcji
  $action = strtolower($request->query->get("action"));
}
// rejestruje się akcję
$logger->write("action [$action] demandée\n");

// czy akcja istnieje?
if (!$erreur && !array_key_exists($action, $config["actions"])) {
  $erreurs[] = "action [$action] invalide";
  $erreur = TRUE;
  $état = 102;
}

// przed wykonaniem niektórych działań należy znać typ sesji
if (!$erreur && !$session->has("type") && $action !== "init-session") {
  $erreurs[] = "pas de session en cours. Commencer par action [init-session]";
  $erreur = TRUE;
  $état = 103;
}

// w przypadku niektórych działań konieczne jest uwierzytelnienie
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
  $erreurs[] = "action demandée par utilisateur non authentifié";
  $erreur = TRUE;
  $état = 104;
}

// błędy?
if ($erreurs) {
  // przygotowuje się odpowiedź bez jej wysyłania  
  $statusCode = Response::HTTP_BAD_REQUEST;
  $content = ["réponse" => $erreurs];
  $headers = [];
} else {
  // ---------------------------
  // wykonuje się akcję za pomocą jej kontrolera
  $controller = __NAMESPACE__ . $config["actions"][$action];
  $logger->write("contrôleur : $controller\n");
  list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}

// --------------------- wysyłamy odpowiedź
// przypadek błędu krytycznego HTTP_INTERNAL_SERVER_ERROR
// wysyła się wiadomość e-mail do administratora, jeśli jest to możliwe
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();
}
// odpowiedź zależy od typu sesji
if ($session->has("type")) {
  // typ sesji znajduje się w sesji
  $type = $session->get("type");
} else {
  // jeśli w sesji nie ma typu, to domyślnie będzie to odpowiedź w formacie jSON
  $type = "json";
}
// do odpowiedzi kontrolera dodajemy klucze [action, état]
$content = ["action" => $action, "état" => $état] + $content;
// tworzymy instancję obiektu [Response] odpowiedzialnego za wysłanie odpowiedzi do klienta
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);

// odpowiedź została wysłana – zwalnia się zasoby
$logger->close();
exit;

Uwagi

  • po przeprowadzeniu wstępnych weryfikacji i upewnieniu się, że może działać, główny kontroler zajmuje się zleceniem, które otrzymał: musi ono spełniać określone warunki;
  • wiersz 21: rejestrujemy fakt, że otrzymaliśmy nowe żądanie. Nie mogliśmy tego zrobić wcześniej, ponieważ nie mieliśmy pewności, czy dysponujemy prawidłowym plikiem logów;
  • wiersz 23: wszystkie informacje z żądania klienta są umieszczane w obiekcie Symfony [Request];
  • wiersz 26: rozpoczynamy nową sesję lub pobieramy istniejącą, jeśli taka istnieje;
  • wiersz 27: sesja zostaje aktywowana;
  • wiersz 29: tablica komunikatów o błędach;
  • wiersz 30: wartość logiczna, która w trakcie testów informuje, czy wystąpił błąd, czy nie;
  • wiersz 32: parametr [action] musi być częścią parametru URL w postaci [main.php?action=uneAction]. Parametr [action] jest zatem częścią parametrów [$request→query];
  • wiersze 33–36: przypadek braku parametru [action] w URL. Błąd zostaje odnotowany i przypisany jest mu stan [101];
  • wiersz 39: jeśli parametr [action] występuje w URL, jest on zapisywany;
  • wiersz 42: typ akcji jest rejestrowany;
  • wiersze 45–49: jeśli występuje parametr [action], musi on być prawidłowy. Wszystkie dozwolone działania są zdefiniowane w tablicy asocjacyjnej [$config["actions"]];
  • wiersze 46–48: jeśli akcja jest nieprawidłowa, odnotowuje się błąd i przypisuje jej się stan [102];
  • wiersze 52–56: mamy do czynienia z prawidłową akcją. Musi ona jednak spełnić jeszcze inne warunki. Aplikacja internetowa zwraca trzy rodzaje odpowiedzi (jSON, XML, HTML). Ten typ jest ustalany przez akcję [init-session]. Akcja ta umieszcza typ sesji w kluczu [type];
  • wiersz 52: poza akcją [init-session] każda inna akcja musi przebiegać z kluczem [type] w sesji;
  • wiersze 53–55: jeśli tak nie jest, odnotowuje się błąd i przypisuje się mu stan [103];
  • wiersze 58–63: z wyjątkiem akcji [init-session] i [authentifier-utilisateur] wszystkie pozostałe akcje muszą być wykonywane po uwierzytelnieniu. Uwierzytelnianie odbywa się za pomocą akcji [authentifier-utilisateur], która w przypadku pomyślnego uwierzytelnienia umieszcza klucz [user] w sesji;
  • wiersz 59: jeśli akcja nie jest ani [init-session], ani [authentifier-utilisateur], a klucz [user] nie znajduje się w sesji, wówczas występuje błąd;
  • wiersze 60–62: odnotowuje się błąd i przypisuje mu stan [104];
  • wiersze 66–71: sprawdzamy, czy tablica [$erreurs] nie jest pusta. Jeśli tak jest, oznacza to, że żądana akcja lub jej kontekst wykonania są nieprawidłowe;
  • wiersze 68–70: przygotowuje się odpowiedź, która ma zostać wysłana do klienta, ale na razie nie jest ona wysyłana;
  • wiersz 68: kod statusu HTTP;
  • wiersz 69: treść odpowiedzi;
  • wiersz 70: nagłówki do dodania do odpowiedzi, tutaj brak;
  • wiersz 73: mamy prawidłową akcję. Poprosimy jej kontroler (drugorzędny) o jej przetworzenie;
  • wiersz 74: tworzymy nazwę klasy kontrolera, który ma zostać uruchomiony. [__NAMESPACE__] to przestrzeń nazw, w której się znajdujemy, w tym przypadku [Application] (wiersz 7);
  • nazwy klas kontrolerów pomocniczych znajdują się w pliku [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"
            },

Każdej akcji odpowiada kontroler pomocniczy. Jeśli akcja to [authentifier-utilisateur], zmienna [$controller] w wierszu 74 będzie miała wartość [Application/AuthentifierUtilisateurController];

  • wiersz 75: rejestruje się nazwę kontrolera pomocniczego w celu weryfikacji w trakcie programowania;
  • wiersz 76: uruchamiany jest kontroler pomocniczy. Do kontrolerów pomocniczych wrócimy nieco później;
  • wiersz 76: wszystkie kontrolery pomocnicze zwracają ten sam typ wyniku, którym jest tablica:
    • pierwszy element tablicy [$statusCode] to kod statusu HTTP odpowiedzi, którą należy wysłać;
    • drugi element [$état] to stan aplikacji po wykonaniu kontrolera;
    • trzeci element [$content] to tablica asocjacyjna z unikalnym kluczem [réponse], który stanowi treść odpowiedzi, jaką należy wysłać do klienta;
    • czwarty element [$headers] to tablica nagłówków HTTP, które należy dodać do odpowiedzi wysyłanej do klienta;
  • wiersz 79: dochodzimy do tego miejsca:
    • albo dlatego, że wystąpił błąd (wiersze 68–70);
    • albo po wykonaniu kontrolera (wiersze 72–76);
    • w obu przypadkach znane są elementy [$statusCode, $état, $content, $headers] niezbędne do przygotowania odpowiedzi dla klienta;
  • wiersze 82–87: dotyczą szczególnego przypadku kodu statusu [500 Internal Server Error]. Jeśli kontroler ustawił ten kod statusu, oznacza to, że aplikacja nie może działać. Dzieje się tak na przykład w przypadku obliczania podatku, jeśli używany moduł SGBD nie został uruchomiony lub przestał odpowiadać. Wówczas wysyłana jest wiadomość e-mail do administratora aplikacji w celu powiadomienia go o tym fakcie. Nie będziemy szczegółowo omawiać tego kodu. Sposób wykorzystania klasy [SendAdminMail] został już przedstawiony (akapit z linkiem);
  • wiersze 89–95: określa się typ [jSON, XML, HTML] aplikacji internetowej. Jeśli akcja [init-session] została pomyślnie wykonana, typ ten znajduje się w sesji powiązanej z kluczem [type] (wiersz 91). Jeśli tak nie jest, wówczas arbitralnie ustala się typ odpowiedzi, którym jest typ jSON (wiersz 94);
  • wiersz 97: [$content] to tablica zawierająca jeden klucz [réponse] i jedną wartość – treść odpowiedzi, która ma zostać wysłana do klienta. Dodajemy do niej klucze [action] i [état]. Klucz [action] pozwoli na lepsze śledzenie logów w pliku [logs.txt]. Klucz [état] będzie pełnił dwie role:
    • pozwoli klientom jSON i XML sprawdzić, w jakim stanie znajduje się aplikacja internetowa po wykonaniu danej akcji;
    • w przypadku odpowiedzi HTML umożliwi wybór widoku HTML, który należy wysłać do przeglądarki klienta;
  • wiersz 99: wybieramy typ klasy [Response], która ma zostać uruchomiona w celu wysłania odpowiedzi do klienta;

Klasę [JsonResponse] przedstawiliśmy już w akapicie dotyczącym linków. Implementuje ona interfejs [InterfaceResponse] i rozszerza klasę [ParentResponse]. Tak samo jest w przypadku dwóch pozostałych klas: [XmlResponse] i [HtmlResponse].

Odpowiedzi zebrano w folderze [Responses]:

Image

Wszystkie te klasy implementują interfejs [InterfaceResponse], przedstawiony również w akapicie [link]:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

interface InterfaceResponse {

  // Żądanie $request: żądanie w trakcie przetwarzania
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł rejestrujący do zapisywania logów
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}

Interfejs ten posiada jedną metodę [send], której zadaniem jest wysłanie odpowiedzi do klienta. Metoda ta posiada 7 parametrów opisanych w wierszach 11–17. Wszystkie klasy i interfejsy w folderze [Responses] znajdują się w przestrzeni nazw [Application] (wiersz 3).

Wróćmy do kodu klasy [main.php]:



// do odpowiedzi kontrolera dodaje się klucze [action, état]
$content = ["action" => $action, "état" => $état] + $content;
// tworzymy instancję obiektu [Response] odpowiedzialnego za wysłanie odpowiedzi do klienta
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);

// odpowiedź została wysłana – zwalnia się zasoby
$logger->close();
exit;
  • wiersz 5: tworzona jest instancja klasy [Response], która odpowiada typowi aplikacji. Klasy te są zdefiniowane w pliku [config.json] w następujący sposób:

"types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
  • wiersz 5: nazwa klasy jest poprzedzona prefiksem jej przestrzeni nazw;
  • wiersz 6: tworzona jest instancja klasy [Response] i wywoływana jest jej metoda [send] z 7 oczekiwanymi parametrami. Są to parametry interfejsu [InterfaceResponse], który implementują wszystkie klasy odpowiedzi. Spowoduje to wysłanie odpowiedzi do klienta;
  • wiersz 9: zamyka się plik logów;
  • wiersz 10: główny kontroler zakończył swoją pracę;

23.9.7. Testy [Postman] – 3

Przetestujemy różne przypadki błędów parametru [action] w klasie URL.

Image

  • w [1]:
    • [erreur-101]: przypadek braku parametru [action] w URL;
    • [erreur-102]: przypadek, w którym parametr [action] występuje w pliku URL, ale nie jest rozpoznawany;
    • [erreur-103]: przypadek, w którym parametr [action] występuje w URL, został rozpoznany, ale nie zdefiniowano oczekiwanego typu odpowiedzi [json, xml, html];

Każde zapytanie zostało wykonane. Poniżej przedstawiamy uzyskane wyniki:

Powyżej:

  • w [2-4] zapytanie bez parametru [action] w URL [4];
  • w [5-7] – wynik jSON;

Image

Powyżej:

  • w [5-9], zapytanie z nieprawidłowym parametrem [action];
  • w [10-13], odpowiedź jSON;

Image

Powyżej:

  • w [14-19] – rozpoznano akcję, ale typ (json, xml, html) nie został jeszcze określony;
  • w [20-23] – odpowiedź serwera jSON;

23.10. Kontrolery pomocnicze

Każda akcja jest wykonywana przez jeden z kontrolerów z folderu [Controllers]:

Image

Image

W ogólnej architekturze powyższej aplikacji kontrolery podrzędne znajdują się w [2a].

Każdy kontroler implementuje następujący interfejs [InterfaceController]:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

interface InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos=NULL): array;
}

Uwagi

  • wszystkie kontrolery podrzędne są uruchamiane za pomocą metody [execute] z linii 17. Do tej metody przekazywane są informacje znane kontrolerowi głównemu:
    • wiersz 18: [array $config], który zawiera konfigurację aplikacji;
    • wiersz 19: [Request $request], czyli żądanie HTTP, które jest aktualnie przetwarzane;
    • wiersz 20: [Session $session], czyli bieżąca sesja aplikacji internetowej;
    • wiersz 21: [array $infos=NULL], czyli dodatkowa tablica informacji dla kontrolera na wypadek, gdyby trzy pierwsze parametry metody okazały się niewystarczające. W tej aplikacji parametr ten nigdy nie został wykorzystany. Znajduje się tam ze względów bezpieczeństwa;
  • wiersz 21: metoda [execute] zwraca tablicę [$statusCode, $état, $content, $headers]
    • [int $statusCode]: kod statusu odpowiedzi HTTP;
    • [int $état]: stan, w jakim znajduje się aplikacja po zakończeniu wykonywania;
    • [array $content]: tablica asocjacyjna [réponse=>résultat], w której [résultat] jest dowolnego typu: jest to wynik wygenerowany przez kontroler, który zostanie wysłany do klienta po zserializowaniu go w postaci ciągu znaków;
    • [array $headers]: lista nagłówków HTTP, które mają zostać dołączone do odpowiedzi serwera HTTP;

Każdy kontroler pomocniczy jest wywoływany przez następujący kod kontrolera głównego:


// akcja jest wykonywana za pomocą jej kontrolera
 $controller = __NAMESPACE__ . $config["actions"][$action];
 list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);

W wierszu 3 widać, że czwarty parametr [array $infos=NULL] metody [execute] nie jest używany.

23.11. Działania

Przeanalizujemy teraz różne możliwe akcje serwisu internetowego:

Akcja
Rola
Kontekst wykonania
init-session
Służy do określenia typu (json, xml, html) oczekiwanych odpowiedzi
Żądanie GET main.php?action=init-session&type=x
może zostać wysłane w dowolnym momencie
uwierzytelnianie użytkownika
Zezwala lub nie zezwala użytkownikowi na zalogowanie się
Żądanie POST main.php?action=authentifier-utilisateur
Żądanie musi zawierać dwa parametry wysyłane metodą POST [user, password]
Może zostać wysłane tylko wtedy, gdy typ sesji (json, xml, html) jest znany
oblicz-podatek
Przeprowadza symulację obliczenia podatku
Żądanie POST main.php?action=obliczyć-podatek
Żądanie musi zawierać trzy parametry wysyłane metodą POST: [marié, enfants, salaire]
Może zostać wysłane tylko wtedy, gdy znany jest typ sesji (json, xml, html) i użytkownik jest uwierzytelniony
lister-simulations
Żądanie wyświetlenia listy symulacji przeprowadzonych od początku sesji
Żądanie GET main.php?action=lister-simulations
Żądanie nie akceptuje żadnych innych parametrów
Może zostać wysłane tylko wtedy, gdy typ sesji (json, xml, html) jest znany, a użytkownik jest uwierzytelniony
usuń-symulację
Usuwa symulację z listy symulacji
Żądanie GET main.php?action=lister-simulations&numer=x
Żądanie nie akceptuje żadnych innych parametrów
Może zostać wysłane tylko wtedy, gdy typ sesji (json, xml, html) jest znany, a użytkownik jest uwierzytelniony
zakończenie-sesji
Zakończenie sesji symulacji.
Z technicznego punktu widzenia poprzednia sesja internetowa zostaje usunięta, a tworzona jest nowa sesja
Może zostać wysłane tylko wtedy, gdy znany jest typ sesji (json, xml, html) i użytkownik jest uwierzytelniony

Wszystkie kontrolery podrzędne postępują w ten sam sposób:

  • sprawdzają swoje parametry. Parametry te znajdują się w obiekcie [Request→query] dla parametrów obecnych w URL oraz w obiekcie [Request→request] dla tych, które są wysyłane (żądanie POST);
  • kontroler przypomina funkcję lub metodę, która sprawdza poprawność swoich parametrów. W przypadku kontrolera sprawa jest jednak nieco bardziej skomplikowana:
    • oczekiwane parametry mogą nie występować;
    • wszystkie oczekiwane parametry są ciągami znaków, podczas gdy funkcja może określić typ swoich parametrów. Jeśli oczekiwanym parametrem jest liczba, należy sprawdzić, czy ciąg znaków parametru rzeczywiście odpowiada liczbie;
    • po sprawdzeniu, czy oczekiwane parametry są obecne i poprawne pod względem składniowym, należy sprawdzić, czy są one prawidłowe w bieżącym kontekście wykonania. Kontekst ten jest zawarty w sesji. Przykładem kontekstu wykonania jest uwierzytelnianie. Niektóre działania mogą być realizowane dopiero po uwierzytelnieniu klienta. Zazwyczaj klucz w sesji wskazuje, czy uwierzytelnienie miało miejsce, czy nie;
    • po przeprowadzeniu powyższych weryfikacji kontroler pomocniczy może rozpocząć pracę. Ta weryfikacja parametrów jest bardzo ważna. Nie można zaakceptować sytuacji, w której klient wysyła nam dowolne dane w dowolnym momencie cyklu życia aplikacji. Musimy mieć pełną kontrolę nad jej cyklem życia;
    • po zakończeniu swojej pracy kontroler pomocniczy zwraca tablicę [$statusCode, $état, $content, $headers], której oczekuje kontroler główny, który go wywołał;

Przejdziemy teraz do omówienia poszczególnych kontrolerów, czyli – co sprowadza się do tego samego – poszczególnych akcji, które wyznaczają rytm działania aplikacji internetowej.

23.11.1. Akcja [init-session]

Akcja [init-session] jest przetwarzana przez następujący kontroler [InitSessionController]:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;

class InitSessionController implements InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // musi istnieć GET oraz jeden parametr inny niż [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], []];
    }
    // pobieramy parametry z GET
    $erreur = FALSE;
    // typ
    if (!$request->query->has("type")) {
      $erreur = TRUE;
      $état = 702;
      $message = "paramètre [type] manquant";
    } else {
      $type = strtolower($request->query->get("type"));
    }
    // weryfikacja typu
    if (!$erreur && !array_key_exists($type, $config["types"])) {
      $erreur = TRUE;
      $état = 703;
      $message = "paramètre type [$type] invalide";
    }
    // błąd?
    if ($erreur) {
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // ustawiamy typ sesji w sesji
    $session->set("type", $type);
    // komunikat o pomyślnym zakończeniu
    $message = "session démarrée avec type [$type]";
    $état = 700;
    return [Response::HTTP_OK, $état, ["réponse" => $message], []];
  }

}

Uwagi

  • oczekuje się na żądanie [GET main.php?action=init-session&type=xxx]
  • wiersze 25–26: sprawdzamy, czy żądanie jest żądaniem GET z dwoma parametrami w URL;
  • wiersze 27–31: jeśli tak nie jest, odnotowuje się błąd i wysyła wynik [$statusCode, $état, $content, $headers] do głównego kontrolera;
  • wiersze 35–39: sprawdzamy, czy parametr [type] rzeczywiście występuje w URL. Jeśli tak nie jest, odnotowujemy błąd;
  • wiersz 40: odnotowuje się typ sesji;
  • wiersze 43–47: sprawdza się, czy typ sesji jest jednym z podanych terminów (json, xml, html). Jeśli tak nie jest, odnotowuje się błąd;
  • wiersze 49–51: jeśli wystąpił błąd, wysyłamy wynik [$statusCode, $état, $content, $headers] do głównego kontrolera;
  • wiersz 53: typ sesji jest zapisywany w sesji aplikacji internetowej;
  • wiersze 55–57: kontroler zakończył swoją pracę. Wysyłamy wynik [$statusCode, $état, $content, $headers] oznaczający sukces do kontrolera głównego;

Przypomnijmy, co robi kontroler główny z odpowiedziami kontrolerów podrzędnych:


// błędy?
if ($erreurs) {
  // przygotowuje się odpowiedź bez jej wysyłania  
  $statusCode = Response::HTTP_BAD_REQUEST;
  $content = ["réponse" => $erreurs];
  $headers = [];
} else {
  // ---------------------------
  // wykonujemy akcję za pomocą jej kontrolera
  $controller = __NAMESPACE__ . $config["actions"][$action];
  $logger->write("contrôleur : $controller\n");
  list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}

// --------------------- wysyłanie odpowiedzi
// przypadek błędu krytycznego HTTP_INTERNAL_SERVER_ERROR
// wysyła się wiadomość e-mail do administratora, jeśli jest to możliwe
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();
}
// odpowiedź zależy od typu sesji
if ($session->has("type")) {
  // typ sesji znajduje się w sesji
  $type = $session->get("type");
} else {
  // jeśli w sesji nie ma typu, to domyślnie będzie to odpowiedź w formacie jSON
  $type = "json";
}
// do odpowiedzi kontrolera dodaje się klucze [action, état]
$content = ["action" => $action, "état" => $état] + $content;
// tworzymy instancję obiektu [Response] odpowiedzialnego za wysłanie odpowiedzi do klienta
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);

// odpowiedź została wysłana – zwalnia się zasoby
$logger->close();
exit;
  • wiersz 12: kontroler główny pobiera wynik z kontrolera pomocniczego;
  • wiersze 35–36: po przeprowadzeniu kilku sprawdzeń wysyła odpowiedź, instancjonując jedną z klas [JsonResponse, XmlResponse, HtmlResponse] w zależności od typu (json, xml, html) bieżącej sesji;

W dalszej części przeprowadzimy testy klasy [Postman] w ramach sesji symulacyjnej z typem [json]. Działanie klasy [JsonResponse] zostało przedstawione w akapicie [link].

23.11.2. Testy [Postman]

Image

Powyżej:

  • w [2] – trzy nowe testy;
  • w [3-7] – akcja [init-session] z brakującym parametrem [type];
  • w pliku [8-11] – odpowiedź serwera jSON;

Image

Powyżej:

  • w [1-7] – akcja [init-session] z nieprawidłowym parametrem [type];
  • w [8-11] – odpowiedź serwera jSON;

Image

Powyżej:

  • w [1-8], akcja [init-session] o typie jSON;
  • w [9-12] – odpowiedź serwera jSON;

23.11.3. Akcja [authentifier-utilisateur]

Akcja [authentifier-utilisateur] jest wykonywana przez następujący kontroler [AuthentifierUtilisateurController]:


<?php

namespace Application;

// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

class AuthentifierUtilisateurController implements InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // powinno zawierać POST oraz jeden parametr 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]";
      // wynik jest przekazywany do głównego kontrolera
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // pobieramy parametry z POST
    $erreurs = [];
    // użytkownik
    $état = 210;
    if (!$request->request->has("user")) {
      $état += 2;
      $erreurs[] = "paramètre [user] manquant";
    } else {
      $user = $request->request->get("user");
    }
    // hasło
    if (!$request->request->has("password")) {
      $état += 4;
      $erreurs[] = "paramètre [password] manquant";
    } else {
      $password = trim($request->request->get("password"));
    }
    // błąd?
    if ($erreurs) {
      // przekazujemy wynik do kontrolera głównego
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
    }
    // weryfikacja danych uwierzytelniających użytkownika
    // czy użytkownik istnieje?
    $users = $config["users"];
    $i = 0;
    $trouvé = FALSE;
    while (!$trouvé && $i < count($users)) {
      $trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
      $i++;
    }
    // znaleziono?
    if (!$trouvé) {
      // komunikat o błędzie
      $message = "Echec de l'authentification [$user, $password]";
      $état = 221;
      // przekazujemy wynik do głównego kontrolera
      return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
    } else {
      // odnotowujemy w sesji, że użytkownik został uwierzytelniony
      $session->set("user", TRUE);
      // komunikat o pomyślnym zakończeniu
      $message = "Authentification réussie [$user, $password]";
      $état = 200;
      // zwracamy wynik do głównego kontrolera
      return [Response::HTTP_OK, $état, ["réponse" => $message], []];
    }
  }

}

Uwagi

  • oczekuje się na żądanie [POST main.php?action=authentifier-utilisateur] z dwoma parametrami przesłanymi metodą POST [user, password];
  • wiersze 24–25: sprawdzamy, czy otrzymaliśmy żądanie POST z jednym parametrem w URL;
  • wiersze 26–31: w przypadku wystąpienia błędu odnotowuje się go i zwraca wynik [$statusCode, $état, $content, $headers] do głównego kontrolera;
  • wiersze 36–39: sprawdzamy obecność parametru [user] w przesłanych wartościach. Jeśli go nie ma, odnotowujemy błąd;
  • wiersze 43–45: sprawdza się obecność parametru [password] w przesłanych wartościach. Jeśli go nie ma, odnotowuje się błąd;
  • wiersze 50–53: jeśli brakuje którejkolwiek z zapisanych wartości, do głównego kontrolera zwracany jest wynik [$statusCode, $état, $content, $headers];
  • wiersze 56–62: sprawdzane jest, czy pobrana para [$user,$password] znajduje się w tablicy [$config[‘users’]] pliku konfiguracyjnego;
  • wiersze 64–69: jeśli tak nie jest, odnotowuje się błąd. Kod statusu HTTP jest zmieniany na [Response::HTTP_UNAUTHORIZED], a wynik [$statusCode, $état, $content, $headers] jest przekazywany do kontrolera głównego;
  • wiersz 72: uwierzytelnienie zakończyło się powodzeniem. Odnotowuje się to w sesji, umieszczając w niej klucz [user]. To właśnie obecność tego klucza wskazuje na pomyślne uwierzytelnienie;
  • wiersze 73–77: zwracamy wynik [$statusCode, $état, $content, $headers] potwierdzający powodzenie do kontrolera głównego;

23.11.4. Testy [Postman]

Przeprowadzamy testy [Postman] kontrolera [AuthentifierUtilisateurController] w trybie jSON;

Image

Powyżej:

  • w trybie [1-6], akcja [authentifier-utilisateur] z GET [2], podczas gdy wymagany jest POST;
  • w [7-10] odpowiedź serwera jSON;

Zastąpmy GET na POST [2] bez umieszczania parametrów w treści odpowiedzi [7]:

Image

Powyżej:

  • w [1-7], POST bez parametrów wysłanych w [7];
  • w [8-11] – odpowiedź jSON z serwera;

Dodajmy teraz parametr [password] do treści (body) żądania [4]:

Image

Powyżej:

  • w [1-6], żądanie POST [2] z parametrem [password] wysłanym w treści (body) [4-6]. Wysyłane parametry należy dodać do treści (body) żądania [4]. Istnieje kilka sposobów wysyłania wartości do serwera. Wybieramy metodę [x-www-form-urlencoded] [5];
  • w [8-10] otrzymujemy odpowiedź jSON z serwera;

Teraz zdefiniujmy parametr [user] bez parametru [password]:

Image

Powyżej:

  • w [1-7], zapytanie POST bez parametru [password] [4-7];
  • w [8-11] – odpowiedź serwera jSON;

Teraz zdefiniujmy dwa parametry wysyłane w żądaniu [user, password], ale z wartościami powodującymi niepowodzenie uwierzytelnienia:

Image

Powyżej:

  • w [1-9] – żądanie POST z nieprawidłowymi parametrami wysłanymi metodą POST [user, password];
  • w [10-13] – odpowiedź serwera jSON. Należy zwrócić uwagę na kod statusu [401 Unauthorized] [10] w odpowiedzi;

Teraz żądanie POST z prawidłowymi identyfikatorami:

Image

Powyżej:

  • w [1-9], żądanie POST [2] z prawidłowymi identyfikatorami [6-9];
  • w [10-13] – odpowiedź serwera jSON. Należy zwrócić uwagę na kod statusu HTTP, [200 OK] oraz [10];

23.11.5. Akcja [calculer-impot]

Akcja [calculer-impot] jest przetwarzana przez następujący moduł sterujący [CalculerImpotController]:


<?php

namespace Application;

// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias warstwy [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class CalculerImpotController implements InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // musi zawierać jeden parametr GET oraz trzy parametry POST
    $method = strtolower($request->getMethod());
    $erreur = $method !== "post" || $request->query->count() != 1;
    if ($erreur) {
      // zauważamy błąd
      $message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
      $état = 301;
      // wynik jest zwracany do głównego kontrolera
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // odbieramy parametry z POST
    $erreurs = [];
    $état = 310;
    // stan cywilny
    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é]";
      }
    }
    // pobierana jest liczba dzieci
    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]";
      }
    }
    // pobieranie rocznego wynagrodzenia
    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]";
      }
    }
    // błąd?
    if ($erreurs) {
      // zwrot wyniku do głównego kontrolera
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
    }

    // mamy wszystko, co potrzebne do pracy
    // Redis
    \Predis\Autoloader::register();
    try {
      // klient [predis]
      $redis = new \Predis\Client();
      // łączymy się z serwerem, aby sprawdzić, czy jest dostępny
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // coś poszło nie tak
      // zwracamy wynik z błędem do głównego kontrolera
      $état = 350;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // mamy prawidłowe parametry
    // tworzenie warstwy [dao]
    if (!$redis->get("taxAdminData")) {
      try {
        // pobieramy dane podatkowe z bazy
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // przechowywanie pobranych danych w Redis
        $redis->set("taxAdminData", $dao->getTaxAdminData());
      } catch (\RuntimeException $ex) {
        // wystąpił błąd
        // zwracanie wyniku z błędem do głównego kontrolera
        $état = 340;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // dane podatkowe są pobierane z pamięci o ograniczonym zasięgu [application]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
      // instancja warstwy [dao]
      $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
    }
    // utworzenie warstwy [métier]
    $métier = new ServerMetier($dao);

    // mamy wszystko, co potrzebne do pracy – obliczenie podatku
    $résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
    // dodajemy do sesji właśnie przeprowadzoną symulację
    $simulation = new Simulation();
    $résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
    $simulation->setFromArrayOfAttributes($résultat);
    // czy w sesji istnieje lista symulacji?
    if (!$session->has("simulations")) {
      $simulations = [];
    } else {
      $simulations = $session->get("simulations");
    }
    // dodanie symulacji do listy symulacji
    $simulations[] = $simulation;
    // przywracamy symulacje do sesji
    $session->set("simulations", $simulations);
    // zwracanie wyniku do głównego kontrolera
    $état = 300;
    return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
  }

}

Uwagi

  • Oczekiwane żądanie to [POST main.php?action=calculer-impot] z trzema parametrami przesłanymi w metodzie POST [marié, enfants, salaire]:
    • [marié] musi mieć swoją wartość w [oui, non];
    • [enfants, salaire] muszą być liczbami całkowitymi dodatnimi lub zerowymi;
  • wiersze 26–27: sprawdzamy, czy w URL rzeczywiście występuje POST z jednym parametrem;
  • wiersze 28–34: jeśli tak nie jest, do głównego kontrolera wysyłany jest komunikat o błędzie;
  • wiersz 36: komunikaty o błędach są gromadzone w tablicy [$erreurs];
  • wiersze 39–41: sprawdzamy obecność parametru [marié]. Jeśli go nie ma, odnotowujemy błąd;
  • wiersze 43–49: sprawdzamy, czy wartość parametru [marié] znajduje się w parametrze [oui, non]. Jeśli tak nie jest, odnotowujemy błąd;
  • wiersze 51–54: sprawdzana jest obecność parametru [enfants]. Jeśli go nie ma, odnotowuje się błąd;
  • wiersze 55–61: sprawdzane jest, czy wartość parametru [enfants] jest liczbą dodatnią lub zerem. Jeśli tak nie jest, odnotowuje się błąd;
  • wiersze 63–66: sprawdzana jest obecność parametru [salaire]. Jeśli go nie ma, odnotowuje się błąd;
  • wiersze 67–72: sprawdzane jest, czy wartość parametru [salaire] jest liczbą dodatnią lub zerem. Jeśli tak nie jest, odnotowuje się błąd;
  • wiersze 75–78: jeśli tablica [$erreurs] nie jest pusta, oznacza to, że wystąpiły błędy. Umieszcza się tabelę błędów w odpowiedzi i zwraca wynik do głównego kontrolera;
  • wiersz 80: mamy prawidłowe parametry. Można obliczyć podatek. W tym celu należy utworzyć warstwy [dao] i [métier], które potrafią wykonać to obliczenie;
  • wiersze 82–94: tworzymy klienta [Redis];
  • wiersze 88–94: jeśli nie udało się połączyć z serwerem [Redis], wysyłamy kod [500 Internal Server Error] do klienta;
  • wiersz 98: sprawdzamy, czy serwer [Redis] posiada klucz [taxAdminData]. Klucz ten reprezentuje dane administracji podatkowej. Jeśli klucza nie ma, wówczas dane podatkowe należy pobrać z bazy danych;
  • wiersz 101: tworzenie warstwy [dao], gdy dane podatkowe muszą zostać pobrane z bazy danych. Klasa [ServerDaoWithRedis] została opisana w akapicie dotyczącym powiązania;
  • wiersz 103: dane pobrane z bazy są zapisywane w pamięci [Redis] wraz z kluczem [taxAdminData];
  • wiersze 104–110: jeśli wyszukiwanie w bazie danych zakończyło się niepowodzeniem, odnotowuje się błąd zwrócony przez warstwę [dao] i uwzględnia się go w wyniku zwracanym do głównego kontrolera;
  • wiersz 109: komunikat o błędzie zwrócony przez warstwę [PDO] jest zakodowany w [iso-8859-1]. Należy go zakodować w [utf-8];
  • wiersze 111–117: jeśli klucz [taxAdminData] istnieje w pamięci [Redis], wówczas dane podatkowe są przekazywane bezpośrednio do konstruktora warstwy [dao];
  • wiersz 119: tworzona jest warstwa [métier]. Klasa [ServerMetier] została opisana w akapicie dotyczącym powiązania;
  • wiersze 124–126: po obliczeniu kwoty podatku tworzony jest obiekt [Simulation]. Klasa [Simulation] zawiera dane symulacji i została opisana w akapicie „link”;
  • wiersze 128–132: właśnie utworzona symulacja musi zostać dodana do listy już obliczonych symulacji. Lista ta znajduje się w sesji, chyba że nie przeprowadzono jeszcze żadnej symulacji;
  • wiersze 133–136: symulacja zostaje dodana do listy symulacji, a lista ta jest ponownie umieszczana w sesji;
  • wiersze 137–139: wynik jest przekazywany do kontrolera głównego;

23.11.6. Testy [Postman]

Przeprowadzamy testy [Postman] kontrolera [CalculerImpotController] w trybie jSON;

Image

Powyżej:

  • w trybie [1-7] wysyłamy żądanie [GET] zamiast [POST];
  • w [8-11] otrzymujemy odpowiedź jSON z serwera;

Teraz użyjmy metody [POST], z parametrami wysłanymi w body lub bez nich, a także z nieprawidłowymi parametrami wysłanymi w body:

Image

Powyżej:

  • wysyłamy żądanie [POST] [2] z nieprawidłowymi parametrami przesłanymi metodą POST: [6-11] [marié, enfants, salaire]. Można pominąć jeden z tych parametrów, odznaczając odpowiednie pole wyboru w [16]. Umożliwi to przetestowanie różnych scenariuszy. Na powyższym zrzucie ekranu widoczne są wszystkie trzy parametry, a wszystkie są nieprawidłowe;
  • w [12-15] otrzymujemy odpowiedź serwera jSON;

Teraz odznaczmy dwa z trzech przesłanych parametrów:

Image

Powyżej,

  • w [5-8] wysłano tylko parametr [salaire], który ponadto jest nieprawidłowy;
  • w przypadku [9-11] otrzymujemy wynik jSON z serwera;

Teraz przeprowadźmy obliczenie podatku przy użyciu prawidłowych parametrów:

Image

Powyżej:

  • w [1118] – zapytanie z prawidłowymi parametrami [6-8];
  • w [12-14] – odpowiedź serwera jSON;

23.11.7. Akcja [lister-simulations]

Akcja [lister-simulations] jest przetwarzana przez następujący kontroler pomocniczy [ListerSimulationsController]:


<?php

namespace Application;

// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

class ListerSimulationsController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // musi być podany tylko jeden parametr 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";
      // zwracany jest wynik z błędem do głównego kontrolera
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // pobierana jest lista symulacji w sesji
    if (!$session->has("simulations")) {
      $simulations = [];
    } else {
      $simulations = $session->get("simulations");
    }
    // zwracamy wynik z powodzeniem do głównego kontrolera
    $état = 500;
    return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
  }

}

Uwagi

  • żądanie [GET main.php?action=lister-simulations];
  • wiersze 24–25: sprawdzane jest, czy mamy żądanie GET z jednym parametrem;
  • wiersze 26–31: jeśli tak nie jest, do głównego kontrolera zwracany jest wynik z błędem;
  • wiersze 33–37: pobierana jest lista symulacji z sesji, jeśli się tam znajduje (wiersz 36), w przeciwnym razie lista ta jest pusta (wiersz 34);
  • wiersze 39–40: zwracana jest lista symulacji do głównego kontrolera;

23.11.8. Testy [Postman]

Utworzymy dwa testy: jeden z błędem i jeden zakończony sukcesem.

Image

Powyżej:

  • w [1-8] wysyłamy zapytanie [GET] z dodatkowym parametrem [param1] w URL [3, 7-8];
  • w [9-12], odpowiedź serwera jSON;

Teraz wyślijmy prawidłowe zapytanie:

Image

Powyżej:

  • [1-5] – prawidłowe zapytanie;

Wynik zapytania jest następujący:

Image

  • w [3-6] – odpowiedź serwera jSON. Przed tym testem kilkakrotnie przeprowadzono testy [Postman] i [calculer-impot-300] w celu stworzenia symulacji w sesji internetowej serwera;

23.11.9. Akcja [supprimer-simulation]

Akcja [supprimer-simulation] jest przetwarzana przez następujący kontroler pomocniczy [SupprimerSessionController]:


<?php

namespace Application;

// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

class SupprimerSimulationController {

  /// $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // muszą występować dwa parametry 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]";
    }
    // parametr [numéro] musi istnieć
    if (!$erreur) {
      $état += 4;
      $erreur = !$request->query->has("numéro");
      if ($erreur) {
        $message = "paramètre [numéro] manquant";
      }
    }
    // parametr [numéro] musi być prawidłowy
    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";
      }
    }
    // parametr [numéro] musi mieścić się w przedziale [0,n-1]
    // jeśli n jest liczbą symulacji
    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";
      }
    }
    // błąd?
    if ($erreur) {
      // wynik jest przekazywany do głównego kontrolera
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // usuwamy symulację $numéro
    unset($simulations[$numéro]);
    $simulations = array_values($simulations);
    // przywracamy symulacje do sesji
    $session->set("simulations", $simulations);
    // zwracamy listę symulacji do klienta
    $état = 600;
    return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
  }

}

Uwagi

  • żądanie [GET main.php?action=supprimer-simulation&numéro=x];
  • wiersze 24–30: sprawdzane jest, czy mamy żądanie GET z dwoma parametrami;
  • wiersze 32–38: sprawdzamy, czy parametr [numéro] występuje wśród parametrów URL;
  • wiersze 40–47: sprawdzamy, czy wartość parametru [numéro] jest poprawna pod względem składniowym;
  • wiersze 50–61: sprawdzamy, czy symulacja nr [numéro] rzeczywiście istnieje. Istnieją dwa przypadki błędu:
    • nie można znaleźć listy symulacji w sesji (wiersz 52);
    • numer [numéro] symulacji, która ma zostać usunięta, nie występuje na liście symulacji;
  • wiersze 63–66: w przypadku błędu do kontrolera głównego zwracany jest wynik z błędem;
  • wiersz 68: symulacja o numerze [numéro] zostaje usunięta;
  • wiersz 69: operacja [unset] nie zmienia indeksów [0, n-1] na liście. Aby je zaktualizować, pobierane są wartości z tablicy [$simulations] w celu usunięcia brakującej symulacji;
  • wiersz 71: umieszczamy nową tabelę symulacji w sesji;
  • wiersze 73–74: przekazujemy głównemu kontrolerowi nową listę symulacji;

23.11.10. Testy [Postman]

Przeprowadzimy testy błędów i powodzenia:

Image

Powyżej:

  • w [1-6] – zapytanie GET bez parametru [numéro];
  • w [7-10] – odpowiedź serwera jSON;

Teraz żądanie z numerem niepoprawnym pod względem składniowym:

Image

Powyżej:

  • w [1-5], zapytanie GET z nieprawidłowym parametrem [numéro] [3, 5];
  • w [6-9] – odpowiedź serwera jSON;

Teraz żądanie z nieistniejącym numerem symulacji:

Image

Powyżej:

  • w [1-5], zapytanie z numerem symulacji równym 100, który nie istnieje na liście symulacji;
  • w [6-9] – odpowiedź serwera jSON;

Teraz usuniemy symulację nr 0 z listy, czyli pierwszą symulację. Najpierw ponownie wywołajmy tę listę za pomocą zapytania [lister-simulations-500]:

Image

  • W pliku [1] znajdują się obecnie 2 symulacje;

Usuwamy pierwszą symulację (numer 0):

Image

Powyżej:

  • w pliku [1-5] usuwamy symulację nr 0 [5];
  • w pliku [6-9] pojawia się odpowiedź serwera jSON. Widać, że symulacja nr 0 została usunięta;

Powtórzmy tę operację:

Image

Powyżej:

  • w [1] – w sesji internetowej serwera nie ma już żadnych symulacji;

23.11.11. Akcja [fin-session]

Akcja [fin-session] jest przetwarzana przez następujący kontroler pomocniczy [FinSessionController]:


<?php

namespace Application;

// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

class FinSessionController implements InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]

  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // musi być tylko jeden parametr GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    // błąd?
    if ($erreur) {
      $état = 401;
      // wynik w kontrolerze głównym
      $message = "GET requis avec le seul paramètre [action] dans l'URL";
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // zapisujemy typ sesji
    $type = $session->get("type");
    // unieważniamy bieżącą sesję
    $session->invalidate();
    // przywracamy typ w nowej sesji
    $session->set("type", $type);
    // wysyłanie odpowiedzi
    $état = 400;
    // wynik przekazywany do głównego kontrolera
    $content = ["réponse" => "session supprimée"];
    return [Response::HTTP_OK, $état, $content, []];
  }

}

Uwagi

  • żądanie [GET main.php?action=fin-session];
  • wiersze 25–33: sprawdzane jest, czy akcja to GET z jedynym parametrem [fin-action];
  • wiersz 38: unieważnia się bieżącą sesję. Powoduje to usunięcie danych zapisanych w tej sesji i rozpoczęcie nowej sesji;
  • wiersz 36: przed zakończeniem sesji zapisuje się jej typ [json, xml, html];
  • wiersz 40: typ poprzedniej sesji jest przywracany w nowej sesji. Ostatecznie rozpoczynamy nową sesję o unikalnym kluczu [type];
  • wiersze 44–45: wynik jest przekazywany do głównego kontrolera;

23.11.12. Testy [Postman]

Przeprowadzimy test błędu oraz test pomyślnego wykonania:

Image

Powyżej:

  • w [1-5] żądamy zakończenia sesji [5], podając POST [2] zamiast oczekiwanego GET;
  • w [6-9] – odpowiedź serwera jSON;

A teraz przykład pomyślnego działania. Przyjrzyjmy się najpierw plikowi cookie sesji wymienionemu między klientem [Postman] a serwerem podczas ostatniego przeprowadzonego testu:

Image

Powyżej:

  • w [3] – plik cookie sesji wysłany przez klienta [Postman] do serwera;

Przyjrzyjmy się teraz nagłówkom HTTP wysłanym przez serwer w odpowiedzi:

Image

Powyżej:

  • w przypadku pliku [3-4] plik cookie sesji nie znajduje się w odpowiedzi serwera. Jest to normalne. Serwer wysyła go tylko raz: na początku nowej sesji internetowej;

Teraz wykonajmy prawidłową akcję [fin-session]:

Image

Powyżej:

  • w [1-3] – prawidłowa akcja [fin-session];
  • w [4-7] – odpowiedź serwera jSON;

Przyjrzyjmy się nagłówkom HTTP przesłanym w odpowiedzi serwera:

Image

  • w [3] serwer wysyła nagłówek [Set-Cookie], wskazując tym samym, że rozpoczyna się nowa sesja internetowa;

23.12. Rodzaje odpowiedzi serwera

23.12.1. Wprowadzenie

Wróćmy do ogólnej architektury aplikacji:

Image

Przedstawimy możliwe typy odpowiedzi [3a]. Znajdują się one w folderze [Responses] projektu:

Image

Klasę [JsonResponse] przedstawiliśmy już w akapicie dotyczącym linków. Implementuje ona interfejs [InterfaceResponse] i rozszerza klasę [ParentResponse]. Tak samo jest w przypadku dwóch pozostałych klas: [XmlResponse] i [HtmlResponse].

Przypomnijmy definicję interfejsu [InterfaceResponse]:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

interface InterfaceResponse {

  // Żądanie $request: żądanie w trakcie przetwarzania
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł logujący do zapisywania logów
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}
  • wiersze 19–27: interfejs [InterfaceResponse] posiada jedną metodę [send] służącą do wysyłania odpowiedzi do klienta;
  • wiersze 11–17: znaczenie poszczególnych parametrów metody [send];
  • wiersze 23–25: parametry [$statusCode, $content, $headers] stanowią standardową odpowiedź kontrolerów pomocniczych aplikacji. Jednak odpowiedź może wymagać dodatkowych informacji. Dlatego przekazujemy jej trzy pierwsze parametry (wiersze 20–22), które zapewniają dostęp do wszystkich informacji dotyczących żądania, sesji i konfiguracji;
  • wiersz 26: odpowiedź wymaga parametru [Logger], ponieważ ma ona zarejestrować odpowiedź wysłaną do klienta;

Przypomnijmy teraz kod klasy [ParentResponse], klasy nadrzędnej dla trzech typów odpowiedzi, która wyodrębnia to, co je łączy: faktyczne wysłanie odpowiedzi tekstowej do klienta:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Response;

class ParentResponse {

  // int $statusCode: kod statusu odpowiedzi HTTP
  // ciąg znaków $content: treść odpowiedzi do wysłania
  // w zależności od przypadku jest to ciąg znaków jSON, XML, HTML
  // tablica $headers: nagłówki HTTP, które należy dodać do odpowiedzi

  public function sendResponse(
    int $statusCode,
    string $content,
    array $headers): void {

    // przygotowanie tekstowej odpowiedzi serwera
    $response = new Response();
    $response->setCharset("utf-8");
    // kod statusu
    $response->setStatusCode($statusCode);
    // nagłówki
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // wysyłamy odpowiedź
    $response->setContent($content);
    $response->send();
  }
}

Komentarze

  • wiersze 10–13: znaczenie trzech parametrów metody [send];
  • wiersz 17: należy zauważyć, że treść odpowiedzi ma typ [string] i tym samym jest gotowa do wysłania (wiersz 30);
  • wiersz 22: odpowiedź będzie zawierała znaki UTF-8;
  • wiersz 24: kod statusu odpowiedzi HTTP;
  • wiersze 26–28: dodanie nagłówków o formacie HTTP podanych przez kod wywołującego;
  • wiersze 30–31: wysłanie odpowiedzi do klienta;

Na koniec przypomnijmy kod głównego kontrolera, który żąda wysłania odpowiedzi do klienta:


// dodajemy klucze [action, état] do odpowiedzi kontrolera
$content = ["action" => $action, "état" => $état] + $content;
// tworzy się instancję obiektu [Response] odpowiedzialnego za wysłanie odpowiedzi do klienta
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);

// odpowiedź została wysłana – zwalnia się zasoby
$logger->close();
exit;
  • wiersz 4: ustalamy nazwę klasy [Response], która ma zostać zainicjowana;
  • wiersz 5: instancjonujemy ją i wysyłamy odpowiedź do klienta za pomocą metody [send($request, $session, $config, $statusCode, $content, $headers, $logger)]. Ponieważ implementują one ten sam interfejs [InterfaceResponse], metody [send] dla różnych typów odpowiedzi mają wszystkie tę samą sygnaturę;

23.12.2. Klasa [JsonResponse]

Została już przedstawiona w akapicie „link”. Przytaczamy jednak ponownie jej kod, aby lepiej podkreślić jednolitość trzech klas odpowiedzi:

Klasa [JsonResponse] implementuje interfejs [InterfaceResponse] w następujący sposób:


<?php

namespace Application;

// zależności 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 {

  // Żądanie $request: żądanie w trakcie przetwarzania
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP, które należy dodać do odpowiedzi
  // Logger $logger: moduł logujący służący do zapisywania logów

  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {

    // przygotowanie serializatora symfony
    $serializer = new Serializer(
      [
      // niezbędny do serializacji obiektów
      new ObjectNormalizer()],
      // koder jSON
      // w przypadku opcji należy umieścić OU pomiędzy poszczególnymi opcjami
      [new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
    );
    // serializacja jSON
    $json = $serializer->serialize($content, 'json');
    // nagłówki
    $headers = array_merge($headers, ["content-type" => "application/json"]);
    // wysyłanie odpowiedzi
    parent::sendResponse($statusCode, $json, $headers);
    // log
    if ($logger !== NULL) {
      $logger->write("réponse=$json\n");
    }
  }

}

Komentarze

  • wiersz 13: klasa implementuje interfejs [InterfaceResponse];
  • wiersz 13: klasa ta dziedziczy po klasie [ParentResponse]. Wszystkie typy klasy [Response] dziedziczą po tej klasie. To właśnie ta klasa nadrzędna wysyła odpowiedź do klienta (wiersz 46). Ponieważ kod ten był wspólny dla wszystkich typów klasy [Response], został on wyodrębniony do klasy nadrzędnej;
  • wiersze 33–40: instancjonowanie serializatora [Symfony], który przekształci odpowiedź serwera [$content] w ciąg znaków jSON (wiersz 42);
  • wiersze 34–36: pierwszym parametrem konstruktora klasy [Serializer] jest tablica. W tablicy tej umieszcza się instancję klasy [ObjectNormalizer] niezbędną do serializacji obiektów. W tej aplikacji sytuacja ta występuje w przypadku listy symulacji, gdzie każda symulacja jest instancją klasy [Simulation];
  • wiersz 39: drugi parametr konstruktora klasy [Serializer] jest również tablicą: umieszcza się w nim wszystkie kodery używane podczas serializacji (XML, jSON, CSV…);
  • wiersz 39: w tym miejscu będzie tylko jeden enkoder typu [JsonEncoder]. Wystarczyłby konstruktor bez parametrów. W tym przypadku przekazaliśmy do konstruktora parametr [JsonEncode] wyłącznie w celu przekazania opcji kodowania jSON;
  • wiersz 39: parametr konstruktora [JsonEncode] jest tablicą opcji. W tym przypadku używamy opcji [JSON_UNESCAPED_UNICODE], aby zastrzec, że znaki UTF-8 w ciągu jSON mają być renderowane natywnie, a nie „eskapowane”;
  • wiersz 42: treść odpowiedzi HHTP jest serializowana do postaci jSON za pomocą poprzedniego serializatora;
  • wiersz 44: dodaje się nagłówek HTTP, który informuje klienta, że zostanie mu wysłany plik jSON;
  • wiersz 46: zwracamy się do klasy nadrzędnej o wysłanie odpowiedzi do klienta;
  • wiersze 48–50: rejestrujemy odpowiedź jSON;

23.12.3. Klasa [XmlResponse]

Klasa [XmlResponse] implementuje interfejs [InterfaceResponse] w następujący sposób:


<?php

namespace Application;

// zależności 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 {

  // Żądanie $request: żądanie w trakcie przetwarzania
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł logujący do zapisywania logów

  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {

    // przygotowanie serializatora symfony
    $serializer = new Serializer(
      // niezbędny do serializacji obiektów
      [new ObjectNormalizer()],
      [
      // serializacja XML
      new XmlEncoder(
        [
        XmlEncoder::ROOT_NODE_NAME => 'root',
        XmlEncoder::ENCODING => 'utf-8'
        ]
      ),
      // serializacja jSON
      new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
      ]
    );
    // serializacja XML
    $xml = $serializer->serialize($content, 'xml');
    // nagłówki
    $headers = array_merge($headers, ["content-type" => "application/xml"]);
    // wysłanie odpowiedzi
    parent::sendResponse($statusCode, $xml, $headers);
    // log
    if ($logger !== NULL) {
      // log w formacie jSON
      $log = $serializer->serialize($content, 'json');
      $logger->write("réponse=$log\n");
    }
  }

}

Komentarze

  • wiersze 34–48: utworzenie instancji serializatora Symfony. Konstruktor przyjmuje dwa parametry typu tablica;
  • wiersz 36: pierwsza tablica zawiera instancję typu [ObjectNormalizer], która bierze udział w serializacji obiektów;
  • wiersze 37–47: druga tablica zawiera kodery używane do serializacji. Za pomocą tego samego serializatora można przewidzieć różne rodzaje serializacji;
  • wiersze 38–44: koder XML;
  • wiersz 41: ustala się korzeń wygenerowanego kodu XML. Będzie on miał postać <root>[autres balises XML]</root>;
  • wiersz 42: kodowanie będzie wykorzystywać znaki UTF-8;
  • wiersz 46: koder jSON. Zostanie on użyty do zapisania odpowiedzi w pliku [logs.txt], który jest utworzony w formacie jSON;
  • wiersz 50: treść odpowiedzi wysyłanej do klienta jest serializowana w formacie XML;
  • wiersz 52: do nagłówków otrzymanych jako parametr (wiersz 30) dodawany jest nagłówek HTTP, który informuje klienta, że wysyłany jest do niego dokument XML;
  • wiersz 54: faktyczne wysłanie odpowiedzi do klienta przez klasę nadrzędną;
  • wiersze 56–60: zapis odpowiedzi w pliku jSON;

23.12.4. Testy [Postman]

Przeprowadziliśmy już wszystkie możliwe testy błędów w pliku jSON. W pliku XML nie ma już nic do zrobienia. Przedstawiamy dwa przykłady odpowiedzi w pliku XML:

Image

Powyżej:

  • w [1-3] – żądanie rozpoczęcia sesji XML;
  • w [4-7] – odpowiedź serwera XML;

Od tej chwili wszystkie odpowiedzi serwera będą miały postać XML. Można wykorzystać wszystkie żądania użyte wcześniej w [Postman] bez wprowadzania zmian, a dla każdego z nich otrzymamy odpowiedź XML. Przyjrzyjmy się na przykład udanej autoryzacji:

Image

Powyżej:

  • w [1-3] – prawidłowe żądanie uwierzytelnienia;
  • w [4-7], odpowiedź serwera XML;

23.12.5. Odpowiedź [HtmlResponse]

Gdy typ sesji to [html], tworzona jest instancja obiektu typu [HtmlResponse] w celu wysłania odpowiedzi do klienta. Obiekt ten wyśle do klienta strumień HTML, który zależy od kodu stanu zwróconego przez kontroler pomocniczy, który przetworzył akcję. Ta zależność [état=>vue] jest zapisana w pliku konfiguracyjnym [config.json] w następujący sposób:


"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"

Konfigurację tę należy odczytać w następujący sposób: [‘nom de la vue’ => ‘états associés à cette vue’]

  • wiersz 2: jeśli kontroler pomocniczy zwrócił stan tablicy [700, 221, 400], należy wyświetlić widok [vue-authentification.php];
  • wiersz 3: jeśli kontroler pomocniczy zwrócił stan tabeli [200, 300, 341, 350, 800], należy wyświetlić widok [vue-calcul-impot.php];
  • wiersz 4: jeśli kontroler pomocniczy zwrócił stan z tabeli [500, 600], należy wyświetlić widok [vue-liste-simulations.php];
  • wiersz 6: jeśli kontroler pomocniczy zwrócił stan, który nie występuje w żadnej z powyższych tabel, należy wyświetlić widok [vue-erreurs.php];

Widoki są zgromadzone w folderze [Views] projektu:

Image

Kod klasy [HtmlResponse] jest następujący:


<?php

namespace Application;

// zależności 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 {

  // Żądanie $request: żądanie w trakcie przetwarzania
  // Sesja $session: sesja aplikacji internetowej
  // tablica $config: konfiguracja aplikacji
  // int statusCode: kod statusu odpowiedzi HTTP
  // tablica $content: odpowiedź serwera
  // tablica $headers: nagłówki HTTP do dodania do odpowiedzi
  // Logger $logger: moduł logujący do zapisywania logów

  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {

    // przygotowanie serializatora symfony
    $serializer = new Serializer(
      [
      // do serializacji obiektów
      new ObjectNormalizer()],
      [
      // do serializacji jSON dziennika odpowiedzi
      new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
      ]
    );
    // odpowiedź HTML zależy od kodu stanu zwracanego przez kontroler
    $état = $content["état"];
    // każdemu stanowi odpowiada widok – szuka się go w konfiguracji aplikacji
    // lista widoków
    $vues = array_keys($config["vues"]);
    $trouvé = false;
    $i = 0;
    // przeglądamy listę widoków
    while (!$trouvé && $i < count($vues)) {
      // stany powiązane z widokiem nr i
      $états = $config["vues"][$vues[$i]];
      // czy poszukiwany raport znajduje się wśród raportów powiązanych z widokiem nr I?
      if (in_array($état, $états)) {
        // wyświetlany widok będzie widokiem nr i
        $vueRéponse = $vues[$i];
        $trouvé = true;
      }
      // następny widok
      $i++;
    }
    // znaleziono?
    if (!$trouvé) {
      // jeśli nie istnieje żaden widok dla bieżącego stanu aplikacji
      // wyświetlany jest widok błędów
      $vueRéponse = $config["vue-erreurs"];
    }
    // pobieramy widok HTML do wyświetlenia w postaci ciągu znaków
    ob_start();
    require __DIR__ . "/../Views/$vueRéponse";
    $html = ob_get_clean();
    // w nagłówkach wskazuje się, że zostanie wysłany HTML
    $headers = array_merge($headers, ["content-type" => "text/html"]);
    // klasa nadrzędna zajmuje się faktycznym wysłaniem odpowiedzi
    parent::sendResponse($statusCode, $html, $headers);
    // log w formacie jSON zawierający odpowiedź bez HTML
    if ($logger !== NULL) {
      // log w formacie jSON odpowiedzi kontrolera pomocniczego, który przetworzył akcję
      $log = $serializer->serialize($content, 'json');
      $logger->write("réponse=$log\n");
    }
  }

}

Komentarze

  • wiersze 32–41: tworzona jest instancja serializatora Symfony. Jest on niezbędny do logowania jSON odpowiedzi kontrolera, który przetworzył akcję (wiersze 72–82);
  • wiersze 42–57: w konfiguracji aplikacji wyszukiwany jest widok, który ma zostać wyświetlony. Zależy on od kodu stanu zwróconego przez kontroler, który przetworzył akcję. Kod ten znajduje się w [$content[‘état’]] (wiersz 43);
  • wiersze 42–61: szuka się widoku odpowiadającego temu stanowi;
  • wiersze 62–67: jeśli nie znaleziono żadnego widoku, oznacza to, że mamy do czynienia z nieprawidłowym kodem stanu dla aplikacji HTML. Pojęcie stanów nieprawidłowych zostanie wyjaśnione w dalszej części. W takim przypadku wyświetlany jest widok błędu;
  • wiersze 68–70: interpretuje się kod PHP wybranego widoku i zapisuje wynik w zmiennej [$html] (wiersz 71);
  • kod ten wymaga kilku wyjaśnień. Wyobraźmy sobie, że wybranym widokiem jest [vue-authentification.php], który przedstawia internetowy formularz uwierzytelniający:
    • wiersz 69: funkcja [ob_start] uruchamia to, co w dokumentacji nazywa się opóźnieniem wyjściowym. Wszystko, co jest zapisywane przez operacje typu print, require… i co normalnie jest natychmiast wysyłane do klienta, trafia do bufora wyjściowego (ob = output buffer) bez wysyłania do klienta;
    • wiersz 70: ładowany jest widok [vue-authentification.php], który jest dynamicznym widokiem HTML zawierającym kod PHP. Następnie mają miejsce dwie rzeczy:
      • kod PHP z widoku [vue-authentification.php] jest ładowany i interpretowany. W rezultacie powstaje widok, który nazwiemy [vue-authentification.html], zawierający wyłącznie kod HTML, a nawet CSS oraz JavaScript, ale już bez kodu PHP;
      • ten kod HTML jest zazwyczaj wysyłany do klienta. Tak dzieje się w rzeczywistości w przypadku każdego tekstu napotkanego przez interpreter PHP, który nie jest kodem PHP. Z powodu opóźnienia wyjściowego kod HTML jest umieszczany w buforze wyjściowym bez wysyłania go do klienta;
    • wiersz 71: funkcja [ob_get_clean] wykonuje dwie czynności:
      • umieszcza w zmiennej [$html] zawartość bufora wyjściowego, a więc stronę [vue-authentification.html], którą tam umieściliśmy;
      • opróżnia bufor wyjściowy. Z punktu widzenia bufora wygląda to tak, jakby nic się nie wydarzyło. Ponadto klient nadal nie otrzymał niczego;
  • wiersz 70: obecnie trwa wykonywanie klasy [HtmlResponse], która znajduje się w folderze [Responses]. Aby znaleźć widok, należy zatem przejść o jeden poziom wyżej do [..], a następnie przejść do folderu [Views]. [__DIR__] to pełna nazwa folderu, w którym znajduje się aktualnie uruchomiony skrypt; w naszym przykładzie jest to folder [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses];
  • wiersz 73: do nagłówków HTTP otrzymanych jako parametr (wiersz 29) dodajemy nagłówek informujący klienta, że wyślemy mu HTML;
  • wiersz 75: klasa nadrzędna otrzymuje polecenie faktycznego wysłania odpowiedzi do klienta;
  • wiersze 77–81: rejestruje się w jSON odpowiedź [$content] dostarczoną przez kontroler pomocniczy, który przetworzył bieżącą akcję;

23.12.6. Testy [Postman]

Aby naprawdę przetestować tryb HTML sesji, musielibyśmy przejrzeć wszystkie widoki. Zrobimy to później. Przeprowadzimy następujący test:

Przyjrzyjmy się liście widoków w pliku konfiguracyjnym:


"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"

Kontekst powodujący wystąpienie niektórych z powyższych kodów stanu można znaleźć, przeglądając przeprowadzone testy [Postman]:

Image

Widać, że kod stanu [700] odpowiada pomyślnie zakończonej akcji [init-session] [2]. Powyżej mamy odpowiedź jSON, ale może ona mieć również postać XML lub HTML. To właśnie ten ostatni przypadek zostanie przetestowany. Zgodnie z plikiem konfiguracyjnym to widok [vue-authentification.php] stanowi odpowiedź HTML. Sprawdźmy to.

Image

Powyżej:

  • w [1-3] inicjujemy sesję HTML. Oczekujemy zatem odpowiedzi HTML;
  • w [4-8] znajduje się odpowiedź serwera HTML;
  • zakładka [8] umożliwia podgląd otrzymanego kodu HTML;

Image

  • w [8-9] – podgląd widoku HTML;

23.13. Aplikacja internetowa HTML

23.13.1. Przedstawienie widoków

Aplikacja internetowa HTML będzie korzystać z czterech widoków:

Widok uwierzytelniania:

Image

Widok obliczania podatku:

Image

Widok listy symulacji:

Image

Widok nieoczekiwanych błędów:

Image

Opiszemy te widoki po kolei.

23.13.2. Widok uwierzytelniania

23.13.2.1. Opis widoku

Widok uwierzytelniania wygląda następująco:

Image

Widok składa się z dwóch elementów, które nazwiemy fragmentami:

  • fragment [1] jest generowany przez skrypt [v-bandeau.php];
  • fragment [2] jest generowany przez skrypt [v-authentification.php];

Widok uwierzytelniania jest generowany przez następującą stronę [vue-authentification.php]:


<?php
// dane testowe strony
// dane strony są enkapsulowane w $page

?>

<!doctype html>
<html lang="fr">
    <head>
        <!-- Wymagane metatagi -->
        <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">
            <!-- pasek nagłówkowy o szerokości 1 wiersza i 12 kolumn -->
            <?php require "v-bandeau.php"; ?>
            <!-- formularz uwierzytelniający w 9 kolumnach -->
            <div class="row">
                <div class="col-md-9">
                    <?php require "v-authentification.php" ?>
                </div>
            </div>  
            <?php
            // w przypadku błędu – wyświetla się komunikat o błędzie
            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>

Uwagi

  • wiersz 7: dokument HTML rozpoczyna się od tego wiersza;
  • wiersze 8–44: strona HTML jest zawarta w tagach <html> </html>;
  • wiersze 9–16: nagłówek (head) dokumentu HTML;
  • wiersz 11: tag <meta charset> wskazuje, że dokument jest zakodowany w UTF-8;
  • wiersz 12: tag <meta name=’viewport’> określa początkowy sposób wyświetlania widoku: na całej szerokości ekranu, na którym jest wyświetlany (width), w jego pierwotnej skali (initial-scale), bez skalowania w celu dopasowania do mniejszego ekranu (shrink-to-fit);
  • wiersz 14: tag <link rel=’stylesheet’> określa plik CSS, który reguluje wygląd widoku. Wykorzystujemy tutaj framework CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
  • wiersz 15: tag <title> określa tytuł strony:

Image

  • wiersze 17–43: treść strony internetowej jest zawarta w tagach <body></body>;
  • wiersze 18–42: tag <div> wyznacza sekcję wyświetlanej strony. Atrybuty [class] użyte w widoku odnoszą się do frameworka CSS Bootstrap. Tag <div class=’container’> wyznacza kontener Bootstrap;
  • wiersz 20: dołączamy skrypt [v-bandeau.php]. Skrypt ten generuje baner [1] strony. Opiszemy go wkrótce;
  • wiersze 22–26: tag <div class=’row’> wyznacza wiersz Bootstrap. Wiersze te składają się z 12 kolumn;
  • wiersz 23: tag <div class=’col-md-9’> wyznacza sekcję składającą się z 9 kolumn;
  • wiersz 24: dołączamy skrypt [v-authentification.php], który wyświetla formularz uwierzytelniający [2] na stronie. Opiszemy go wkrótce;
  • wiersz 27: tag <?php wprowadza kod PHP do wnętrza strony HTML. Kod ten jest wykonywany przed wyświetleniem strony HTML i może ją modyfikować;
  • wiersz 29: całość danych dynamicznych wyświetlanego widoku zostanie zamknięta w obiekcie [$modèle] typu [stdClass]. Jest to wybór arbitralny. Zamiast tego można było wybrać tablicę asocjacyjną, uzyskując ten sam rezultat;
  • wiersz 29: uwierzytelnianie kończy się niepowodzeniem, jeśli użytkownik wprowadzi nieprawidłowe dane logowania. W takim przypadku widok uwierzytelniania zostanie ponownie wyświetlony wraz z komunikatem o błędzie. Atrybut [$modèle→error] określa, czy należy wyświetlić ten komunikat o błędzie;
  • wiersze 30–39: ta składnia zapisuje cały tekst umieszczony pomiędzy symbolami PHP <<<EOT (wiersz 30 – w miejsce EOT=End Of Text można wpisać dowolny tekst) oraz symbolem EOT z wiersza 39 (musi być identyczny z symbolem używanym w wierszu 30). Symbol należy wpisać w pierwszej kolumnie wiersza 39. Zmienne PHP znajdujące się w tekście pomiędzy dwoma symbolami EOT są interpretowane;
  • wiersze 33–36: wyznaczają obszar o różowym tle (class="alert alert-danger") (wiersz 33);

Image

  • wiersz 34: tekst;
  • wiersz 35: tag HTML <ul> (lista nieuporządkowana) wyświetla listę punktowaną. Każdy element listy musi mieć składnię <li>element</li>;

Z tego kodu zapamiętajmy elementy dynamiczne, które należy zdefiniować:

  • [$modèle→error]: do wyświetlania komunikatu o błędzie;
  • [$modèle→erreurs]: lista (w rozumieniu HTML tego terminu) komunikatów o błędach;

23.13.2.2. Fragment [v-bandeau.php]

Fragment [v-bandeau.php] wyświetla górny pasek wszystkich widoków aplikacji internetowej:

Image

Kod fragmentu [v-bandeau.php] jest następujący:


<!-- Jumbotron 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>

Komentarze

  • wiersze 2–13: pasek nagłówkowy jest umieszczony w sekcji Bootstrap typu Jumbotron o nazwie [<div class="jumbotron">]. Ta klasa Bootstrap nadaje wyświetlanej treści specyficzny styl, aby ją wyróżnić;
  • wiersze 3–12: wiersz Bootstrap;
  • wiersze 4–6: obraz [img] umieszczony jest w pierwszych czterech kolumnach wiersza;
  • wiersz 5: składnia [<?= $logo ?>] jest równoważna składni [<?php print $logo ?>]. Innymi słowy, wartość atrybutu [src] będzie równa wartości zmiennej PHP [$logo];
  • wiersze 7–11: pozostałe 8 kolumn w wierszu (przypominamy, że jest ich łącznie 12) posłużą do umieszczenia tekstu (wiersz 9) dużymi literami (<h1>, wiersze 8–10);

Elementy dynamiczne:

  • [$logo]: URL z obrazu wyświetlanego w banerze;

23.13.2.3. Fragment [v-authentification.php]

Fragment [v-authentification .php] wyświetla formularz uwierzytelniający aplikacji internetowej:

Image

Kod fragmentu [v-authentification.php] jest następujący:


<!-- formularz HTML – wartości są wysyłane za pomocą akcji [authentifier-utilisateur] -->
<form method="post" action="main.php?action=authentifier-utilisateur">

    <!-- tytuł -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>

    <!-- formularz Bootstrap -->
    <fieldset class="form-group">
        <!-- pierwszy wiersz -->
        <div class="form-group row">
            <!-- opis -->
            <label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
            <div class="col-md-4">
                <!-- pole tekstowe -->
                <input type="text" class="form-control" id="user" name="user"
                       placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
            </div>
        </div>
        <!-- drugi wiersz -->
        <div class="form-group row">
            <!-- opis -->
            <label for="password" class="col-md-3 col-form-label">Mot de passe</label>
            <!-- pole wprowadzania tekstu -->
            <div class="col-md-4">
                <input type="password" class="form-control" id="password" name="password"
                       placeholder="Mot de passe">
            </div>
        </div>
        <!-- przycisk typu [submit] w trzecim wierszu-->
        <div class="form-group row">
            <div class="col-md-2">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>

</form>

Komentarze

  • wiersze 2–39: tag <form> wyznacza formularz HTML. Formularz ten ma zazwyczaj następujące cechy:
    • definiuje pola wprowadzania danych (tagi <input> w wierszach 17 i 27);
    • zawiera przycisk typu [submit] (wiersz 34), który wysyła wprowadzone wartości do elementu URL wskazanego w atrybucie [action] tagu [form] (wiersz 2). Metoda HTTP używana do wywołania tej metody URL jest określona w atrybucie [method] tagu [form] (wiersz 2);
    • w tym przypadku, gdy użytkownik kliknie przycisk [Valider] (wiersz 34), przeglądarka prześle (wiersz 2) wartości wprowadzone w formularzu do URL [main.php?action=authentifier-utilisateur] (wiersz 2);
    • przesłane wartości to wartości wprowadzone przez użytkownika w polach wprowadzania danych w wierszach 17 i 27. Zostaną one przesłane w postaci [user=xx&password=yy]. Nazwy parametrów [user, password] odpowiadają atrybutom [name] pól wprowadzania danych w wierszach 17 i 27;
  • wiersz 5–7: sekcja Bootstrap służąca do wyświetlenia tytułu na niebieskim tle:

Image

  • wiersze 10–37: formularz Bootstrap. Wszystkie elementy formularza zostaną wówczas stylizowane w określony sposób;
  • wiersze 12–20: definiują pierwszy wiersz formularza:

Image

  • wiersz 14 definiuje etykietę [1] w trzech kolumnach. Atrybut [for] tagu [label] łączy ten tekst z atrybutem [id] pola wprowadzania danych w wierszu 17;
  • wiersze 15–19: umieszcza pole wprowadzania danych w układzie czterech kolumn;
  • wiersz 17: znacznik HTML [input] opisuje pole wprowadzania danych. Posiada on kilka parametrów:
    • [type=’text’]: jest to pole tekstowe. Można w nim wpisać dowolną treść;
    • [class=’form-control’]: styl Bootstrap dla pola wprowadzania danych;
    • [id=’user’]: identyfikator pola wprowadzania danych. Identyfikator ten jest zazwyczaj wykorzystywany przez CSS oraz kod JavaScript;
    • [name=’user’]: nazwa pola tekstowego. Pod tą nazwą wartość wprowadzona przez użytkownika zostanie przesłana przez przeglądarkę [user=xx];
    • [placeholder=’invite’]: tekst wyświetlany w polu wprowadzania danych, gdy użytkownik jeszcze nic nie wpisał;

Image

  • [value=’valeur’]: tekst „wartość” zostanie wyświetlony w polu wprowadzania danych, gdy tylko zostanie ono wyświetlone, a więc zanim użytkownik wprowadzi cokolwiek innego. Mechanizm ten jest wykorzystywany w przypadku błędu w celu wyświetlenia wpisu, który spowodował błąd. W tym przypadku wartością tą będzie wartość zmiennej PHP [$modèle→login];
  • wiersze 21–30: analogowy kod dla wprowadzania hasła;
  • wiersz 27: [type=’password’] powoduje, że mamy pole do wprowadzania tekstu (można wpisać cokolwiek), ale wpisane znaki są ukryte:

Image

  • wiersze 32–36: trzeci wiersz dla przycisku [Valider];
  • wiersz 34: ponieważ przycisk ten posiada atrybut [type=submit], kliknięcie go powoduje wysłanie przez przeglądarkę wprowadzonych wartości do serwera, tak jak wyjaśniono to wcześniej. Atrybut CSS [class="btn btn-primary"] powoduje wyświetlenie niebieskiego przycisku:

Image

Pozostało nam wyjaśnić jeszcze jedną rzecz. W wierszu 2 atrybut [action="main.php?action=authentifier-utilisateur"] definiuje niekompletny atrybut URL (nie zaczyna się on od http://machine:port/chemin). W naszym przykładzie wszystkie elementy URL w aplikacji mają postać [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. Widok uwierzytelnienia zostanie uzyskany za pomocą różnych elementów 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]

Te identyfikatory URL odnoszą się do dokumentu [main.php] znajdującego się w ścieżce [http://localhost/php7/scripts-web/impots/version-12]. Dotyczy to wszystkich identyfikatorów URL w tej aplikacji. Parametr [action="main.php?action=authentifier-utilisateur"] zostanie opatrzony tym ścieżką podczas wysyłania wprowadzonych wartości. Zostaną one zatem przesłane do URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur].

23.13.2.4. Testy wizualne

Testy widoków można przeprowadzić na długo przed ich wdrożeniem do aplikacji. Chodzi tu o sprawdzenie ich wyglądu. Wszystkie widoki testowe umieścimy w folderze [Tests] projektu:

Image

Aby przetestować widok [vue-authentification.php], musimy utworzyć model danych, który będzie on wyświetlał:


<?php
// dane testowe strony
//
// obliczamy szablon widoku
$modèle = getModelForThisView();

function getModelForThisView(): object {
  // dane strony są umieszczane w $modèle
  $modèle = new \stdClass();
  // identyfikator użytkownika
  $modèle->login = "albert";
  // lista błędów
  $modèle->error = TRUE;
  $erreurs = ["erreur1", "erreur2"];
  // tworzy się listę HTML błędów
  $content = "";
  foreach ($erreurs as $erreur) {
    $content .= "<li>$erreur</li>";
  }
  $modèle->erreurs = $content;
  // zdjęcie w banerze
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // przekazujemy szablon
  return $modèle;
}
?>

<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Wymagane metatagi -->

    </head>
    <body>
        ….
    </body>
</html>

Komentarze

  • wiersze 1–5: widok uwierzytelniania zawiera części dynamiczne kontrolowane przez obiekt [$modèle]. Obiekt ten nazywamy modelem widoku. Zgodnie z jedną z dwóch definicji podanych dla skrótu MVC mamy tu do czynienia z literą M z MVC;
  • wiersz 5: szablon widoku jest obliczany przez funkcję [getModelForThisView];
  • wiersz 9: szablon widoku zostanie zamknięty w typie [stdClass];
  • wiersze 10–22: definiuje się wartości testowe dla dynamicznych elementów widoku uwierzytelniania;

Test wizualny można przeprowadzić w środowisku NetBeans:

Image

Testy wizualne kontynuuje się do momentu uzyskania zadowalającego wyniku.

23.13.2.5. Obliczanie modelu widoku

Po ustaleniu wyglądu widoku można przystąpić do obliczenia modelu widoku w rzeczywistych warunkach. Przypomnijmy kody stanów, które prowadzą do tego widoku. Znajdują się one w pliku konfiguracyjnym:


"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"

Są to zatem kody stanu [700, 221, 400], które powodują wyświetlenie widoku uwierzytelniania. Aby poznać znaczenie tych kodów, można skorzystać z testów [Postman] przeprowadzonych na aplikacji jSON:

  • [init-session-json-700]: 700 to kod stanu po pomyślnym wykonaniu akcji [init-session]: wyświetlany jest wówczas pusty formularz uwierzytelniający;
  • [authentifier-utilisateur-221]: 221 to kod stanu po nieudanej operacji [authentifier-utilisateur] (nieuznane dane uwierzytelniające): wyświetlany jest wówczas formularz uwierzytelniający w celu wprowadzenia poprawek;
  • [fin-session-400]: 400 to kod stanu po pomyślnym zakończeniu akcji [fin-session]: wyświetlany jest wówczas pusty formularz uwierzytelniający;

Teraz, gdy wiemy, w jakich sytuacjach należy wyświetlić formularz uwierzytelniający, możemy obliczyć jego szablon w [vue-authentification.php]:

Image

Kod obliczający szablon widoku [vue-authentification.php] jest następujący:


<?php
// dziedziczymy następujące zmienne
// Żądanie $request: bieżące żądanie
// Sesja $session: sesja aplikacji
// tablica $config: konfiguracja aplikacji
// tablica $content: odpowiedź kontrolera
//
// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

// obliczamy model widoku
$modèle = getModelForThisView($request, $session, $config, $content);

function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new stdClass();
  // stan aplikacji
  $état = $content["état"];
  // model zależy od stanu
  switch ($état) {
    case 700:
    case 400:
      // przypadek wyświetlania pustego formularza
      $modèle->login = "";
      // nie ma błędu do wyświetlenia
      $modèle->error = FALSE;
      break;
    case 221:
      // nieprawidłowe uwierzytelnienie
      // ponownie wyświetlany jest pierwotnie wprowadzony użytkownik
      $modèle->login = $request->request->get("user");
      // należy wyświetlić błąd
      $modèle->error = TRUE;
      // lista komunikatów o błędach HTML – w tym przypadku tylko jeden
      $modèle->erreurs = "<li>Echec de l'authentification</li>";
  }
  // wynik
  return $modèle;
}
?>

<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Komentarze

  • wiersze 3–6: wywołujemy zmienne odziedziczone po klasie [HtmlResponse], która za pomocą [require] wyświetla widok [vue-authentification.php];
  • wiersze 9–10: klasy Symfony używane w kodzie widoku;
  • wiersze 15–40: funkcja [getModelForThisView] odpowiada za obliczenie modelu widoku;
  • wiersz 19: pobierany jest kod stanu zwrócony przez kontroler, który przetworzył bieżącą akcję;
  • wiersze 21–37: szablon zależy od tego kodu stanu;
  • wiersze 22–28: sytuacja, w której należy wyświetlić pusty formularz uwierzytelniający;
  • wiersze 29–37: sytuacja nieprawidłowego uwierzytelnienia: wyświetla się identyfikator wprowadzony przez użytkownika oraz komunikat o błędzie. Użytkownik może wówczas ponownie spróbować uwierzytelnić się za pomocą klawiatury;

Dla paska nagłówkowego [v-bandeau.php] napisano specjalny szablon:


<?php
  // logo
  $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>

Komentarze

  • w wierszu 16 wykorzystano zmienną [$modèle→logo], która odpowiada wartości URL z logo paska. Zamiast obliczać tę zmienną cztery razy dla czterech widoków aplikacji, obliczenie to zostało wyodrębnione do fragmentu [v-bandeau.php];
  • wiersze 1–11 pokazują, jak zbudować URL i [http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] na podstawie informacji znalezionych w środowisku serwera [$request→server];

23.13.2.6. Testy [Postman]

Stworzyliśmy już zapytania generujące kody [700, 221, 400], które wyświetlają ekran uwierzytelniania. Przypomnijmy je:

  • [init-session-html-700]: 700 to kod stanu po pomyślnym wykonaniu akcji [init-session]: wyświetlany jest wówczas pusty formularz uwierzytelniający;
  • [authentifier-utilisateur-221]: 221 to kod stanu po nieudanej akcji [authentifier-utilisateur] (nieznane dane logowania): wyświetlany jest wówczas formularz uwierzytelniający w celu wprowadzenia poprawek;
  • [fin-session-400]: 400 to kod stanu po pomyślnym zakończeniu akcji [fin-session]: wyświetlany jest wówczas pusty formularz uwierzytelniający;

Wystarczy je ponownie wykorzystać i sprawdzić, czy prawidłowo wyświetlają ekran uwierzytelniania. Poniżej przedstawiono tylko dwa testy:

  • [init-session-html-700]: rozpoczęcie sesji HTML;

Image

  • [authentifier-utilisateur-221]: uwierzytelnienie użytkownika [x, x];

Image

Powyżej:

  • żądanie wysłało ciąg znaków [user=x&password=x];
  • w [4] wyświetlono komunikat o błędzie;
  • w [3] ponownie wyświetlono nieprawidłowego użytkownika;

23.13.2.7. Wniosek

Udało nam się przetestować widok [vue-authentification.php] bez konieczności pisania pozostałych widoków. Było to możliwe, ponieważ:

  • wszystkie kontrolery są już napisane;
  • [Postman] pozwala nam wysyłać zapytania do serwera bez konieczności korzystania z widoków. Podczas pisania kontrolerów należy mieć świadomość, że każdy może to zrobić. Należy więc być przygotowanym na obsługę zapytań, których żaden widok nie obsługuje. Są one tworzone ręcznie w [Postman]. Nigdy nie należy z góry zakładać, że „to zapytanie jest niemożliwe”. Trzeba to sprawdzić;

23.13.3. Widok służący do obliczania podatku

23.13.3.1. Opis widoku

Widok obliczania podatku wygląda następująco:

Image

Widok składa się z trzech części:

  • 1: górny pasek jest generowany przez fragment [v-bandeau.php], który został już przedstawiony;
  • 2: formularz obliczania podatku generowany przez fragment [v-calcul-impot.php];
  • 3: menu zawierające dwa linki, generowane przez fragment [v-menu.php];

Widok obliczania podatku jest generowany przez następujący skrypt [vue-calcul-impot.php]:

Image


<?php
// dziedziczymy następujące zmienne
// Żądanie $request: bieżące żądanie
// Sesja $session: sesja aplikacji
// tablica $config: konfiguracja aplikacji
// tablica $content: odpowiedź kontrolera, który przetworzył akcję
//
// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

// obliczamy model widoku
$modèle = getModelForThisView($request, $session, $config, $content);

function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new \stdClass();

  // zwracamy model
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Wymagane metatagi -->
        <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">
            <!-- pasek nagłówkowy -->
            <?php require "v-bandeau.php"?>
            <!-- układ dwukolumnowy -->
            <div class="row">
                <!-- menu -->
                <div class="col-md-3">
                    <?php require "v-menu.php" ?>
                </div>
                <!-- formularz obliczeniowy -->
                <div class="col-md-9">
                    <?php require "v-calcul-impot.php" ?>
                </div>
            </div>  
            <!-- przypadek powodzenia -->
            <?php
            if ($modèle->success) {
              // wyświetla się komunikat o pomyślnym zakończeniu
              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) {
              // lista błędów w 9 kolumnach
              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>

Komentarze

  • komentujemy wyłącznie nowości, z którymi jeszcze się nie spotkaliśmy;
  • wiersz 37: włączenie górnego paska widoku do pierwszego wiersza Bootstrap w widoku;
  • wiersze 41–43: umieszczenie menu, które zajmie trzy kolumny drugiego wiersza Bootstrap w widoku;
  • wiersze 45–47: wstawienie formularza do obliczania podatku, który zajmie dziewięć kolumn drugiego wiersza Bootstrap w widoku;
  • wiersze 51–69: jeśli obliczenie podatku zakończy się powodzeniem ([$modèle→success=TRUE]), wówczas wynik obliczenia podatku jest wyświetlany w zielonej ramce (wiersze 59–65). Ramka ta znajduje się w trzecim wierszu Bootstrap widoku (wiersz 54) i zajmuje dziewięć kolumn (wiersz 58) po prawej stronie trzech pustych kolumn (wiersze 55–57). Ramka ta będzie zatem znajdować się bezpośrednio pod formularzem obliczania podatku;
  • wiersze 71–87: jeśli obliczenie podatku dla [$modèle→error=TRUE] zakończy się niepowodzeniem, wówczas komunikat o błędzie zostanie wyświetlony w różowej ramce (wiersze 80–83). Ramka ta znajduje się w trzecim wierszu Bootstrap widoku (wiersz 75) i zajmuje dziewięć kolumn (wiersz 79) po prawej stronie trzech pustych kolumn (wiersze 76–78). Ramka ta będzie zatem znajdować się bezpośrednio pod formularzem obliczania podatku;

23.13.3.2. Fragment [v-calcul-impot.php]

Fragment [v-calcul-impot.php] wyświetla formularz uwierzytelniający aplikacji internetowej:

Image

Kod fragmentu [v-calcul-impot.php] jest następujący:


<!-- formularz HTML wysłany -->
<form method="post" action="main.php?action=calculer-impot">
    <!-- komunikat w 12 kolumnach na niebieskim tle -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </div>
    </div>
    <!-- elementy formularza -->
    <fieldset class="form-group">
        <!-- pierwszy wiersz w 9 kolumnach -->
        <div class="row">
            <!-- napis w 4 kolumnach -->
            <legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
            <!-- przyciski opcji w 5 kolumnach-->
            <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>
        <!-- drugi wiersz w 9 kolumnach -->
        <div class="form-group row">
            <!-- opis w 4 kolumnach -->
            <label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
            <!-- pole do wprowadzania liczby dzieci w 5 kolumnach -->
            <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>
        <!-- trzeci wiersz w 9 kolumnach -->
        <div class="form-group row">
            <!-- opis w 4 kolumnach -->
            <label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
            <!-- pole do wprowadzania liczby dzieci w 5 kolumnach -->
            <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>
        <!-- czwarty wiersz, przycisk [submit] w 5 kolumnach -->
        <div class="form-group row">
            <div class="col-md-5">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>

</form>

Uwagi

  • wiersz 2: formularz HTML zostanie przesłany (atrybut [method]) do URL [main.php?action=calculer-impot] (atrybut [action]). Przesłane wartości będą odpowiadały wartościom z pól wprowadzania danych:
    • wartość zaznaczonego przycisku opcji w postaci:
      • [marié=oui], jeśli zaznaczono przycisk opcji [Oui] (wiersze 16–22). [marié] to wartość atrybutu [name] z wiersza 18, [oui] to wartość atrybutu [value] z wiersza 18;
      • [marié=non], jeśli przycisk opcji [Non] jest zaznaczony (wiersze 23–28). [marié] to wartość atrybutu [name] z wiersza 24, a [non] to wartość atrybutu [value] z wiersza 24;
    • wartość pola wprowadzania liczbowego w wierszu 37 w postaci [enfants=xx], gdzie [enfants] jest wartością atrybutu [name] z wiersza 37, a [xx] to wartość wprowadzona przez użytkownika za pomocą klawiatury;
    • wartość pola wprowadzania liczbowego w wierszu 46 w postaci [salaire=xx], gdzie [salaire] jest wartością atrybutu [name] z wiersza 46, a [xx] to wartość wprowadzona przez użytkownika za pomocą klawiatury;

Ostatecznie wysłana wartość będzie miała postać [marié=xx&enfants=yy&salaire=zz].

  • Wprowadzone wartości zostaną zapisane, gdy użytkownik kliknie przycisk typu [submit] w wierszu 53;
  • wiersze 16–30: dwa przyciski opcji:

Image

Oba przyciski opcji należą do tej samej grupy przycisków opcji, ponieważ mają ten sam atrybut [name] (wiersze 18, 24). Przeglądarka dba o to, aby w grupie przycisków opcji w danym momencie był zaznaczony tylko jeden. Dlatego kliknięcie jednego z nich powoduje odznaczenie tego, który był zaznaczony wcześniej;

  • są to przyciski opcji ze względu na atrybut [type="radio"] (wiersze 18, 24);
  • podczas wyświetlania formularza (przed wprowadzeniem danych) jeden z przycisków opcji musi być zaznaczony: w tym celu wystarczy dodać atrybut [checked=’checked’] do odpowiedniego tagu <input type="radio">. Odbywa się to za pomocą zmiennych dynamicznych:
    • [<?= $modèle->checkedOui ?>] w wierszu 18;
    • [<?= $modèle->checkedNon ?>] w wierszu 24;

Zmienne te będą częścią szablonu widoku.

  • wiersz 37: pole wprowadzania liczbowego [type="number"] o minimalnej wartości 0 [min="0"]. W nowszych przeglądarkach oznacza to, że użytkownik będzie mógł wprowadzić jedynie liczbę >=0. W tych samych nowszych przeglądarkach wprowadzenie wartości może odbywać się za pomocą suwaka, który można przesuwać w górę lub w dół. Atrybut [step="1"] w wierszu 37 wskazuje, że suwak będzie działał z krokami co 1 jednostkę. W rezultacie suwak przyjmie wyłącznie wartości całkowite w zakresie od 0 do n z krokiem równym 1. W przypadku ręcznego wprowadzania danych oznacza to, że liczby z przecinkiem nie będą akceptowane;

Image

  • wiersz 37: w niektórych przypadkach pole wprowadzania danych dotyczących dzieci powinno być wstępnie wypełnione ostatnią wartością wprowadzoną w tym polu. W tym celu stosuje się atrybut [value], który określa wartość wyświetlaną w polu wprowadzania danych. Wartość ta będzie dynamiczna i generowana przez zmienną [$modèle→enfants];
  • wiersz 46: te same wyjaśnienia dotyczące wprowadzania wynagrodzenia, co w przypadku dzieci;
  • wiersz 53: przycisk typu [submit], który uruchamia POST z wartościami wprowadzonymi w URL i [main.php?action=calculer-impot];

Image

23.13.3.3. Fragment [v-menu.php]

Ten fragment wyświetla menu po lewej stronie formularza obliczania podatku:

Image

Kod tego fragmentu jest następujący:


<!-- menu Bootstrap -->
<nav class="nav flex-column">
    <?php
    // wyświetlanie listy linków HTML
    foreach($modèle->optionsMenu as $texte=>$url){
      print <<<EOT3
      <a class="nav-link" href="$url">$texte</a>
EOT3;
    }
    ?>
</nav>

Komentarze

  • wiersze 2–11: tag HTML [nav] otacza fragment dokumentu HTML zawierający linki nawigacyjne do innych dokumentów;
  • wiersz 7: znacznik HTML [a] wprowadza link nawigacyjny:
    • [$url]: jest to URL, do której następuje przejście po kliknięciu linku [$texte]. Jest to zatem operacja [GET $url] wykonywana przez przeglądarkę. Jeśli [$url] jest linkiem względnym do URL, to poprzedza go korzeń adresu URL aktualnie wyświetlanego w pasku adresu przeglądarki. Zatem aby uzyskać link [1], podczas gdy aktualny link URL w przeglądarce jest typu [http://chemin/main.php?paramètres], utworzymy link:
<a href=’main.php?action=liste-simulation’>Liste des simulations</a>
  • wiersz 5: szablon fragmentu o nazwie [$modèle→optionsMenu] będzie tablicą o postaci:
[‘ Liste des simulations’=>’main.php?action=liste-simulations’,
‘ Fin de session’=>’main.php?action=fin-session’]
  • wiersze 2, 7: klasy CSS i [nav, flex-column, nav-link] to klasy Bootstrap, które określają wygląd menu;

23.13.3.4. Test wizualny

Gromadzimy te różne elementy w folderze [Tests] i tworzymy szablon testowy dla widoku [vue-calcul-impot.php]:

Image

Model danych widoku [vue-calcul-impot] będzie wyglądał następująco:


<?php
// dane testowe strony
//
// obliczamy szablon widoku
$modèle = getModelForThisView();

function getModelForThisView(): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new \stdClass();
  // formularz
  $modèle->checkedOui = "";
  $modèle->checkedNon = 'checked="checked"';
  $modèle->enfants = 2;
  $modèle->salaire = 300000;
  // komunikat o pomyślnym zakończeniu
  $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 %";
  // komunikat o błędzie
  $modèle->error = TRUE;
  $erreurs = ["erreur1", "erreur2"];
  // tworzy się listę HTML błędów
  $content = "";
  foreach ($erreurs as $erreur) {
    $content .= "<li>$erreur</li>";
  }
  $modèle->erreurs = $content;
  // menu
  $modèle->optionsMenu = [
    'Lista symulacji' => 'main.php?action=lista-symulacji',
    'Zakończenie sesji' => 'main.php?action=fin-session'];
  // obraz w banerze
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // przesyłamy szablon
  return $modèle;
}

?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Komentarze

  • wiersze 7–39: inicjujemy wszystkie dynamiczne części widoku [vue-calcul-impot.php] oraz fragmentów [v-calcul-impot.php] i [v-menu.php];

Sprawdzamy widok [vue-calcul-impot.php]:

Image

Otrzymujemy następujący wynik:

Image

Pracujemy nad tym widokiem, aż uzyskany efekt wizualny będzie dla nas satysfakcjonujący. Następnie możemy przejść do integracji widoku z tworzoną aplikacją internetową.

23.13.3.5. Obliczanie modelu widoku

Image

Po ustaleniu wyglądu widoku można przystąpić do obliczenia modelu widoku w rzeczywistych warunkach. Przypomnijmy kody stanów, które prowadzą do tego widoku. Znajdują się one w pliku konfiguracyjnym:


"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"

Są to zatem kody stanu [200, 300, 341, 350, 800], które powodują wyświetlenie ekranu uwierzytelniania. Aby poznać znaczenie tych kodów, można skorzystać z testów [Postman] przeprowadzonych w aplikacji jSON:

  • [authentifier-utilisateur-200]: 200 to kod stanu po pomyślnym wykonaniu akcji [authentifier-itilisateur]: wyświetlany jest wówczas pusty formularz do obliczania podatku;
  • [calculer-impot-300]: 300 to kod stanu po pomyślnym zakończeniu operacji [calculer-impot]. Wyświetlany jest wówczas formularz obliczeniowy z wprowadzonymi danymi oraz kwotą podatku. Użytkownik może wówczas wykonać kolejne obliczenie;
  • [fin-session-400]: 400 to kod stanu po pomyślnym zakończeniu akcji [fin-session]: wyświetlany jest wówczas pusty formularz uwierzytelniający;
  • kod stanu [341] jest kodem uzyskanym w przypadku prawidłowego obliczenia podatku, ale brak połączenia z serwerem SGBD powoduje błąd;
  • kod stanu [350] jest kodem uzyskanym w przypadku prawidłowego obliczenia podatku, ale brak połączenia z serwerem [Redis] powoduje błąd;
  • kod stanu [800] zostanie przedstawiony w późniejszym terminie. Nie spotkaliśmy się z nim jeszcze;
  • założono tutaj, że użytkownik korzysta z najnowszej wersji przeglądarki. W związku z tym w analizowanym formularzu nie ma możliwości wpisania liczb ujemnych, ciągów znaków niebędących cyframi ani liczb z przecinkiem w polach wprowadzania danych [enfants, salaire]. W przypadku starszych przeglądarek byłoby to możliwe. Błędy te potraktujemy jako błędy nieoczekiwane i wyświetlimy wówczas widok [vue-erreurs];

Teraz, gdy wiemy, w jakich sytuacjach należy wyświetlić formularz obliczania podatku, możemy opracować jego szablon w [vue-calcul-impot.php]:


<?php
// dziedziczymy następujące zmienne
// Żądanie $request: bieżące żądanie
// Sesja $session: sesja aplikacji
// tablica $config: konfiguracja aplikacji
// tablica $content: odpowiedź kontrolera, który przetworzył akcję
//
// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

// obliczamy model widoku
$modèle = getModelForThisView($request, $session, $config, $content);

function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new \stdClass();
  // stan aplikacji
  $état = $content["état"];
  // model zależy od stanu
  switch ($état) {
    case 200 :
    case 800:
      // początkowe wyświetlenie pustego formularza
      $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:
      // pomyślne wykonanie obliczeń – wyświetlenie wyniku
      $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) . " %";
      // formularz przywrócony z wprowadzonymi wartościami
      $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:
    // baza danych HS
    case 350:
      // serwer Redis HS
      // formularz przywrócony z wprowadzonymi wartościami
      $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");
      // błąd
      $modèle->success = FALSE;
      $modèle->error = TRUE;
      $modèle->erreurs = "<li>{$content["réponse"]}</li>";
      break;
  }
  //menu
  $modèle->optionsMenu = [
    "Liste des simulations" => "main.php?action=lister-simulations",
    "Fin de session" => "main.php?action=fin-session"];
  // zwracamy szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
        <title>Application impots</title>
    </head>
    <body>
        
    </body>
</html>

Komentarze

  • wiersze 22–30: wyświetlenie pustego formularza;
  • wiersze 31–45: przypadek pomyślnego obliczenia podatku. Ponownie wyświetlane są wprowadzone wartości oraz kwota podatku;
  • wiersze 46–59: sytuacja, w której obliczenie podatku nie powiodło się z powodu niedostępności jednego z serwerów [Redis] lub [MySQL];
  • wiersze 62–64: obliczenie dwóch opcji z menu;

23.13.3.6. Testy [Postman]

Test [calculer-impot-300] pozwala uzyskać kod stanu 300. Odpowiada on pomyślnemu obliczeniu podatku:

Image

  • w [3] wartości, które doprowadziły do wyniku [2];

Sprawdźmy przypadek błędu: błąd [350] spowodowany niedostępnością serwera [Redis]:

Image

23.13.4. Widok listy symulacji

23.13.4.1. Opis widoku

Widok przedstawiający listę symulacji wygląda następująco:

Image

Widok wygenerowany przez skrypt [vue-liste-simulations] składa się z trzech części:

  • 1: górny pasek jest generowany przez fragment [v-bandeau.php], który został już przedstawiony;
  • 2: tabela symulacji generowana przez fragment [v-liste-simulations.php];
  • 3: menu zawierające dwa linki, wygenerowane przez fragment [v-menu.php];

Widok symulacji jest generowany przez następujący skrypt [vue-liste-simulations.php]:

Image


<?php

// obliczamy szablon widoku
$modèle = getModelForThisView();

function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new \stdClass();
  
  // zwracamy szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Wymagane metatagi -->
        <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">
            <!-- pasek nagłówkowy -->
            <?php require "v-bandeau.php"; ?>
            <!-- układ dwukolumnowy -->
            <div class="row">
                <!-- menu w trzech kolumnach-->
                <div class="col-md-3">
                    <?php require "v-menu.php" ?>
                </div>
                <!-- lista symulacji w 9 kolumnach-->
                <div class="col-md-9">
                    <?php require "v-liste-simulations.php" ?>
                </div>
            </div>  
        </div>
    </body>
</html>

Komentarze

  • wiersz 28: dołączenie paska aplikacji [1];
  • wiersz 33: dodanie menu [2]. Zostanie ono wyświetlone w trzech kolumnach pod banerem;
  • wiersz 37: wstawienie tabeli symulacji [3]. Zostanie ona wyświetlona w dziewięciu kolumnach pod paskiem nagłówkowym i po prawej stronie menu;

Omówiliśmy już dwa z trzech fragmentów tego widoku:

Fragment [v-liste-simulations.php] wygląda następująco:


<!-- komunikat na niebieskim tle -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>
<!-- tabela symulacji -->
<table class="table table-sm table-hover table-striped">
    <!-- nagłówki sześciu kolumn tabeli -->
    <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>
    <!-- treść tabeli (wyświetlane dane) -->
    <tbody>
        <?php
        $i = 0;
        // każda symulacja jest wyświetlana poprzez przeglądanie tabeli symulacji
        foreach ($modèle->simulations as $simulation) {
          // wyświetlanie wiersza tabeli z 6 kolumnami – tag <tr>
          // kolumna 1: nagłówek wiersza (nr symulacji) – tag <th scope='row'>
          // kolumna 2: wartość parametru [marié] – tag <td>
          // kolumna 3: wartość parametru [enfants] – tag <td>
          // kolumna 4: wartość parametru [salaire] – tag <td>
          // kolumna 5: wartość parametru [impôt] (podatku) – tag <td>
          // kolumna 6: wartość parametru [surcôte] – tag <td>
          // kolumna 7: wartość parametru [décôte] – tag <td>
          // kolumna 8: wartość parametru [réduction] – tag <td>
          // kolumna 9: wartość parametru [taux] (podatku) – tag <td>
          // kolumna 10: link do usunięcia symulacji – tag <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>

Komentarze

  • tabela HTML została utworzona za pomocą tagu <table> (wiersze 6 i 58);
  • nagłówki kolumn tabeli znajdują się wewnątrz tagu <thead> (nagłówek tabeli, wiersze 8, 21). Tag <tr> (wiersz tabeli, wiersze 9 i 20) wyznacza wiersz. W wierszach 10–15 tag <th> (nagłówek tabeli) definiuje nagłówek kolumny. Jest ich więc dziesięć. [scope="col"] oznacza, że nagłówek odnosi się do kolumny. [scope="row"] oznacza, że nagłówek odnosi się do wiersza;
  • wiersze 23–57: znacznik <tbody> otacza dane wyświetlane w tabeli;
  • wiersze 40–51: tag <tr> otacza wiersz tabeli;
  • wiersz 41: tag <th scope=’row’> definiuje nagłówek wiersza;
  • wiersze 42–50: każdy tag <td> definiuje kolumnę wiersza;
  • wiersz 27: lista symulacji znajduje się w szablonie [$modèle→simulations], który jest tabelą asocjacyjną;
  • wiersz 50: link umożliwiający usunięcie symulacji. Model URL przejmuje numer wyświetlany w pierwszej kolumnie tabeli (wiersz 41);

23.13.4.2. Test wizualny

Gromadzimy te różne elementy w folderze [Tests] i tworzymy model testowy dla widoku [vue-liste-simulations.php]:

Image

Model danych widoku [vue-liste-simulations] będzie wyglądał następująco:


<?php
// obliczamy szablon widoku
$modèle = getModelForThisView();

function getModelForThisView(): object {
  // dane strony są umieszczane w $modèle
  $modèle = new \stdClass();
  // przekształca się symulacje do formatu wymaganego przez stronę
  $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
    ]
  ];
  // opcje menu
  $modèle->optionsMenu = [
    "Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
    "Fin de session" => "main.php?action=fin-session"];
  // obraz w banerze
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // generuje się szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Komentarze

  • wiersze 9–30: tabela symulacji wyświetlanych przez tabelę HTML;
  • wiersze 32–34: tabela opcji menu;

Wyświetlmy ten widok:

Image

Otrzymujemy następujący wynik:

Image

Pracujemy nad tym widokiem, aż uzyskany efekt wizualny będzie nas satysfakcjonował. Następnie możemy przejść do integracji widoku z tworzoną aplikacją internetową.

23.13.4.3. Obliczanie modelu widoku

Image

Po ustaleniu wyglądu widoku można przystąpić do obliczenia modelu widoku w rzeczywistych warunkach. Przypomnijmy kody stanów, które prowadzą do tego widoku. Znajdują się one w pliku konfiguracyjnym:


"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"

Są to zatem kody stanu [500, 600], które powodują wyświetlenie widoku symulacji. Aby poznać znaczenie tych kodów, można skorzystać z testów [Postman] przeprowadzonych w aplikacji jSON:

  • [lister-simulations-500]: 500 to kod stanu po pomyślnym wykonaniu akcji [lister-simulations]: wyświetlana jest wówczas lista symulacji przeprowadzonych przez użytkownika;
  • [supprimer-simulation-600]: 600 to kod stanu po pomyślnym zakończeniu akcji [supprimer-simulation]. Wyświetlana jest wówczas nowa lista symulacji uzyskana po tym usunięciu;

Teraz, gdy wiemy, w jakich momentach należy wyświetlić listę symulacji, możemy obliczyć jej model w [vue-liste-simulations.php]:


<?php
// dziedziczymy następujące zmienne
// Żądanie $request: bieżące żądanie
// Sesja $session: sesja aplikacji
// tablica $config: konfiguracja aplikacji
// tablica $content: odpowiedź kontrolera
// brak możliwych błędów
// tablica $content: odpowiedź kontrolera
//
// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

// obliczamy model widoku
$modèle = getModelForThisView($request, $session, $config, $content);

function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new \stdClass();
  // przekształcamy symulacje do formatu oczekiwanego przez stronę
  // znajdują się one w odpowiedzi kontrolera, który wykonał akcję
  // w postaci tablicy obiektów typu [Simulation]
  $objetsSimulation = $content["réponse"];
  // każdy obiekt typu [Simulation] zostanie przekształcony w tablicę asocjacyjną
  $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()
    ];
  }
  // opcje menu
  $modèle->optionsMenu = [
    "Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
    "Fin de session" => "main.php?action=fin-session"];
  // przekształcamy szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
       
    </body>
</html>

Komentarze

  • wiersze 26–36: obliczenie modelu [$modèle→simulations] używanego przez fragment [v-liste-simulations.php];
  • wiersze 39–41: obliczenie modelu [$modèle→optionsMenu] wykorzystywanego przez fragment [v-menu.php];

23.13.4.4. Testy [Postman]

Test [lister-simulations-500] generuje kod stanu 500. Odpowiada on żądaniu wyświetlenia symulacji:

Image

Test [supprimer-simulation-600] powoduje wygenerowanie kodu stanu 600. Odpowiada to pomyślnemu usunięciu symulacji nr 0. Zwracanym wynikiem jest lista symulacji, z której usunięto jedną pozycję:

Image

23.13.5. Widok nieoczekiwanych błędów

W tym kontekście jako nieoczekiwany błąd określa się błąd, który nie powinien wystąpić podczas normalnego korzystania z aplikacji internetowej.

Weźmy na przykład test [Postman] [calculer-impot-3xx] zdefiniowany w następujący sposób:

Image

  • w [1-3], żądanie POST z akcją [calculer-impot];
  • na [4-6]: tutaj można zdefiniować dowolne wartości dla trzech parametrów POST:
    • [4]: brakuje parametru [marié];
    • [5-6]: parametry [enfants, salaire] występują, ale są nieprawidłowe;
  • w przypadku [9] te trzy błędy są zgłaszane z kodem stanu 338;

Jednak w formularzu HTML aplikacji internetowej taka sytuacja nie może wystąpić:

  • wszystkie parametry są obecne;
  • parametr [marié], którego wartość pochodzi z atrybutów [value] dwóch przycisków opcji, musi przyjąć jedną z wartości [oui] lub [non];
  • w nowszych przeglądarkach atrybuty <input type=’number’ min=’0’ step=’1’ …> sprawiają, że wartości wprowadzane w polach dotyczących dzieci i wynagrodzenia muszą być liczbami całkowitymi >=0;

Jednak nic nie uniemożliwia użytkownikowi wybrania wartości [Postman] i wysłania do naszego serwera powyższego testu [calcul-impot-3xx]. Widzieliśmy, że nasza aplikacja internetowa potrafiła poprawnie odpowiedzieć na to żądanie. Błąd nieoczekiwany to taki, który nie powinien wystąpić w ramach działania aplikacji HTML. Jeśli jednak do niego dojdzie, oznacza to prawdopodobnie, że ktoś próbuje „zhakować” aplikację. Ze względów edukacyjnych postanowiliśmy wyświetlać stronę błędów w takich przypadkach. W rzeczywistości moglibyśmy ponownie wyświetlić ostatnią stronę wysłaną do klienta. Wystarczy w tym celu zapisać w sesji ostatnią wysłaną odpowiedź HTML. W przypadku nieoczekiwanego błędu zwracamy tę odpowiedź. Dzięki temu użytkownik będzie miał wrażenie, że serwer nie reaguje na jego błędy, ponieważ wyświetlana strona nie ulega zmianie.

23.13.5.1. Prezentacja widoku

Widok przedstawiający nieoczekiwane błędy wygląda następująco:

Image

Widok wygenerowany przez skrypt [vue-erreurs.php] składa się z trzech części:

  • 1: górny pasek jest generowany przez fragment [v-bandeau.php], który został już przedstawiony;
  • 2: nieoczekiwany błąd lub błędy;
  • 3: menu zawierające trzy linki, wygenerowane przez fragment [v-menu.php];

Widok nieoczekiwanych błędów jest generowany przez następujący skrypt [vue-erreurs.php]:

Image


<?php
// obliczamy szablon widoku
$modèle = getModelForThisView();

function getModelForThisView(): object {
  // enkapsuluje się dane strony w $modèle
  $modèle = new \stdClass();

  // zwracany jest szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Wymagane metatagi -->
        <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">
            <!-- pasek w 12 kolumnach -->
            <?php require "v-bandeau.php"; ?>
            <!-- dwu-kolumnowy wiersz -->
            <div class="row">
                <!-- menu w 3 kolumnach-->
                <div class="col-md-3">
                    <?php require "v-menu.php" ?>
                </div>
                <!-- lista błędów -->
                <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>

Uwagi

  • wiersz 27: dołączenie paska aplikacji [1];
  • wiersz 32: wstawienie menu [2]. Zostanie ono wyświetlone w trzech kolumnach pod paskiem nagłówkowym;
  • wiersze 34–44: wyświetlenie obszaru błędów w dziewięciu kolumnach;
  • wiersze 37–44: operacja [print], która wyświetla nieoczekiwane błędy;
  • wiersz 38: wyświetlanie to nastąpi w ramce Bootstrap na różowym tle;
  • wiersz 39: tekst wprowadzający;
  • wiersz 40: tag <ul> otacza listę punktowaną. Lista ta pochodzi z szablonu [$modèle->erreurs];

Omówiliśmy już dwa fragmenty tego widoku:

23.13.5.2. Test wizualny

Zbieramy te różne elementy w folderze [Tests] i tworzymy szablon testowy dla widoku [vue-erreurs.php]:

Image

Model danych widoku [vue-erreurs.php] będzie wyglądał następująco:


<?php
// obliczamy szablon widoku
$modèle = getModelForThisView();

function getModelForThisView(): object {
  // dane strony są pakowane w $modèle
  $modèle = new \stdClass();

  // tabela nieoczekiwanych błędów
  $erreurs = ["erreur1", "erreur2"];
  // tworzy się listę błędów HTML
  $modèle->erreurs = "";
  foreach ($erreurs as $erreur) {
    $modèle->erreurs .= "<li>$erreur</li>";
  }
  // opcje menu
  $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",];
  // obraz banera
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // zwracamy szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Komentarze

  • wiersze 9–15: tworzenie listy błędów HTML;
  • wiersze 17–20: tablica opcji menu;

Wyświetlmy ten widok:

Image

Otrzymujemy następujący wynik:

Image

Pracujemy nad tym widokiem, aż uzyskany efekt wizualny będzie nas satysfakcjonował. Następnie możemy przejść do integracji widoku z tworzoną aplikacją internetową.

23.13.5.3. Obliczanie modelu widoku

Image

Po ustaleniu wyglądu widoku można przystąpić do obliczenia modelu widoku w rzeczywistych warunkach. Przypomnijmy kody stanów, które prowadzą do tego widoku. Znajdują się one w pliku konfiguracyjnym:


"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"

Są to zatem kody stanów, które nie znajdują się wśród tych z wierszy [2-4], które powodują wyświetlenie widoku nieoczekiwanych błędów.

Kod obliczeniowy modelu widoku [vue-erreurs.php] jest następujący:


<?php
// dziedziczymy następujące zmienne
// Żądanie $request: bieżące żądanie
// Sesja $session: sesja aplikacji
// tablica $config: konfiguracja aplikacji
// tablica $content: odpowiedź kontrolera
//
// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

// obliczamy model widoku
$modèle = getModelForThisView($request, $session, $config, $content);

function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // dane strony są enkapsulowane w $modèle
  $modèle = new \stdClass();

  // pobieranie błędów z odpowiedzi kontrolera
  $réponse = $content["réponse"];
  if (!is_array($réponse)) {
    // pojedynczy komunikat o błędzie
    $erreurs = [$réponse];
  } else {
    // kilka komunikatów o błędach
    $erreurs = $réponse;
  }
  // tworzymy listę HTML błędów
  $modèle->erreurs = "";
  foreach ($erreurs as $erreur) {
    $modèle->erreurs .= "<li>$erreur</li>";
  }
  // opcje menu
  $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",];

  // zwracany jest szablon
  return $modèle;
}
?>
<!-- dokument HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Komentarze

  • wiersze 19–32: obliczenia modelu [$modèle→erreurs] wykorzystywanego przez widok [vue-erreurs.php];
  • wiersze 34–37: obliczenia modelu [$modèle→optionsMenu] używanego przez fragment [v-menu.php];

23.13.5.4. Testy [Postman]

Test [calculer-impot-3xx] pozwala nam uzyskać kod stanu 338, który nie jest oczekiwanym kodem stanu. Odpowiedź HTML jest zatem następująca:

Image

23.13.6. Implementacja akcji menu aplikacji

W tym miejscu omówimy implementację opcji menu. Przypomnijmy znaczenie linków, z którymi się spotkaliśmy

Widok
Link
Cel
Rola
Obliczanie podatku
[Liste des simulations]
[main.php?action=lister-simulations]
Poproś o listę symulacji
 
[Fin de session]
[main.php?action=fin-session]
Lista symulacji
[Calcul de l’impôt]
[main.php?action=afficher-calcul-impot]
Wyświetl widok obliczeń podatku
 
[Fin de session]
[main.php?action=fin-session]
Nieoczekiwane błędy
[Calcul de l’impôt]
[main.php?action=afficher-calcul-impot]
Wyświetl widok obliczeń podatku
 
[Liste des simulations]
[main.php?action=lister-simulations]
 
[Fin de session]
[main.php?action=fin-session]

Należy pamiętać, że kliknięcie linku powoduje wykonanie akcji GET prowadzącej do miejsca docelowego linku. Akcje [lister-simulations, fin-session] zostały zaimplementowane za pomocą operacji GET, co pozwala nam ustawić je jako miejsca docelowe linków. Gdy akcja jest realizowana za pomocą POST, użycie linku nie jest już możliwe, chyba że zostanie on powiązany z kodem JavaScript.

Z powyższych akcji wynika, że akcja [afficher-calcul-impot] nie została jeszcze zaimplementowana. Jest to operacja nawigacji między dwoma widokami: serwer jSON lub XML nie ma powodu, by ją wdrażać, ponieważ nie obsługują one pojęcia widoku. To serwer HTML wprowadza to pojęcie.

Musimy zatem zaimplementować akcję [afficher-calcul-impot]. Pozwoli nam to przeanalizować sposób implementacji akcji w serwerze.

Najpierw musimy dodać nowy kontroler pomocniczy. Nazwiemy go [AfficherCalculImpotController]:

Image

Ten kontroler należy dodać do pliku konfiguracyjnego [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"
}
  • wiersz 15: nowy kontroler;
  • wiersz 30: nowa akcja i jej kontroler;
  • wiersz 35: nowy kontroler zwróci kod stanu 800. Przy zmianie widoku nie może wystąpić błąd;

Kontroler [AfficherCalculImpotController.php] będzie wyglądał następująco:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Response;

class AfficherCalculImpotController implements InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // zmiana widoku – wystarczy ustawić kod stanu
    return [Response::HTTP_OK, 800, ["réponse" => ""], []];
  }

}

Uwagi

  • wiersz 10: podobnie jak inne kontrolery pomocnicze, nowy kontroler implementuje interfejs [InterfaceController];
  • zmiany widoku są łatwe do wdrożenia: wystarczy ustawić kod stanu powiązany z widokiem docelowym, w tym przypadku kod 800, jak wspomniano powyżej;

23.13.7. Testy w rzeczywistych warunkach

Kod został napisany, a każda akcja przetestowana przy użyciu [Postman]. Pozostaje nam przetestować sekwencję widoków w rzeczywistych warunkach. Potrzebujemy sposobu na zainicjowanie sesji HTML. Wiemy, że należy wysłać do serwera parametry [action=init-session&type=html]. Aby uniknąć konieczności wpisywania ich w pasku adresu przeglądarki, dodamy do naszej aplikacji skrypt [index.php]:

Image

Skrypt [index.php] będzie wyglądał następująco:


<?php

// przekierowanie do [main.php] w trybie [html]
header('Location: main.php?action=init-session&type=html');
  • wiersz 4: [header] to funkcja PHP dodająca nagłówek HTTP do odpowiedzi. Nagłówek HTTP [Location: main.php?action=init-session&type=html] nakazuje przeglądarce klienckiej przekierowanie się do miejsca docelowego URL wskazanego w [Location]. Skrypt [index.php] jest wywoływany wraz z URL i [http://localhost/php7/scripts-web/impots/version-12/index.php]. Gdy przeglądarka klienta otrzyma przekierowanie do strony względnej URL z [main.php?action=init-session&type=html], zażąda adresu bezwzględnego URL (odnoszącego się do [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html]), a sesja HTML zostanie uruchomiona;

Adres startowy URL można uprościć do [http://localhost/php7/scripts-web/impots/version-12/]. W przypadku, gdy w URL nie określono żadnej strony, domyślnie wykorzystywane są strony [index.html, index.php]. W tym przypadku zostanie zatem użyty skrypt [index.php];

Zacznijmy: przedstawimy teraz kilka sekwencji widoków.

W naszej przeglądarce włączamy śledzenie żądań (F12 w przeglądarce Firefox) i wysyłamy żądanie uruchamiające URL:

Image

  • w [4] pierwszą odpowiedzią serwera jest przekierowanie 302:
  • w [5] wysyłane jest nowe żądanie do URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html];

Przyjrzyjmy się bliżej przekierowaniu 302:

Image

  • w [8] kod HTTP [302] jest kodem przekierowania: informuje przeglądarkę klienta, że żądana strona URL została przeniesiona. Nowy adres URL jest określony jako [9]. Przeglądarka wykona to przekierowanie, wysyłając nowe żądanie o adresie GET:

Image

  • do [12-13], nowego żądania wysłanego przez przeglądarkę;

Wypełnijmy otrzymany formularz;

Image

Przeprowadźmy zatem kilka symulacji:

Image

Image

Poprośmy o listę symulacji:

Image

Usuńmy pierwszą symulację:

Image

Zakończmy sesję:

Image

Zachęcamy czytelnika do przeprowadzenia dalszych testów.

23.14. Klient serwisu internetowego jSON

23.14.1. Architektura klient-serwer

Image

Skupiamy się teraz na kliencie jSON [A] usługi internetowej [B]. Klient [A], podobnie jak usługa internetowa [B], ma strukturę warstwową:

Image

Architektura ta znajduje odzwierciedlenie w następującej organizacji kodu:

Image

Większość klas została już omówiona:

BaseEntity
akapit z linkiem.
TaxPayerData
akapit z linkiem.
Simulation
akapit z linkiem.
ExceptionImpots
akapit z linkiem.
TraitDao
akapit z linkiem.
Utilitaires
akapit z linkiem.

23.14.2. Warstwa [dao]

Image

23.14.2.1. Interfejs

Interfejs warstwy [dao] będzie wyglądał następująco: [InterfaceClientDao.php]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceClientDao {

  // odczyt danych podatnika
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // obliczanie podatków podatnika
  public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;

  // zapisywanie wyników
  public function saveResults(string $resultsFilename, array $simulations): void;

  // uwierzytelnianie
  public function authentifierUtilisateur(String $user, string $password): void;

  // lista symulacji
  public function listerSimulations(): array;

  // usunięcie symulacji
  public function supprimerSimulation(int $numéro): array;

  // rozpoczęcie sesji
  public function initSession(string $type = 'json'): void;

  // zakończenie sesji
  public function finSession(): void;
}

Uwagi

  • wiersz 9: metoda [getTaxPayersData] umożliwia przetwarzanie pliku jSON zawierającego dane podatników. Metoda ta jest zaimplementowana przez funkcję [TraitDao], omówioną już wcześniej (akapit dotyczący linku);
  • wiersz 15: metoda [saveResults] umożliwia zapisanie wyników kilku obliczeń podatku w pliku jSON. Również w tym przypadku metoda ta jest zaimplementowana przez cechę [TraitDao], omówioną już wcześniej (akapit z linkiem);
  • wiersze 12, 18, 21, 27, 30: utworzono metodę dla każdej z akcji akceptowanych przez serwis internetowy;

23.14.2.2. Implementacja

Interfejs [InterfaceClientDao] jest zaimplementowany przez następującą klasę [ClientDao]:


<?php

namespace Application;

// zależności
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;

class ClientDao implements InterfaceClientDao {
  // użycie cechy
  use TraitDao;
  // atrybuty
  private $urlServer;
  private $sessionCookie;
  private $verbose;

  // konstruktor
  public function __construct(string $urlServer, bool $verbose = TRUE) {
    $this->urlServer = $urlServer;
    $this->verbose = $verbose;
  }

}

Komentarze

  • wiersze 18–21: konstruktor otrzymuje dwa parametry:
    • URL [$urlServer] usługi internetowej jSON;
    • wartość logiczną [$verbose], która w przypadku wartości TRUE oznacza, że klasa ma wyświetlać odpowiedzi serwera na konsoli;
  • wiersz 14: plik cookie sesji. Jego rola została opisana w wersji 09 klienta (akapit „link”);
  • wiersz 11: klasa wykorzystuje cechę [TraitDao], która implementuje dwie metody interfejsu:
    • [getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array];
    • [function calculerImpot(string $marié, int $enfants, int $salaire): Simulation];

23.14.2.2.1. Metoda [initSession]

Metoda [initSession] jest zaimplementowana w następujący sposób:


public function initSession(string $type = 'json'): void {
    // tworzymy klienta HTTP
    $httpClient = HttpClient::create();
    // wysyłamy żądanie do serwera bez uwierzytelniania
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "init-session",
          "type" => $type
        ],
        "verify_peer" => false
    ]);
    // odbieramy odpowiedź
    $this->getResponse($response);
    // pobieramy plik cookie sesji
    $headers = $response->getHeaders();
    if (isset($headers["set-cookie"])) {
      // plik cookie sesji?
      foreach ($headers["set-cookie"] as $cookie) {
        $match = [];
        $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
        if ($match) {
          $this->sessionCookie = "PHPSESSID=" . $champs[1];
        }
      }
    }
  }

Ponieważ akcja [init-session] ma być pierwszą akcją żądaną od serwisu internetowego, metoda [initSession] będzie pierwszą metodą warstwy [dao], która zostanie wywołana.

Komentarze

  • wiersz 1: jako parametr przekazywany jest żądany typ sesji. W przypadku braku parametru uruchomiona zostanie sesja typu jSON;
  • wiersze 5–11: wysyłane jest żądanie GET do serwisu internetowego;
  • wiersze 7–8: dwa parametry GET;
  • wiersz 10: w przypadku bezpiecznej komunikacji (schemat https) certyfikat bezpieczeństwa przesłany przez serwis internetowy nie zostanie zweryfikowany;
  • wiersz 13: metoda [getResponse] pobiera odpowiedź z serwera. Zwraca ją w postaci tablicy. W tym przypadku wynik działania metody nie jest wykorzystywany. Metoda [getResponse] zgłasza wyjątek, jeśli kod HTTP w odpowiedzi serwisu internetowego jest inny niż 200 OK;
  • wiersze 14–25: ponieważ metoda [initSession] jest pierwszą metodą warstwy [dao], która zostanie wykonana, pobierany jest plik cookie sesji, aby kolejne metody mogły go odesłać do serwisu internetowego. Kod ten został już skomentowany w wersji 09;

23.14.2.2.2. Metoda [getResponse]

Metoda [getResponse] odpowiada za przetworzenie odpowiedzi serwisu internetowego:


private function getResponse(CurlResponse $response) {
    // odbieramy odpowiedź
    $json = $response->getContent(false);
    // logi
    if ($this->verbose) {
      print "$json\n";
    }
    // pobieramy status odpowiedzi
    $statusCode = $response->getStatusCode();
    // błąd?
    if ($statusCode !== 200) {
      // wystąpił błąd
      throw new ExceptionImpots($json);
    }
    // zwracamy odpowiedź
    $array = json_decode($json, true);
    return $array["réponse"];
  }

Komentarze

  • wiersz 1: metoda jest prywatna;
  • wiersz 1: parametrem metody jest odpowiedź serwisu internetowego typu [Symfony\Component\HttpClient\Response\CurlResponse], czyli typ odpowiedzi Symfony, gdy [HttpClient] jest zaimplementowany przez [CurlClient], tj. przez bibliotekę [curl];
  • wiersz 3: pobieramy odpowiedź jSON z serwera. Przypominamy, że parametr [false] ma na celu zapobieganie wygenerowaniu wyjątku przez Symfony, gdy status odpowiedzi serwera HTTP znajduje się w zakresie [3xx, 4xx, 5xx];
  • wiersze 5–7: jeśli jesteśmy w trybie [$verbose], wyświetlamy odpowiedź serwera na konsoli;
  • wiersze 9–14: jeśli status odpowiedzi serwera HTTP jest inny niż 200, to generowany jest wyjątek z komunikatem o błędzie będącym odpowiedzią serwera jSON;
  • wiersz 16: ciąg znaków jSON jest dekodowany do tablicy;
  • wiersz 17: przydatne informacje znajdują się w [$array["réponse"]];

23.14.2.2.3. Metoda [authentifierUtilisateur]

Metoda [authentifierUtilisateur] wygląda następująco:


public function authentifierUtilisateur(string $user, string $password): void {
    // tworzymy klienta HTTP
    $httpClient = HttpClient::create();
    // wysyłamy żądanie do serwera z uwierzytelnieniem
    $response = $httpClient->request('POST', $this->urlServer,
      ["query" => [
          "action" => "authentifier-utilisateur"
        ],
        "body" => [
          "user" => $user,
          "password" => $password
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // odbieramy odpowiedź
    $this->getResponse($response);
  }

Uwagi

  • wiersz 5: żądanie klienta to POST;
  • wiersze 6–8: parametry w pliku URL;
  • wiersze 9–12: parametry w pliku POST;
  • wiersz 14: plik cookie sesji;
  • wiersz 17: odczytujemy odpowiedź. Wiemy, że w przypadku błędu (kod HTTP inny niż 200) metoda [getResponse] sama zgłasza wyjątek;

23.14.2.2.4. Metoda [calculerImpot]

public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
    // tworzy się klienta HTTP
    $httpClient = HttpClient::create();
    // wysyłamy żądanie do serwera bez uwierzytelniania, ale z plikiem cookie sesji
    $response = $httpClient->request('POST', $this->urlServer,
      ["query" => [
          "action" => "calculer-impot"],
        "body" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // odbieramy odpowiedź
    $array = $this->getResponse($response);
    return (new Simulation())->setFromArrayOfAttributes($array);
  }

Komentarze

  • wiersze 6–7: jedyny parametr metody URL;
  • wiersze 8–12: trzy parametry metody POST (wiersz 5);
  • wiersz 17: odpowiedź jest przetwarzana;
  • wiersz 18: jeśli dotarliśmy do tego miejsca, oznacza to, że metoda [getResponse] nie wygenerowała wyjątku. Zwracamy obiekt [Simulation] zainicjowany tablicą zwróconą przez [getResponse];

23.14.2.2.5. Metoda [listerSimulations]

public function listerSimulations(): array {
    // tworzymy klienta HTTP
    $httpClient = HttpClient::create();
    // wysyłamy żądanie do serwera bez uwierzytelniania, ale z plikiem cookie sesji
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "lister-simulations"
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // odbieramy odpowiedź
    return $this->getSimulations($response);
  }

Komentarze

  • wiersz 5: metoda GET;
  • wiersze 6–8: jedyny parametr metody GET;
  • wiersz 13: pobieranie symulacji jest powierzone prywatnej metodzie [getSimulations];

23.14.2.2.6. Metoda [getSimulations]

private function getSimulations(CurlResponse $response): array {
    // pobieramy odpowiedź JSON
    $array = $this->getResponse($response);
    // mamy tablicę obiektów asocjacyjnych
    // utworzymy z tego tablicę obiektów Simulation
    $simulations = [];
    foreach ($array as $simulation) {
      $simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
    }
    // zwracamy listę obiektów Simulation
    return $simulations;
}

Komentarze

  • wiersz 3: pobieramy tablicę z odpowiedzi. Jest to tablica tablic, z których każda posiada wszystkie atrybuty obiektu [Simulation];
  • wiersz 6: jeśli dotarliśmy do tego miejsca, oznacza to, że metoda [getResponse] nie wygenerowała wyjątku;
  • wiersze 6–9: wykorzystujemy odpowiedź do utworzenia tablicy obiektów [Simulation];
  • wiersz 11: zwracamy tę tablicę;

23.14.2.2.7. Metoda [SupprimerSimulation]

public function supprimerSimulation(int $numéro): array {
    // tworzymy klienta HTTP
    $httpClient = HttpClient::create();
    // wysyłamy żądanie do serwera bez uwierzytelniania, ale z plikiem cookie sesji
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "supprimer-simulation",
          "numéro" => $numéro
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // pobieramy odpowiedź
    return $this->getSimulations($response);
  }

Komentarze

  • wiersz 5: wysyłamy zapytanie GET;
  • wiersze 6–9: dwa parametry metody URL;
  • wiersz 14: po usunięciu serwer zwraca nową tablicę symulacji. Zwracamy tę tablicę;

23.14.2.2.8. Metoda [finSession]

Sesja pracy z serwisem internetowym kończy się zazwyczaj wywołaniem metody [finSession]:


public function finSession(): void {
    // tworzymy klienta HTTP
    $httpClient = HttpClient::create();
    // wysyłamy żądanie do serwera bez uwierzytelniania, ale z plikiem cookie sesji
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "fin-session"
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // odbieramy odpowiedź
    $this->getResponse($response);
  }

Komentarze

  • wiersz 5: wysyłamy żądanie GET;
  • wiersze 6–8: jedyny parametr metody URL;
  • wiersz 13: odczytujemy odpowiedź. Jeśli kod HTTP w odpowiedzi różni się od 200, zostanie zgłoszony wyjątek;

23.14.3. Warstwa [métier]

Image

23.14.3.1. Interfejs

Interfejs warstwy [métier] wygląda następująco: [InterfaceClientMetier.php]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceClientMetier {

  // obliczanie podatków podatnika
  public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;

  // obliczanie podatków w trybie wsadowym
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;

  // uwierzytelnianie
  public function authentifierUtilisateur(String $user, string $password): void;

  // lista symulacji
  public function listerSimulations(): array;

  // zapisywanie wyników
  public function saveResults(string $resultsFilename, array $simulations): void;

  // usunięcie symulacji
  public function supprimerSimulation(int $numéro): array;

  // rozpoczęcie sesji
  public function initSession(string $type = 'json'): void;

  // koniec sesji
  public function finSession(): void;
}

Uwagi

  • tylko metoda [executeBatchImpots] z wiersza 12 jest specyficzna dla warstwy [métier]. Wszystkie pozostałe należą do warstwy [dao], która je implementuje;

23.14.3.2. Klasa [ClientMetier]

Klasa implementująca warstwę [métier] jest następująca:


<?php

namespace Application;

class ClientMetier implements InterfaceClientMetier {
  // atrybut
  private $clientDao;

  // producent
  public function __construct(InterfaceClientDao $clientDao) {
    $this->clientDao = $clientDao;
  }

  // obliczenie podatku
  public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }

  // obliczanie podatków w trybie wsadowym
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // pozwala na przekazywanie wyjątków pochodzących z warstwy [dao]
    // pobieranie danych podatników
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela wyników
    $simulations = [];
    // przetwarzamy je
    foreach ($taxPayersData as $taxPayerData) {
      // obliczamy podatek     
      $simulations [] = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
    }
    // zapis wyników
    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);
  }

}

Komentarze

  • wiersze 10–12: aby się skompilować, warstwa [métier] wymaga odwołania do warstwy [dao];
  • wiersze 20–38: tylko metoda [executeBatchImpots] jest specyficzna dla warstwy [métier]. Implementacja pozostałych metod przekazuje zadania do wykonania metodom o tych samych nazwach w warstwie [dao];
  • wiersz 23: zwracamy się do warstwy [dao], aby uzyskać dane podatników w tablicy obiektów typu [TaxPayerData];
  • wiersz 25: w tablicy [$simulations] sumowane są wyniki poszczególnych obliczeń symulacyjnych;
  • wiersze 27–33: oblicza się podatek dla każdego z podatników z tabeli [$taxPayersData];
  • wiersze 35–37: wyniki uzyskane w tabeli [$simulations] są zapisywane w pliku jSON;

Uwaga: Warstwa [métier] praktycznie nic nie robi. Można by ją usunąć i zebrać wszystko w warstwie [dao].

23.14.4. Skrypt główny

Image

Skrypt główny jest skonfigurowany przez następujący plik [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"
}

Główny skrypt [main.php] wygląda następująco:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare(strict_types = 1);

// przestrzeń nazw
namespace Application;

// obsługa błędów przez PHP
// ini_set("display_errors", "0");
//
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", "../Data/config.json");

// pobieramy konfigurację
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// dołączamy zależności niezbędne dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// definicja stałych
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// zależności Symfony
use Symfony\Component\HttpClient\HttpClient;

// tworzenie warstwy [dao]
$clientDao = new ClientDao($config["urlServer"]);
// tworzenie warstwy [métier]
$clientMetier = new ClientMetier($clientDao);

// obliczanie podatków w trybie wsadowym
try {
  // inicjalizacja sesji
  $clientMetier->initSession('json');
  // uwierzytelnianie
  $clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
  // obliczanie podatków bez zapisywania wyników
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
  // lista symulacji
  $clientMetier->listerSimulations();
  // usunięcie symulacji
  $simulations = $clientMetier->supprimerSimulation(1);
  // zapisywanie wyników
  $clientMetier->saveResults(RESULTS_FILENAME, $simulations);
  // zakończenie sesji
  $clientMetier->finSession();
  // działanie bez uwierzytelnienia – powinno spowodować awarię
  $clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
  // wyświetlenie komunikatu o błędzie
  print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// koniec
print "Terminé\n";
exit();

Komentarze

  • wiersze 12–16: wykorzystanie pliku konfiguracyjnego [config.json];
  • wiersze 18–26: ładowanie wszystkich zależności;
  • wiersze 28–34: definiowanie stałych i aliasów;
  • wiersze 36–39: tworzenie warstw [dao] i [métier];
  • wiersz 44: inicjalizacja sesji jSON;
  • wiersz 46: uwierzytelnianie się na serwerze;
  • wiersz 48: obliczenie podatku dla grupy podatników. Wyniki nie są zapisywane (2. parametr NULL);
  • wiersz 50: żądanie wyników wszystkich tych obliczeń;
  • wiersz 52: usuwa się symulację nr 1 (drugą na liście);
  • wiersz 54: zapisuje się pozostałe symulacje;
  • wiersz 56: kończy się sesję. Oznacza to, że plik cookie sesji zostaje usunięty;
  • wiersz 58: wywołuje listę symulacji. Ponieważ plik cookie sesji został usunięty, należy ponownie przeprowadzić uwierzytelnianie. Powinien więc pojawić się komunikat o błędzie informujący, że użytkownik nie jest uwierzytelniony;

Plik [taxpayersdata.json] ma następującą treść:


[
    {
        "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
    }
]

Jest 12 podatników, z których 1 jest nieprawidłowy. Łącznie daje to 11 symulacji. Jedna z nich zostanie usunięta. Powinno pozostać 10.

Po uruchomieniu głównego skryptu plik jSON [results.json] wygląda następująco:


[
    {
        "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
    }
]

Jest tu rzeczywiście 10 symulacji.

Plik jSON [errors.json] ma następującą zawartość:


{
    "numéro": 1,
    "erreurs": [
        {
            "marié": "ouix"
        },
        {
            "enfants": "2x"
        },
        {
            "salaire": "55555x"
        }
    ]
}

Wyniki wyświetlane na konsoli są następujące (w trybie szczegółowym odpowiedzi serwera o numerze jSON są wyświetlane na konsoli):


{"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. Testy [Codeception]

Podobnie jak w przypadku poprzednich klientów, klient w wersji 12 może zostać poddany testom [Codeception]:

Image

Kod klasy testowej warstwy [métier] klienta jest analogiczny do kodu klas testowych poprzednich klientów:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// definicja stałych
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12");
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config.json");

// pobieranie konfiguracji
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);

// dołączanie zależności niezbędnych dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
// zależności Symfony
use Symfony\Component\HttpClient\HttpClient;

// klasa testowa
class ClientDaoTest extends \Codeception\Test\Unit {
  // warstwa DAO
  private $clientDao;

  public function __construct() {
    parent::__construct();
    // pobieranie konfiguracji
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // tworzenie warstwy [dao]
    $clientDao = new ClientDao($config["urlServer"]);
    // tworzenie warstwy [métier]
    $this->métier = new ClientMetier($clientDao);
    // inicjalizacja sesji
    $this->métier->initSession("json");
    // uwierzytelnianie
    $this->métier->authentifierUtilisateur("admin", "admin");
  }

  // testy
  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() {

  }

}

Uwagi

  • wiersze 34–46: przypominamy, że konstruktor klasy testowej jest uruchamiany przed każdym testem;
  • wiersze 38–41: tworzenie warstw [dao] i [métier];
  • wiersze 42–45: metody testowe [test1…, test11] testują metodę [calculerImpot]. Aby było to możliwe, należy wcześniej zainicjować sesję jSON i uwierzytelnić się;

Wyniki testu są następujące:

Image

Należy przeprowadzić wiele innych testów:

  • przetestować różne metody warstwy [dao];
  • przetestować statusy zwracane przez serwer WWW. Statusy te są istotne, ponieważ ich wartość decyduje o tym, która strona HTML ma zostać wyświetlona;