14. Ćwiczenie praktyczne – wersja 6

Właśnie zaimplementowaliśmy następującą strukturę warstwową:

SGBD używany w przykładach to MySQL. W akapicie dotyczącym linków zauważyliśmy, że nic w klasie implementującej warstwę [dao] nie sugerowało, że używamy konkretnego obiektu SGBD. Teraz to sprawdzimy, wykorzystując inny obiekt SGBD, a mianowicie SGBD i PostgreSQL. Architektura warstwowa wygląda następująco:

14.1. Instalacja plików SGBD i PostgreSQL
Dystrybucje SGBD i PostgreSQL są dostępne w URL i [https://www.postgresql.org/download/] (maj 2019 r.). Poniżej przedstawiamy instalację wersji dla systemu Windows 64-bitowego:


- w [1-4] należy pobrać instalator z pliku SGBD;
Uruchamiamy pobrany instalator:

- w [6] należy wskazać folder instalacyjny;

- w [8] opcja [Stack Builder] nie jest potrzebna do tego, co chcemy tutaj zrobić;
- w [10] pozostaw domyślną wartość;

- w polu [12-13] wprowadzono hasło [root]. Będzie to hasło administratora SGBD, który nazywa się [postgres]. PostgreSQL nazywa go również superużytkownikiem;
- w polu [15] należy pozostawić wartość domyślną: jest to port nasłuchowy serwera SGBD;

- w polu [17] pozostaw wartość domyślną;
- w [19] – podsumowanie konfiguracji instalacji;


W systemie Windows usługa SGBD PostgreSQL jest zainstalowana jako uruchamiana automatycznie usługa systemu Windows. W większości przypadków nie jest to pożądane. Zmienimy tę konfigurację. Wpisz [services] w pasku wyszukiwania systemu Windows [24-26]:

- na [29]. Widzimy, że usługa SGBD PostgreSQL działa w trybie automatycznym. Zmieniamy to, przechodząc do właściwości usługi [30]:

- w przypadku usługi [31-32] należy ustawić uruchamianie w trybie ręcznym;
- w przypadku usługi [33] należy zatrzymać usługę;
Gdy zechcesz ręcznie uruchomić usługę SGBD, wróć do aplikacji [services], kliknij prawym przyciskiem myszy usługę [postgresql] (34) i uruchom ją (35).
14.2. Aktywacja rozszerzenia PDO dla SGBD i PostgreSQL
Zmodyfikujemy plik [php.ini], który konfiguruje PHP (patrz akapit dotyczący linku):

- na [2], upewnij się, że rozszerzenie PDO dla pliku PostgreSQL jest włączone. Po wykonaniu tej czynności zapisz zmianę, a następnie uruchom ponownie Laragon, aby mieć pewność, że zmiana zostanie uwzględniona. Następnie sprawdź konfigurację PHP bezpośrednio z poziomu Laragon [3-5].
14.3. Zarządzanie PostgreSQL za pomocą narzędzia [pgAdmin]
Uruchom usługę systemu Windows dla SGBD PostgreSQL (patrz akapit „link”). Następnie, w taki sam sposób, w jaki uruchomiłeś narzędzie [services], uruchom narzędzie [pgadmin], które umożliwia zarządzanie narzędziami SGBD, PostgreSQL i [1-3]:

Możliwe, że w pewnym momencie pojawi się prośba o podanie hasła superużytkownika. Nazywa się ono [postgres]. Hasło to zostało przez Państwa ustawione podczas instalacji programu SGBD. W niniejszym dokumencie podczas instalacji nadaliśmy superużytkownikowi hasło [root].
- w przypadku [4], [pgAdmin] jest aplikacją internetową;
- w [5] znajduje się lista serwerów PostgreSQL wykrytych przez [pgAdmin], w tym przypadku 1;
- w [6] – serwer PostgreSQL, który uruchomiliśmy;
- w [7] – bazy danych serwera SGBD, w tym przypadku 1;
- w [8] baza [postgresql] jest zarządzana przez superużytkownika [postgres];
Najpierw utwórzmy użytkownika o nazwie [admimpots] z hasłem [mdpimpots]:


- w [17] wpisaliśmy [mdpimpots];

- w [21] kod SQL, który narzędzie [pgAdmin] wygeneruje do SGBD PostgreSQL. Jest to sposób na naukę języka SQL, będącego własnością PostgreSQL;
- w [22], po zatwierdzeniu przez kreatora [Save], utworzono użytkownika [admimpots];
Teraz tworzymy bazę danych o nazwie [dbimpots-2019]:

Klikamy prawym przyciskiem myszy na [23], a następnie na [24-25], aby utworzyć nową bazę danych. W zakładce [26] definiujemy nazwę bazy danych [27] oraz jej właściciela [admimpots] [28].

- w [30] kod SQL tworzenia bazy;
- na [31], po zatwierdzeniu przez kreatora [Save], baza [dbimpots-2019] zostaje utworzona;
Teraz utworzymy tabelę [tbtranches] z kolumnami [id, limites, coeffr, coeffn]. Cechą charakterystyczną tabeli PostgreSQL jest to, że nazwy kolumn rozróżniają wielkość liter (wielkie/małe), co zazwyczaj nie ma miejsca w przypadku innych tabel typu SGBD. Tak więc w przypadku pliku MySQL polecenie [select limites, coeffR, coeffN from tbtranches] zadziała, nawet jeśli rzeczywiste kolumny tabeli [tbtranches] to [LIMITES, COEFFR, COEFFN]. W przypadku PostgreSQL polecenie SQL nie zadziała. Można by wtedy wpisać [select LIMITES, COEFFR, COEFFN from tbtranches], ale to nadal nie zadziała, ponieważ PostgreSQL wykona polecenie [select limites, coeffr, coeffn from tbtranches]: domyślnie przekazuje nazwy kolumn pisane małymi literami. Aby tego uniknąć, należy wpisać: [select "LIMITES", "COEFFR", "COEFFN" from tbtranches], tzn. nazwy kolumn należy ująć w cudzysłowy. Z tych powodów nadamy kolumnom nazwy pisane małymi literami. Nazwy obiektów w bazie danych mogą być źródłem niezgodności między SGBD, ponieważ niektóre nazwy są słowami zastrzeżonymi w niektórych SGBD, a w innych nie.
Tworzymy tabelę [tbtranches]:

- użyj przycisku [40], aby utworzyć kolumny;


- po zakończeniu pracy kreatora za pomocą [Save] tworzona jest tabela [tbtranches] [52-53];
Musimy poinformować tabelę SGBD, że ma ona sama generować klucz główny [id] podczas wstawiania wiersza do tabeli:

- w [56] uzyskujemy dostęp do właściwości klucza głównego [id];
- w polu [59] określa się, że kolumna ma typ [Identity]. Spowoduje to, że SGBD wygeneruje wartości klucza głównego;

- w [62] – kod SQL wygenerowany dla tej operacji;
Tabela [tbtranches] jest teraz gotowa.
Powtarzamy te same operacje, aby utworzyć tabelę [tbconstantes]. Podajemy wynik, który ma zostać uzyskany:



Baza danych [dbimpots-2019] jest już gotowa. Teraz wypełnimy ją danymi.
Podobnie jak w przypadku bazy MySQL, można wyeksportować bazę danych [dbimpots-2019] do pliku SQL. Następnie można zaimportować ten plik SQL, aby odtworzyć bazę danych w przypadku jej utraty lub uszkodzenia. W tym przypadku wyeksportujemy jedynie strukturę bazy danych, a nie jej dane:


Wygenerowany plik wygląda następująco:
--
-- zrzut bazy danych PostgreSQL
--
-- Zrzut z bazy danych w wersji 11.2
-- Zrzut wykonany przez pg_dump w wersji 11.2
-- Rozpoczęto 2019-07-04 o godz. 08:20:31
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Wpis TOC nr 198 (klasa 1259 OID 16408)
-- Nazwa: tbconstantes; Typ: TABLE; Schemat: public; Właściciel: postgres
--
CREATE TABLE public.tbconstantes (
plafond_qf_demi_part double precision NOT NULL,
id integer NOT NULL,
plafond_revenus_celibataire_pour_reduction double precision NOT NULL,
plafond_revenus_couple_pour_reduction double precision NOT NULL,
valeur_reduc_demi_part double precision NOT NULL,
plafond_decote_celibataire double precision NOT NULL,
plafond_decote_couple double precision NOT NULL,
plafond_impot_celibataire_pour_decote double precision NOT NULL,
plafond_impot_couple_pour_decote double precision NOT NULL,
abattement_dix_pourcent_max double precision NOT NULL,
abattement_dix_pourcent_min double precision NOT NULL
);
ALTER TABLE public.tbconstantes OWNER TO postgres;
--
-- TOC wpis 199 (klasa 1259 OID 16411)
-- Nazwa: tbconstantes_id_seq; Typ: SEQUENCE; Schemat: public; Właściciel: postgres
--
ALTER TABLE public.tbconstantes ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME public.tbconstantes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);
--
-- TOC wpis 196 (klasa 1259 OID 16399)
-- Nazwa: tbtranches; Typ: TABLE; Schemat: public; Właściciel: admimpots
--
CREATE TABLE public.tbtranches (
limites double precision NOT NULL,
id integer NOT NULL,
coeffr double precision NOT NULL,
coeffn double precision NOT NULL
);
ALTER TABLE public.tbtranches OWNER TO admimpots;
--
-- TOC wpis 197 (klasa 1259 OID 16404)
-- Nazwa: tbimpots_id_seq; Typ: SEQUENCE; Schemat: public; Właściciel: admimpots
--
ALTER TABLE public.tbtranches ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME public.tbimpots_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);
--
-- TOC wpis 2694 (klasa 2606 OID 16429)
-- Nazwa: tbconstantes tbconstantes_pkey; Typ: CONSTRAINT; Schemat: public; Właściciel: postgres
--
ALTER TABLE ONLY public.tbconstantes
ADD CONSTRAINT tbconstantes_pkey PRIMARY KEY (id);
--
-- TOC wpis 2692 (klasa 2606 OID 16403)
-- Nazwa: tbtranches tbimpots_pkey; Typ: CONSTRAINT; Schemat: public; Właściciel: admimpots
--
ALTER TABLE ONLY public.tbtranches
ADD CONSTRAINT tbimpots_pkey PRIMARY KEY (id);
--
-- TOC wpis 2821 (klasa 0 OID 0)
-- Zależności: 198
-- Nazwa: TABLE tbconstantes; Typ: ACL; Schemat: public; Właściciel: postgres
--
GRANT ALL ON TABLE public.tbconstantes TO admimpots;
-- Zakończono 2019-07-04 o godz. 08:20:32
--
-- Zrzut bazy danych PostgreSQL zakończony
--
14.4. Wypełnianie tabeli [tbtranches]
Wykonaliśmy już tę pracę w przypadku plików SGBD i MySQL w sekcji „link”. Wystarczy, że zmodyfikujemy plik [database.json], który opisuje bazę danych:

