Przetwarzanie skomplikowanych zależności formularza (PHP)

Wszyscy zapewne zetknęli sie kiedyś z potrzebą walidacji danych jakiegoś formularza. Często robi się to prewencyjnie jeszcze po stronie klienta (w JavaScripcie) oraz oczywiście (koniecznie!) także po stronie serwera (PHP).
Bywają jednakże skomplikowane formularze zawierające zaleźności między swoimi polami a wtedy pisanie odpowiednich warunków robi się skomplikowane. Szczególnie jak nagle trzeba będzie coś dodać/zmienić w formularzu czy zależnościach.
Jakiś czas temu zwrócił się do mnie kolega z pracy z podobnym, ale ciut innym problemem - miał skomplikowany formularz z wieloma zależnościami i na podstawie wybranych/wpisanych danych musiał wygenerować docelowo plik umowy, zawierający stosowne paragrafy (wraz z odpowiednimi, wpisanymi danymi).

Myślałem o tym, jak zautomatyzować i uprościć generowanie umowy (po stronie PHP). Wpadłem na pomysł, by każdy paragraf, który ma się pojawić na umowie, powiązać z odpowiednimi warunkami, które zależą od pól formularza. Oczywiście pojawiają się tu różne problemy, głównie z wygodną formą zapisu tychże warunków.
Dodatkowym problemem, jaki występował w tym projekcie, to narzucona odgórnie kolejność poszczególnych paragrafów, które jednak w zależności od pewnych pól formularza mogą się zmienić. Tego nie warto formalizować - jeśli są to sporadyczne przypadki (a zazwyczaj tak właśnie jest) - wygodniej będzie po prostu dopisać stosowne warunki przy docelowym generowaniu umowy i tam zmienić kolejność paragrafów.

Każdy paragraf w docelowej umowie ma szereg warunków, od których zależy jego występowanie jak i jego treść. Przykładowo, jeśli mamy dwa pola z datą - jedno to data rozpoczęcia umowy, a drugie data jej zakończenia, no i nie wypełnimy daty zakończenia, to przyjmuje się, że umowa jest bezterminowa i w paragrafie należy wypisać stosowny tekst, tj. w pierwszym przypadku "umowa na czas określony od ... do ..." a w drugim "umowa na czas nieokreślony od dnia ...".
Ponieważ każdy taki paragraf może mieć kilka części (fragmentów), których treść może się niezależnie zmieniać, trzeba podzielić go na kilka kawałków, a taka postać będzie miała odzwierciedlenie w tablicy warunków (to już szczegóły implementacji), gdzie prócz numeru paragrafu i warunków jego występowania trzeba też trzymać wewnętrzny numer części tego paragrafu (definiującego kolejność tych fragmentów w całym paragrafie).

Wiemy już zatem, że w tablicy warunków (której rozmiar zależy od liczby warunków w niej uwzględnionych – czyli liczby wszystkich fragmentów poszczególnych paragrafów) trzeba trzymać:
  - numer paragrafu;
  - indeks (kolejność) danego fragmentu paragrafu;
  - listę warunków dla jego wystąpienia.
Przy analizie wystąpienia paragrafów (fragmentów) przeglądamy kolejno wszystkie wpisy w tabeli warunków i jeśli dany warunek jest spełniony, to zaznaczamy to w tej tabeli, a później na końcu ją sobie przejrzymy i uporządkujemy te dane tak, by było wygodnie potem wyświetlać poszczególne paragrafy (i ich fragmenty).
Oczywistym jest, że numery paragrafów w tabeli warunków mogą i muszą się powtarzać, ale numery poszczególnych ich fragmentów już nie (fragmenty danego paragrafu muszą mieć unikalny numer i najlepiej żeby były umieszczone we właściwej kolejności – w takiej, w jakiej się pojawią w aneksie/umowie – dzięki temu unikniemy dodatkowego sortowania docelowej tabeli dla każdego paragrafu z osobna).
Po przejrzeniu wszystkich warunków i ich przetworzeniu możemy już w bardzo prosty sposób przeglądać tę docelową (dwuwymiarową) tablicę i generować stosowną zawartość umowy (w PDF czy HTML).

Doprecyzowania wymaga lista warunków wystąpienia danego paragrafu/jego fragmentu, a raczej forma przechowywania tychże (w kodzie skryptu PHP lub z bazie danych - co umożliwi modyfikację tych warunków ustalonym użytkownikom - oczywiście po napisaniu stosownego interfejsu).
Ponieważ warunki te mogą (i zapewne będą) dość skomplikowane, potrzebujemy w miarę prostej (i czytelnej) formy ich zapisu, a przy okazji niezbyt pracowitej i czasochłonnej do zakodowania metody ich analizowania i obliczania.
Patrząc od strony danych przekazanych z formularza mamy do czynienia z nazwami pól oraz przypisanymi im wartościami. Zazwyczaj są to pojedyncze wartości, ale w przypadku multi-selectów oraz pól typu radio/checkbox może to być tablica kilku wartości. W pierwszym przypadku możemy posługiwać się zwykłymi porównaniami (np. "ilosc_dni > 30"), gdzie możliwe są operatory podobne jak w PHP (=  !=  <  >  <=  >=) – ponieważ to my je interpretujemy, to równość "= =" stosowaną w PHP zastąpimy - jako mało wygodną - przez symbol "=". Przypadek tablicy wartości z pozoru jest bardziej skomplikowany, ale możemy go (w zapisie warunku) potraktować tak samo jak dla pojedynczej wartości, tylko w kodzie funkcji sprawdzającej taki warunek musimy uwzględnić istnienie tablicy z wieloma możliwymi wartościami (w tym celu skorzystamy z funkcji is_array() w celu rozpoznania zmiennej tablicowej).
Same porównania wartości (wymienione powyżej) to jeszcze nie wszystko czego potrzebujemy. Warunki jakie będziemy definiować są bardziej skomplikowane i zazwyczaj zależą od wielu zmiennych, dlatego potrzebujemy też móc operować na logicznych porównaniach, jak AND, OR i NOT (jak &&, || i ! w PHP, które możemy oczywiście sobie uprościć do zwykłych &, | i !). Przydatne też będą nawiasy "()" do zapewnienia stosownej kolejności obliczania tych warunków.

Przykładowy warunek: "abonament=stary & (abonament=nowy | aneks_umowa!=1) & okres>6".

Dla uproszczenia analizy i sprawdzania warunków zakładamy, że operatory logiczne (& i |) mają ten sam priorytet i są liczone kolejno od lewej do prawej strony. Negacja (!) ma wyższy priorytet i jest wykonywana w pierwszej kolejności – zawsze neguje wartość warunku, który bezpośrednio poprzedza. Najwyższy priorytet mają operatory porównania (co jest oczywiste). Do wymuszenia (zmiany) kolejności operatorów logicznych używamy nawiasów zwykłych "()". Można dowolnie zagnieżdżać warunki w nawiasach, trzeba tylko pamiętać o poprawnym ich zamknięciu (program to sprawdza i sygnalizuje ewentualny błąd i miejsce jego wystąpienia).

Jeśli chodzi o operacje porównań, to zakładamy, że operujemy na napisach lub liczbach (całkowitych i rzeczywistych), z tym, że ani zmienne ani napisy nie są (dla uproszczenia) zapisywane w cudzysłowach, dlatego nie mogą zawierać spacji (generalnie nie jest to duże ograniczenie, bo dodatkowo, prócz małych i dużych liter oraz cyfr, w nazwach zmiennych możemy też używać znaku łącznika "_"). Wartości tekstowe mogą zawierać wszystkie inne znaki prócz "&" i "|". W zasadzie nie ma przeszkód, żeby i nazwy zmiennych były złożone z dowolnych znaków – z wyjątkiem "!", "=", "<" i ">". Można używać przy opisywaniu warunku spacji, ale służy ona wyłącznie do poprawy czytelności wyrażenia i jest podczas pracy programu usuwana z formuły. Liczby rzeczywiste muszą mieć postać taką jak w PHP (czyli znakiem miejsca dziesiętnego jest kropka), jeśli chcemy, by program poprawnie je zinterpretował przy nierównościach.
Nie możemy używać w warunku wyrażeń arytmetycznych (czyli operatorów + - * / % itd.), ale również nie jest to duże utrudnienie (użyte znaki te będą potraktowane jako fragment napisu) - napisanie pełnego parsera arytmetycznego przekracza potrzeby tego projektu.

Dodatkowo, jeśli zmienna (z formularza) nie ma przypisanej wartości (nie jest ustawiona - jak wyrażenie !isset(zmienna)), to wszystkie porównania, w których występuje, dadzą wartość fałsz (false) - również w nierówności typu "pole!=1" (ale wyrażenie "!pole=1" zwróci prawdę - true). Jest to o tyle logiczne, że jeśli używamy w warunku jakiejś zmiennej, to powinna ona mieć nadaną jakąś wartość (w formularzu), a jak nie ma wartości, to każde porównanie będzie zawsze nieprawdziwe (i zwróci wartość fałsz - false).

Na podstawie powyższej specyfikacji przygotowałem przykładowy program (z ładnie skomentowanym kodem), który powinien rozjaśnić ewentualne wątpliwości co do sposobu korzystania z opisanego mechanizmu.

Oczywiście przygotowanie i zapisanie tych wszystkich warunków występujących w konkretnym rozwiązaniu zajmie trochę czasu i będzie dużym wyzwaniem. Najlepiej jak się tworzy program od początku i wprowadza kolejne warunki kontrolując uzyskany wynik, to łatwiej jest ogarnąć nawet dość złożony problem.

Możnaby nawet przygotować stosowną tabelę w SQL do przechowywania tych wszystkich warunków i zależności oraz formatkę (osobny formularz w PHP) do ich przeglądania, edytowania, dodawania i usuwania. Dałoby to wybranym użytkownikom elastyczne narzędzie do kontroli tychże warunków i jeśli kiedyś w przyszłości byłyby potrzebne jakieś zmiany, to można by je wprowadzić bez potrzeby ingerencji w kod PHP.
Prócz przechowywania warunków można by trzymać w bazie SQL również treść paragrafów (z podziałem na fragmenty), które można by również edytować poprzez stosowną formatkę w PHP. Dane, które zależą od pól formularza (bezpośrednio, a także pośrednio) można zapisywać w specjalnej postaci (np. %zmienna%). Ich wartość będzie podstawiana przez branie wartości takiej zmiennej z ustalonej tablicy (lub tablic - jednej dla pól formularza - $_POST lub $_GET, a drugiej ustalonej w programie, w której trzymane byłyby dodatkowe wartości zależne pośrednio od pól formularza – np. formy słowne odmienionych rzeczowników czy liczebników - choć te można by również trzymać w jednej z wymienionych tablic). Sam znak "%" w treści paragrafu można by uzyskiwać poprzez zapis "%%" (pusta zmienna). Przykładowy kod to realizujący (przy użyciu wyrażenia regularnego) mógłby mieć postać:
    $fragment=preg_replace_callback('/%([a-zA-Z_]*)%/x',create_function('$match','return ($match[1]?$_POST[$match[1]]:"%");'),$fragment);
Ustaliłem tutaj na sztywno w kodzie funkcji, że zmienne są pobierane z tablicy $_POST, by nie pisać osobnej funkcji zawierającej stosowny parametr to ustalający.

Analogiczną funkcjonalność można uzyskać w JavaScripcie dla pól formularza, przerobienie programu jest dość proste, bo PHP i JS mają podobną składnię.

Powrót do strony z wykazem projektów

Valid HTML 4.01 TransitionalValid CSS