3. Kontroler generyczny
3.1. Wprowadzenie
W poprzedniej metodzie zakładaliśmy, że musimy napisać kontroler o nazwie main.php. Przy odrobinie doświadczenia zdajemy sobie sprawę, że kontroler ten często wykonuje te same czynności, dlatego kuszące jest napisanie kontrolera generycznego, który można by wykorzystać w większości aplikacji internetowych. Kod tego kontrolera mógłby wyglądać następująco:
<?php
// kontroler ogólny
// odczyt konfiguracji
include 'config.php';
// dołączanie bibliotek
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
// rozpoczynamy lub wznawiamy sesję
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
// pobieranie akcji do wykonania
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
$sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";
// czy sekwencja działań przebiega prawidłowo?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// nienormalna sekwencja
$sAction='enchainementinvalide';
}//if
// przetwarzanie akcji
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
// wysłanie odpowiedzi (widoku) do klienta
$sEtat=$dSession['etat']['principal'];
$scriptVue=$dConfig['etats'][$sEtat]['vue'];
include $scriptVue;
// koniec skryptu – nie powinno się tu dotrzeć, chyba że występuje błąd
trace ("Erreur de configuration.");
trace("Action=[$sAction]");
trace("scriptAction=[$scriptAction]");
trace("Etat=[$sEtat]");
trace("scriptVue=[$scriptVue]");
trace ("Vérifiez que les script existent et que le script [$scriptVue] se termine par l'appel à finSession.");
exit(0);
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: słownik konfiguracyjny
// $dSession: słownik zawierający informacje o sesji
// $dReponse: słownik argumentów strony odpowiedzi
// zapis sesji
if(isset($dSession)){
// parametry zapytania umieszczamy w sesji
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// brak sesji
session_destroy();
}
// wyświetla się odpowiedź
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// koniec skryptu
exit(0);
}//zakończenie sesji
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// sprawdza, czy bieżąca akcja jest dozwolona w odniesieniu do poprzedniego stanu
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// weryfikacja akcji
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
//--------------------------------------------------------------------
function dump($dInfos){
// wyświetla słownik informacji
while(list($clé,$valeur)=each($dInfos)){
echo "[$clé,$valeur]<br>\n";
}//while
}//monitorowanie
//--------------------------------------------------------------------
function trace($msg){
echo $msg."<br>\n";
}//monitorowanie
?>
3.2. Plik konfiguracyjny aplikacji
Aplikacja jest konfigurowana w skrypcie, który musi nosić nazwę config.php. Parametry aplikacji są umieszczane w słowniku o nazwie $dConfig, z którego korzystają zarówno kontroler, jak i skrypty akcji, modele oraz widoki elementarne.
3.3. Biblioteki, które należy dołączyć w kontrolerze
Biblioteki, które należy dołączyć do kodu kontrolera, znajdują się w tabeli $dConfig['includes']. Kontroler dołącza je za pomocą następującej sekwencji kodu:
<?php
...
// odczyt konfiguracji
include "config.php";
// dołączanie bibliotek
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
3.4. Zarządzanie sesjami
Kontroler generyczny automatycznie zarządza sesją. Zapisuje i odzyskuje zawartość sesji za pośrednictwem słownika $dSession. Słownik ten może zawierać obiekty, które należy zserializować, aby można je było później poprawnie odzyskać. Kluczem powiązanym z tym słownikiem jest „session”. Odzyskanie sesji odbywa się zatem za pomocą następującego kodu:
<?php
…
// rozpoczęcie lub wznowienie sesji
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
Jeśli dana akcja chce zapisać informacje w sesji, dodaje klucze i wartości do słownika $dSession. Ponieważ wszystkie akcje korzystają z tej samej sesji, istnieje ryzyko konfliktu kluczy sesji, jeśli aplikacja jest tworzona niezależnie przez kilka osób. Stanowi to pewną trudność. Konieczne jest stworzenie repozytorium zawierającego listę kluczy sesji, które będzie wspólne dla wszystkich. Zobaczymy, że każda akcja kończy się wywołaniem następującej funkcji finSession:
<?php
...
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: słownik konfiguracyjny
// $dSession: słownik zawierający informacje o sesji
// $dReponse: słownik argumentów strony odpowiedzi
// zapis sesji
if(isset($dSession)){
// parametry żądania są umieszczane w sesji
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// brak sesji
session_destroy();
}
// wyświetlanie odpowiedzi
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// koniec skryptu
exit(0);
}//zakończenie sesji
Akcja może podjąć decyzję o niekontynuowaniu sesji. W tym celu wystarczy, aby nie przekazać żadnej wartości do parametru $dSession funkcji finSession, co spowoduje usunięcie sesji (session_destroy). Jeśli słownik $dSession istnieje, jest on zapisywany w sesji, a następnie sesja ta jest rejestrowana (session_write_close). Bieżąca operacja może zatem zapisywać elementy w sesji poprzez dodawanie elementów do słownika $dSession. Należy zauważyć, że kontroler automatycznie zapisuje parametry bieżącego żądania w sesji. Umożliwi to ich odzyskanie w razie potrzeby w celu przetworzenia kolejnego żądania.
3.5. Wysyłanie odpowiedzi do klienta
Ostatecznym celem funkcji finSession jest wysłanie odpowiedzi do użytkownika. Wspomnieliśmy, że odpowiedź może mieć różne szablony stron. Są one określane w konfiguracji w funkcji $dConfig['vuesReponse']. W aplikacji z dwoma szablonami mogłoby to wyglądać następująco:
<?php
…
$dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
$dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');
Bieżąca akcja określa w $dReponse['vuereponse'] żądany model. Jest on wyświetlany przez kontroler za pomocą instrukcji:
Po wysłaniu tej odpowiedzi do klienta kontroler kończy działanie (exit).
3.6. Wykonanie akcji
Kontroler oczekuje na żądania zawierające parametr action=XX. Jeśli ten parametr nie występuje w żądaniu, a żądanie ma postać GET, akcja przyjmuje wartość „init”. Dotyczy to pierwszego żądania skierowanego do kontrolera, które ma postać http://machine:port/chemin/main.php.
<?php
…..
// pobieranie działania do wykonania
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
Zgodnie z konfiguracją do każdej akcji przypisany jest skrypt odpowiedzialny za jej obsługę. Na przykład:
<?php
...
// konfiguracja działań aplikacji
$dConfig['actions']['get:init']=array('url'=>'a-init.php');
$dConfig['actions']['post:calculerimpot']=array('url'=>'a-calculimpot.php');
$dConfig['actions']['get:retourformulaire']=array('url'=>'a-retourformulaire.php');
$dConfig['actions']['post:effacerformulaire']=array('url'=>'a-init.php');
$dConfig['actions']['enchainementinvalide']=array('url'=>'a-enchainementinvalide.php');
$dConfig['actions']['actionInvalide']=array('url'=>'a-actioninvalide.php');
Dwie akcje są wstępnie zdefiniowane:
w przypadku, gdy bieżąca akcja nie może nastąpić po poprzedniej akcji | |
sytuacja, w której żądana akcja nie istnieje w słowniku akcji |
Akcje specyficzne dla aplikacji są zapisywane w postaci metoda:akcja, gdzie metoda to metoda GET lub POST żądania, a akcja to żądana akcja, w tym przypadku: init, calculerimpot, retourformulaire, effacerformulaire. Należy zauważyć, że akcja jest pobierana, niezależnie od metody wysyłania parametrów (GET lub POST), za pomocą sekwencji:
<?php
…
// pobieranie działania, które należy podjąć
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
W rzeczywistości, nawet jeśli formularz jest wysyłany metodą POST, zawsze można zapisać:
Elementy formularza zostaną przesłane metodą POST (method='post'). Niemniej jednak żądany adres URL będzie brzmiał: main.php?action=calculerimpot. Parametry tego adresu URL zostaną pobrane ze słownika $_GET, podczas gdy pozostałe elementy formularza zostaną pobrane ze słownika $_POST.
Korzystając ze słownika akcji, kontroler wykonuje żądaną akcję w następujący sposób:
<?php
...
// przetwarzanie działania
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
Jeśli żądana akcja nie znajduje się w słowniku akcji, zostanie wykonany skrypt odpowiadający akcji nieprawidłowej. Po załadowaniu skryptu akcji do kontrolera następuje jego wykonanie. Należy zauważyć, że ma on dostęp do zmiennych kontrolera ($dConfig, $dSession), a także do słowników superglobalnych PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). W skrypcie znajduje się logika aplikacyjna oraz wywołania klas biznesowych. W każdym przypadku akcja musi
- wypełnić słownik $dSession, jeśli elementy mają zostać zapisane w bieżącej sesji
- w $dReponse['vuereponse'] należy podać nazwę szablonu odpowiedzi, który ma zostać wyświetlony
- zakończyć się wywołaniem funkcji finSession($dConfig, $dReponse, $dSession). Jeśli sesja ma zostać zamknięta, akcja zakończy się po prostu wywołaniem funkcji finSession($dConfig, $dReponse).
Dla zachowania spójności akcja może umieścić w słowniku $dReponse wszystkie informacje potrzebne do wyświetlenia widoków. Nie jest to jednak obowiązkowe. Niezbędna jest jedynie wartość $dReponse['vuereponse']. Należy zauważyć, że każdy skrypt akcji kończy się wywołaniem funkcji finSession, która z kolei kończy się operacją exit. Nie ma zatem możliwości powrotu ze skryptu akcji.
3.7. Sekwencja działań
Aplikację internetową można postrzegać jako automat o skończonych stanach. Poszczególne stany aplikacji są powiązane z widokami wyświetlanymi użytkownikowi. Użytkownik, korzystając z linku lub przycisku, przechodzi do innego widoku. Aplikacja internetowa zmienia stan. Widzieliśmy już, że akcja jest inicjowana przez żądanie typu http://machine:port/chemin/main.php?action=XX. Ten identyfikator URL musi pochodzić z linku zawartego w widoku wyświetlanym użytkownikowi. Chcemy bowiem uniknąć sytuacji, w której użytkownik wpisze bezpośrednio adres URL http://machine:port/chemin/main.php?action=XX, pomijając w ten sposób ścieżkę, którą aplikacja dla niego przewidziała. Dotyczy to również sytuacji, gdy klientem jest program.
Sekwencja będzie poprawna, jeśli żądana strona URL jest stroną URL, którą można wywołać z ostatniego widoku wyświetlonego użytkownikowi. Listę tych stron łatwo ustalić. Składa się ona
- z elementów URL zawartych w widoku, zarówno w postaci linków, jak i celów akcji typu „submit”
- URL, które użytkownik może wpisać bezpośrednio w przeglądarce, gdy widok jest mu wyświetlany.
Lista stanów aplikacji niekoniecznie pokrywa się z listą widoków. Rozważmy na przykład następujący podstawowy widok erreurs.php:
Les erreurs suivantes se sont produites :
<ul>
<?php
for($i=0;$i<count($dReponse["erreurs"]);$i++){
echo "<li class='erreur'>".$dReponse["erreurs"][$i]."</li>\n";
}//for
?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dReponse["href"] ?>"><?php echo $dReponse["lien"] ?></a>
Ten widok elementarny zostanie włączony do kompozycji widoków elementarnych, która utworzy odpowiedź. Na tym widoku znajduje się link, którego położenie można dynamicznie zmieniać. Widok erreurs.php może zatem być wyświetlany z n różnymi linkami w zależności od okoliczności. Spowoduje to powstanie n różnych stanów aplikacji. W stanie nr i widok erreurs.php będzie wyświetlany z linkiem lieni. W tym stanie dopuszczalne jest wyłącznie użycie linku lieni.
Lista stanów aplikacji oraz możliwych działań w każdym stanie zostanie zapisana w słowniku $dConfig['etats']:
<?php
...
// konfiguracja stanów aplikacji
$dConfig['etats']['e-formulaire']=array(
'actionsautorisees'=>array('post:calculerimpot','get:init','post:effacerformulaire'),
'vue'=>'e-formulaire2.php');
$dConfig['etats']['e-erreurs']=array(
'actionsautorisees'=>array('get:retourformulaire','get:init'),
'vue'=>'e-erreurs2.php');
$dConfig['etats']['sansetat']=array('actionsautorisees'=>array('get:init'));
Powyższa aplikacja ma dwa stany o nazwach: e-formularz i e-błędy. Dodajemy stan o nazwie bezstanu, który odpowiada początkowemu uruchomieniu aplikacji, gdy nie miała ona jeszcze żadnego stanu. W stanie E lista dozwolonych akcji znajduje się w tabeli $dConfig['etats'][E]['actionsautorisees']. Podano w niej dozwoloną metodę (GET/POST) dla danej akcji oraz jej nazwę. W powyższym przykładzie istnieją cztery możliwe akcje: get:init, post:alculerimpot, get:retourformulaire oraz post:effacerformulaire.
Dysponując słownikiem $dConfig['etats'], kontroler może stwierdzić, czy bieżąca akcja $sAction jest dozwolona w aktualnym stanie aplikacji. Jest on tworzony dla każdej akcji i przechowywany w sesji w $dSession['etat']. Kod kontrolera służący do sprawdzenia, czy bieżąca akcja jest dozwolona, czy nie, wygląda następująco:
<?php
.....
// czy sekwencja działań przebiega prawidłowo?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// nienormalna sekwencja
$sAction='enchainementinvalide';
}//if
// przetwarzanie akcji
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
..........
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// sprawdza, czy bieżąca akcja jest dozwolona w odniesieniu do poprzedniego stanu
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// weryfikacja akcji
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
Logika jest następująca: akcja $sAction jest dozwolona, jeśli znajduje się na liście $dConfig['etats'][$etat]['actionsautorisees'] lub jeśli lista ta nie istnieje co zezwala wówczas na wykonanie dowolnej akcji. $etat to stan aplikacji na końcu poprzedniego cyklu żądania klienta/odpowiedzi serwera. Stan ten został zapisany w sesji i jest tam odzyskiwany. Jeśli okaże się, że żądana akcja jest niedozwolona, uruchamiany jest skrypt $dConfig['actions']['enchainementInvalide']['url']. Skrypt ten zajmie się przesłaniem odpowiedniej odpowiedzi do klienta.
Na etapie rozwoju nie trzeba wypełniać słownika $dConfig['etats']. W takim przypadku każdy stan zezwala na każdą akcję. Słownik można dopracować po całkowitym wyeliminowaniu błędów w aplikacji. Będzie on chronił aplikację przed nieautoryzowanymi akcjami.
3.8. Debugowanie
Kontroler oferuje dwie funkcje debugowania:
- funkcja trace pozwala wyświetlić komunikat w strumieniu HTML
- funkcja „dump” umożliwia wyświetlenie zawartości słownika w tym samym strumieniu
Każdy skrypt akcji może korzystać z tych dwóch funkcji. Ponieważ kod skryptu akcji jest dołączony (include) do kodu kontrolera, funkcje „trace” i „dump” będą widoczne dla skryptów.
3.9. Wniosek
Kontroler generyczny ma na celu umożliwienie programiście skupienie się na akcjach i widokach swojej aplikacji. Zapewnia mu on:
- zarządzanie sesją (przywracanie, zapisywanie)
- sprawdzanie poprawności żądanych akcji
- wykonanie skryptu powiązanego z akcją
- wysyłanie do klienta odpowiedzi dostosowanej do wyniku wykonania akcji