Plik [database.json] przyjmuje następującą postać:
{
"dsn": "pgsql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "public.tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "public.tbconstantes",
"colPlafondQfDemiPart": "plafond_qf_demi_part",
"colPlafondRevenusCelibatairePourReduction": "plafond_revenus_celibataire_pour_reduction",
"colPlafondRevenusCouplePourReduction": "plafond_revenus_couple_pour_reduction",
"colValeurReducDemiPart": "valeur_reduc_demi_part",
"colPlafondDecoteCelibataire": "plafond_decote_celibataire",
"colPlafondDecoteCouple": "plafond_decote_couple",
"colPlafondImpotCelibatairePourDecote": "plafond_impot_celibataire_pour_decote",
"colPlafondImpotCouplePourDecote": "plafond_impot_couple_pour_decote",
"colAbattementDixPourcentMax": "abattement_dix_pourcent_max",
"colAbattementDixPourcentMin": "abattement_dix_pourcent_min"
}
- wiersz 2: plik DSN uległ zmianie, a [pgsql] wskazuje, że mamy do czynienia z plikiem SGBD dla Postgres;
- wiersze 5 i 9: przed nazwami tabel umieszczono nazwę schematu, do którego należą, tj. [public]. Nie było to konieczne, ponieważ [public] jest schematem domyślnym, gdy w nazwie tabeli nie podano żadnego schematu;
- wiersze 6–8, 10–19: zmieniono nazwy kolumn;
Skrypt [MainTransferAdminDataFromJsonFile2PostgresDatabase.php] służący do wypełnienia bazy danych [dbimpots-2019] ma następującą postać:
<?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”);
// dołączenie interfejsu i klas
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../../version-05/Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// definicja stałych
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
//
try {
// utworzenie warstwy [dao]
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// przeniesienie danych do bazy
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// wyświetlanie błędu
print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// koniec
print "Terminé\n";
exit;
Komentarze
Zmieniają się jedynie wiersze 12–21, które ładują pliki niezbędne do działania aplikacji. Zmieniają się one, ponieważ zmienia się wartość [__DIR__]: obecnie wskazuje ona na folder [version-07/Main].
Po uruchomieniu tego skryptu w tabeli [tbtranches] otrzymujemy następujący wynik:

- klikamy prawym przyciskiem myszy na [1], a następnie na [2-3];
- w tabeli [4] widoczne są dane dotyczące przedziałów podatkowych;
Powtarzamy tę samą procedurę dla tabeli stałych [tbconstantes]:



Należy zauważyć, że do wykonania skryptu aplikacja Laragon nie musi być uruchomiona: nie potrzebujemy ani serwera Apache, ani SGBD ani MySQL. Wymagane są jedynie pliki SGBD i PostgreSQL, dla których uruchomiono usługę systemu Windows.
14.5. Obliczanie podatku

Warstwy [dao] (3) i [métier] (2) zostały już napisane. Główny skrypt dla SGBD i MySQL napisaliśmy już w akapicie „link”. Wystarczy, że wykorzystamy skrypt [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php] i dostosujemy go do plików SGBD oraz PostgreSQL. Nazywa się on teraz [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]:

Skrypt [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.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");
// dołączanie interfejsów i klas
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../../version-05/Métier/InterfaceMetier.php";
require_once __DIR__ . "/../../version-05/Métier/Metier.php";
//
// definicja stałych
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";
try {
// utworzenie warstwy [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// utworzenie warstwy [métier]
$métier = new Metier($dao);
// obliczanie podatku w trybie wsadowym
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// wyświetlenie błędu
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// koniec
print "Terminé\n";
exit;
Komentarze
Zmieniają się jedynie wiersze 12–22, które ładują pliki niezbędne do działania aplikacji. Zmieniają się one, ponieważ zmienia się wartość [__DIR__]: wskazuje ona teraz na folder [version-07/Main].
Wyniki wykonania
Takie same jak w poprzednich wersjach.
14.6. Testy [Codeception]
Podobnie jak w przypadku poprzednich wersji, weryfikujemy tę wersję za pomocą testów [Codeception]:

14.6.1. Test warstwy [dao]
Test [DaoTest.php] wygląda następująco:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Application;
// katalogi główne
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// dołączanie interfejsów i klas
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
// biblioteki zewnętrzne
require_once VENDOR . "/autoload.php";
// definicja stałych
const DATABASE_CONFIG_FILENAME = ROOT ."../Data/database.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// tworzenie warstwy [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// testy
public function testTaxAdminData() {
…
}
}
Komentarze
- wiersze 9–28: definicja środowiska testowego. Używamy tego samego środowiska, bez warstwy [métier], co w głównym skrypcie [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] opisanym w akapicie „link”;
- wiersze 34–39: tworzenie warstwy [dao];
- wiersz 38: atrybut [$this→taxAdminData] zawiera dane do przetestowania;
- wiersze 42–44: metoda [testTaxAdminData] jest tą opisaną w akapicie „link”;
Wyniki testu są następujące:

14.6.2. Test warstwy [métier]
Test [MetierTest.php] wygląda następująco:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Application;
// katalogi główne
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// dołączanie interfejsów i klas
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/../version-05/Métier/InterfaceMetier.php";
require_once ROOT . "/../version-05/Métier/Metier.php";
// biblioteki zewnętrzne
require_once VENDOR . "/autoload.php";
// definicja stałych
const DATABASE_CONFIG_FILENAME = ROOT . "../Data/database.json";
class MetierTest extends \Codeception\Test\Unit {
// warstwa biznesowa
private $métier;
public function __construct() {
parent::__construct();
// tworzenie warstwy [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// tworzenie warstwy [métier]
$this->métier = new Metier($dao);
}
// testy
public function test1() {
…
}
--------------------------------------------------------------------
public function test11() {
…
}
}
Komentarze
- wiersze 9–28: definicja środowiska testowego. Wykorzystujemy to samo środowisko, co w głównym skrypcie [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] opisanym w akapicie „link”;
- wiersze 34–40: tworzenie warstw [dao] i [métier];
- wiersz 39: atrybut [$this→métier] odwołuje się do warstwy [métier]
- wiersze 43–49: metody [test1, test2…, test11] są opisane w akapicie „link”;
Wyniki testu są następujące:
