8. Formularz dodawania zdjęcia

Dynamiczne strony WWW nie mogą obejść się bez formularzy. Ich obsługa wydaje się prosta, ale nic bardziej mylnego. O zaawansowanych, dynamicznych formularzach z dobrą obsługą błędów pisano już wzdłuż i wszerz, proponując szereg wymyślnych rozwiązań. Jeśli pracowałeś z jakimś frameworkiem, z pewnością miałeś do czynienia z tamtejszymi systemami opartymi w całości o PHP. Konfiguracja wyglądu formularzy była tam dość skomplikowana i powiązana z tym jednym zagadnieniem. Uzyskanie bardziej złożonych układów wymaga niemałej gimnastyki, dlatego Open Power Template udostępnia specjalne narzędzie, które może wspomóc system przetwarzania formularzy w obsłudze warstwy widoku – komponenty. Komponent składa się z dwóch części:

  1. Obiektu PHP zawierającego logikę obsługi danego rodzaju pola (np. textarea).
  2. Portu w szablonie opisującego wygląd pola wraz z jego otoczeniem (np. gdzie ma się wyświetlić tytuł, gdzie błędy, gdzie samo pole...)

Obiekt komponentu możemy utworzyć po stronie skryptu, zapisać do zmiennej i odpalić w żądanym porcie, jednak oprócz tego, możemy zlecić całość zadania szablonowi, co ułatwia nam zadanie przy prostszych formularzach lub bardziej skomplikowanych układach, gdzie niezbędna jest manualna ingerencja. Co więcej, podobnie jak większość innych instrukcji, komponenty potrafią bardzo sprytnie współpracować z fragmentami poznanymi już w związku z dziedziczeniem szablonów. Nie musimy definiować wyglądu każdego portu z osobna (spróbuj sobie wyobrazić ogrom pracy dla witryny z dwudziestoma formularzami, z których każdy zawiera średnio po 10 pól), lecz utworzyć go raz, zapisać we fragmencie i później ładować wszędzie, gdzie to konieczne. Podejście to jest wolne od wad PHP (fragment automatycznie dostosowuje się do konkretnych portów podczas kompilacji), a dodatkowo szalenie wygodne i daje dużo swobody twórczej, jednak aby zacząć z niego korzystać, musimy najpierw napisać jakiś system obsługi formularzy potrafiący współpracować z OPT.

Pełen kod systemu obsługi formularzy jest zamieszczony w pliku /libs/stuff.php. Tutaj skoncentrujemy się ze względów objętościowych na samych komponentach. Komponenty muszą być obiektami klas implementujących interfejs Opt_Component_Interface. Zawiera on dziewięć metod definiujących następujące zachowania:

  1. Zarządzanie parametrami (zapis, odczyt, sprawdzanie istnienia)
  2. Wczytywanie kompletu informacji/parametrów z zewnętrznego źródła (tzw. źródło danych)
  3. Wyświetlanie samego komponentu.
  4. Obsługa zdarzeń.
  5. Zarządzanie atrybutami wybranych znaczników HTML (przydatne przy wyborze stylu CSS).

W naszym systemie będziemy wykorzystywać trzy rodzaje pól: zwykłe pola tekstowe, textarea oraz do wysyłania plików. Musimy więc stworzyć po jednej klasie komponentów dla każdego z nich. Zauważmy jednak, że tworząc spójny system, większość funkcjonalności będzie wspólna, dlatego możemy ją oddelegować do jakiejś klasy abstrakcyjnej, zaś w pozostałych skupić się wyłącznie na różnicach, tj. wyświetlaniu samego komponentu.

abstract class baseComponent implements Opt_Component_Interface
{
    protected $_initialized = false;
    protected $_params = array();
    protected $_form;
    protected $_view;

Oto początek naszej klasy bazowej dla komponentów z pliku /libs/stuff.php. Znaczenie poszczególnych pól jest następujące:

  1. $_initialized – Czy komponent został zainicjowany przez OPT?
  2. $_params – lista parametrów komponentu.
  3. $_form – tablica przechowująca dane o formularzu ze skryptu (czy pola są poprawnie wypełnione itd.)
  4. $_view – obiekt widoku, który uruchomił szablon.

Implementację zaczynamy od napisania konstruktora:

    public function __construct($name = '')
    {
        $this->_params['name'] = $name;
    } // end __construct();

OPT zezwala na podanie jednego argumentu do konstruktora, który interpretujemy jako nazwa pola (parametr „name”). Następnie musimy obsłużyć uruchomienie komponentu w szablonie metodą setView():

    public function setView(Opt_View $view)
    {
        $this->_view = $view;
 
        if(!$this->_view->defined('form'))
        {
            return;
        }
        $this->_form = $this->_view->form;
        $this->_initialized = true;
    } // end setView();

Aby komponent działał poprawnie, zakładamy, że użytkownik utworzył zmienną szablonu $form i zapisał w niej dane o formularzu z informacjami o poprawnie wypełnionych polach. Jeśli tego nie zrobił, nie ustawiamy informacji o inicjacji (posłuży nam to później do łapania błędów).

Następnie tworzymy trzy proste metody do zapisywania, odczytywania i testowania obecności parametrów:

    public function set($name, $value)
    {
        $this->_params[$name] = $value;
    } // end set();
 
    public function get($name)
    {
        return $this->_params[$name];
    } // end get();
 
    public function defined($name)
    {
        return isset($this->_params[$name]);
    } // end defined();

Nie powinno być trudności ze zrozumieniem, co one robią. Do przekazywania danych do komponentu istnieje jeszcze jedna metoda, setDatasource(). W zamierzeniu ma ona obsługiwać podanie wszystkich informacji o komponencie w postaci np. tablicy. My potraktujemy tę listę jako listę atrybutów – pamiętaj, że pisząc własną implementację komponentów, masz pełną swobodę decydowania, do czego tak naprawdę wykorzystasz metody. Są one po prostu interfejsem, poprzez który komunikuje się szablon, udostępniając pewną funkcjonalność, a dokładne wykorzystanie zależy już wyłącznie od naszych potrzeb.

    public function setDatasource($data)
    {
        foreach($data as $name => $value)
        {
            $this->set($name, $value);
        }
    } // end setDatasource();

Kolejna metoda jest już nieco ważniejsza. Komponent może manipulować atrybutami wybranych przez twórcę szablonu znaczników w obrębie portu. Jest to zaznaczane poprzez przeniesienie takiego znacznika do przestrzeni nazw com (np. <com:div>). Można to wykorzystać na wiele sposobów. My chcemy, aby otoczenie pola formularza miało inną klasę CSS, gdy jest błędnie wypełnione tak, by webmaster mógł je w całości pokolorować na czerwono. Metoda otrzymuje tablicę z wartościami poszczególnych atrybutów i podobną tablicę ma zwrócić. OPT przetworzy ją na poprawne atrybuty znacznika:

    public function manageAttributes($tagName, Array $attributes)
    {
        if($tagName == 'div' && !is_null($this->_form[$this->_params['name']]))
        {
            $attributes['class'] = 'error';
        }
        return $attributes;
    } // end manageAttributes();

Celem ułatwienia identyfikacji, metoda otrzymuje też nazwę znacznika, ale należy tu pamiętać o jednym istotnym fakcie. Szablon jest przetwarzany jako XML jedynie w momencie kompilacji, tymczasem komponenty pracują podczas wykonywania szablonu. Drzewo XML już dawno wtedy nie istnieje i jeśli w obrębie tego samego komponentu umieścilibyśmy w przestrzeni com dwa takie same znaczniki (np. dwukrotnie <com:div>), metoda samodzielnie nie jest w stanie ich rozróżnić tak samo, jak nie jest w stanie określić ich treści.

Ostatnia z metod służy do obsługi zdarzeń. W porcie komponentu można umieszczać znaczniki opt:onEvent, które wyświetlają pewną treść przy zajściu pewnego zdarzenia. OPT odpytuje wtedy metodę processEvent(), która musi zwrócić true albo false, zależnie od tego, czy zdarzenie zaszło, czy nie. Dodatkowo, korzystając z zapamiętanego obiektu widoku, komponent ma możliwość przekazania dodatkowych parametrów dla zdarzenia. My zdefiniujemy obsługę dwóch zdarzeń:

  1. error – pole zostało błędnie wypełnione. Komponent rejestruje wtedy w szablonie komunikat błędu, aby można go było wyświetlić.

  2. notInitialized – błąd skryptu – zapomnieliśmy przypisać do szablonu informacji o formularzu.

    public function processEvent($event)
    {
        if($event == 'error')
        {
            if(!is_null($this->_form[$this->_params['name']]))
            {
                $this->_view->error = $this->_form[$this->_params['name']];
                return true;
            }
        }
        else if($event == 'notInitialized')
        {
            return $this->_initialized == false;
        }
        return false;
    } // end processEvent();
} // end baseComponent;

Kod nie jest dość trudny – aby określić, czy pole jest błędnie wypełnione, sprawdzamy czy system obsługi formularzy zdefiniował dla niego komunikat błędu. Jeśli tak, przepisujemy go do szablonu i zwracamy true. Dla nieznanych zdarzeń metoda powinna zawsze zwracać false.

Klasa abstrakcyjna kończy się w tym miejscu. Brakuje w niej jeszcze jednej metody – display(). To nie przypadek – dopiero właściwe klasy będą ją implementować, zależnie od rodzaju pola, jakie wyświetlają. Dla formInput kod jest następujący:

class formInput extends baseComponent
{
    public function display($attributes = array())
    {
        $attributes['type'] = 'text';
        $attributes['name'] = $this->_params['name'];
 
        if(!$this->_form['valid'])
        {
            $attributes['value'] = htmlspecialchars($_POST[$this->_params['name']]);
        }
 
        echo '<input';
        foreach($attributes as $name=>$value)
        {
            echo ' '.$name.'="'.$value.'"';
        }
        echo '/>';
    } // end display();
} // end formInput;

OPT przekazuje do metody wartości atrybutów, jakie twórca szablonu przekazał do znacznika <opt:display>, które możemy sobie dowolnie zinterpretować (albo też zignorować). My, zamiast tego, traktujemy je jako normalne atrybuty danego pola, do których dopisujemy odpowiednie informacje generowane przez komponent. Metoda robi trzy rzeczy:

  1. Ustawia nazwę pola na podstawie parametru name.

  2. Jeśli pole jest źle wypełnione, przepisuje z $_POST wartość wpisaną przez użytkownika, jeśli źle wypełnił formularz (dzięki temu nie traci on tego, co wpisał, podczas odrysowywania formularza).

  3. Generuje kod HTML danego pola, ale bez żadnej dodatkowej otoczki, jak to robią przeważnie systemy szablonów oparte o czysty PHP.

Jak wspomnieliśmy, nasz kod definiuje trzy rodzaje komponentów. Ponieważ kod metody display() jest w nich dość podobny, nie będziemy go tutaj przytaczać – możesz spróbować zaimplementować je w ramach ćwiczenia, my tymczasem od razu zarejestrujemy je w OPT i przydzielimy im jakiś znacznik XML:

$tpl->register(Opt_Class::OPT_COMPONENT, 'opt:input', 'formInput');
$tpl->register(Opt_Class::OPT_COMPONENT, 'opt:textarea', 'formTextarea');
$tpl->register(Opt_Class::OPT_COMPONENT, 'opt:file', 'formFile');

Rejestracja nie jest konieczna, lecz wtedy obiekty komponentów danego rodzaju będą mogły być tworzone wyłącznie po stronie skryptu. Port komponentu wygląda następująco:

<opt:jakisKomponent datasource="$dane" str:name="pole">
  <opt:set str:name="title" str:value="Nazwa pola" />
  <com:div>
      <p><label parse:for="$system.component.name~'_id'">
         {$system.component.title}</label></p>
      <opt:display />
      <opt:onEvent name="error">
         <p class=”error”>Błąd: {$error}</p>
      </opt:onEvent>
  </com:div>
</opt:jakisKomponent>

Znaczenie poszczególnych znaczników:

  1. <opt:jakisKomponent> - tworzy port, a wraz z nim automatycznie obiekt komponentu zarejestrowanego dla podanego znacznika metodą register().
  2. datasource – wspomniane wcześniej źródło danych dla komponentu. Atrybut ten jest opcjonalny
  3. str:name – pozostałe atrybuty są interpretowane jako parametry komponentu. Przestrzeń nazw str mówi, że wartość atrybutu nie jest wyrażeniem OPT (np. zmienną), lecz ciągiem tekstowym, by nie pogubić się w apostrofach i cudzysłowach.
  4. <opt:set> - alternatywny sposób tworzenia parametrów w komponencie.
  5. $system.component.name – zmienna specjalna $system pozwala uzyskiwać szereg ciekawych informacji. $system.component daje nam dostęp do wszystkich parametrów aktualnego komponentu, dzięki czemu możemy je prosto wyświetlać w obrębie portu.
  6. <opt:display> - tutaj wyświetli się komponent.
  7. <com:div> - znacznik HTML, którego atrybutami może manipulować komponent.
  8. <opt:onEvent> - definicja zdarzenia. Za pomocą atrybutu name określamy nazwę.

Jeśli chcemy stworzyć sam port bez komponentu, zamiast <opt:jakisKomponent> musimy skorzystać ze znacznika <opt:component>, dla którego dodatkowo musimy podać atrybut from. Określa on zmienną, w jakiej zapisany jest obiekt komponentu. Taki port możemy zamknąć w sekcji, tworząc w pełni dynamiczny formularz – wszystkie pola tworzone są po stronie skryptu, ich obiekty pakowane do listy i wyświetlane po kolei w pętli przy pomocy sekcji. Jeśli podana zmienna nie będzie zawierać poprawnego obiektu komponentu, port po prostu się nie wyświetli.

Teraz kolejna sztuczka, tym razem (w końcu) pokazana na naszej galerii zdjęć. Utwórz plik /templates/snippets.tpl o następującej treści:

<?xml version="1.0" ?>
<opt:root>
    <opt:snippet name="formField">
        <com:div>
            <label parse:for="'l_'~$system.component.name">{$system.component.title}: </label>
            <opt:display parse:id="'l_'~$system.component.name" />
 
            <opt:onEvent name="error">
                <p class="error">{$error}</p>
            </opt:onEvent>
            <opt:onEvent name="notInitialized">
                <p class="error">Komponent nie został właściwie zainicjowany!</p>
            </opt:onEvent>
        </com:div>
    </opt:snippet>
</opt:root>

Wszystkie pola naszych formularzy będą mieć ten sam wygląd, nie ma więc sensu powtarzać w kółko tego samego kodu. Zamiast tego, utworzyliśmy fragment formField – zauważmy, że zawiera on w pełni poprawną definicję wnętrza portu. Teraz możemy podłączyć ją pod komponenty tworzące formularz dodawania zdjęcia:

<?xml version="1.0"?>
<opt:extend file="layout.tpl">
    <opt:snippet name="content">
        <opt:if test="not $form.valid">
            <p>Formularz nie został prawidłowo wypełniony.</p>
        </opt:if>
        <div class="form">
            <form method="post" action="index.php?action=photo" enctype="multipart/form-data">
            <opt:input name="title" template="formField">
                <opt:set str:name="title" str:value="Tytuł zdjęcia" />
            </opt:input>
            <opt:file name="file" template="formField">
                <opt:set str:name="title" str:value="Plik" />
            </opt:file>
            <input type="submit" value="Dodaj" />
            </form>
        </div>
    </opt:snippet>
</opt:extend>

Szablon bazowy layout.tpl ładuje nam plik snippets.tpl, a my możemy wczytać dany fragment do portu atrybutem template, oszczędzając sobie pracy. Jeśli zmienimy wygląd we fragmencie formField, zmiana będzie natychmiast widoczna we wszystkich polach formularzy, które z niego korzystają. Zwróćmy uwagę, że OPT automatycznie dopasowuje fragment do konkretnego portu. Osiągnięcie podobnego efektu w PHP jest dużo trudniejsze i widać to zwłaszcza we frameworkach takich, jak Symfony czy Zend Framework – twórcy żadnego z nich nie byli w stanie w oparciu o czyste PHP zbudować równie prostej obsługi wyglądu formularza, pakując się albo w zaawansowane programowanie obiektowe, albo w potworki, jak szablony szablonów, dla których i tak trzeba było stworzyć parser... przykład ten pokazuje, że całkowicie nowy język szablonów, przy odrobinie wysiłku ze strony autorów, nie musi powielać wad PHP, lecz upraszczać wiele rzeczy. Nie bez znaczenia jest też fakt, że OPT załatwia sprawę na etapie rzadko uruchamianej kompilacji, zaś później operuje już na bardzo prostym kodzie PHP. Nie musi on być czytelny dla programisty, dlatego nie trzeba stosować w nim złożonych mechanizmów języka.

Uwaga

Fragmenty można doklejać również do niemal wszystkich innych znaczników, lecz tam nie służy do tego atrybut template lecz opt:use. Porty komponentów są wyjątkiem, ponieważ muszą jeszcze zezwalać na używanie znacznika <opt:set>, który w przeciwnym wypadku zostałby zamazany. Spróbuj zastąpić atrybut template przez opt:use i obserwuj, co się wydarzy – wszystkie pola nie będą mieć ustawionego tytułu.

Kod akcji dodawania zdjęcia zapiszemy w pliku /actions/photo.php:

<?php
function action()
{
    if($_SERVER['REQUEST_METHOD'] == 'POST')
    {
        $form = array(
            'valid' => true,
            'title' => null,
            'file' => null,
        );
        // Kontrola poprawnosci danych
        validateLength(&$form, 'title', 3, 50, 'Długość tytułu musi znajdować się w granicach od 3 do 50 znaków.');
        validateUpload(&$form, 'file', './photos/', 'Nie udało się skopiować pliku na serwer.');
 
        if($form['valid'])
        {
            // Generowanie miniaturki.
            resizeImage('./photos/'.$_FILES['file']['name'], './photos/thumb/'.$_FILES['file']['name']);
 
            // Dodanie informacji do bazy
            $photo = new Photo;
            $photo->title = $_POST['title'];
            $photo->filename = $_FILES['file']['name'];
            $photo->save();
 
            // Wyswietlenie komunikatu
            $view = new Opt_View('message.tpl');
            $view->title = 'Komunikat';
            $view->message = 'Zdjęcie zostało dodane';
            $view->redirect = 'index.php';
            return $view;
        }
        else
        {
            // Formularz zostal blednie wypelniony
            $view = new Opt_View('photo_add.tpl');
            $view->form = $form;
            $view->title = 'Dodaj zdjęcie';
            return $view;
        }
    }
    else
    {
        // Domyslna tresc formularza.
        $view = new Opt_View('photo_add.tpl');
        $view->form = array(
            'valid' => true,
            'title' => null,
            'file' => null,
        );
        $view->title = 'Dodaj zdjęcie';
        return $view;
    }
} // end action();

validateLength() oraz validateUpload() to dwie funkcje mocno uproszczonej kontroli formularzy. Nie przytaczaliśmy ich kodu, ponieważ nie jest to tematem artykułu – znajdziesz je za to (wraz z opisem) w archiwum z pełnym kodem. Nam wystarczy wiedzieć, że wypełniają one tablicę $form z informacjami o formularzy, którą później wykorzystają komponenty. Akcja działa następująco:

  1. Jeśli strona wyświetlana jest po raz pierwszy, akcja wyświetla czysty formularz.
  2. Jeśli użytkownik źle wypełnił niektóre pola, formularz jest odrysowywany wraz z prawie całą wpisaną przez użytkownika zawartością i zaznaczonymi błędnie wypełnionymi polami.
  3. Jeśli dane zostały poprawnie wpisane, do akcji wkracza Doctrine.

Dodawanie nowego zdjęcia do bazy przy pomocy Doctrine jest bardzo proste. Spójrzmy jeszcze raz na kod:

$photo = new Photo;
$photo->title = $_POST['title'];
$photo->filename = $_FILES['file']['name'];
$photo->save();

Wszystko, co musimy zrobić, to utworzyć obiekt klasy Photo i przypisać polom odpowiednie wartości. Jak pamiętamy z wcześniejszej części artykułu, pole date jest wypełniane automatycznie przez model. Na końcu musimy wywołać metodę save(). Doctrine pamięta, czy dany wiersz znajduje się już w bazie czy nie i generuje zapytanie INSERT albo UPDATE, a w tym drugim przypadku dodatkowo potrafi uwzględnić tylko te pola, które faktycznie się zmieniły. Pobrać obiekt modelu dla istniejącego już wiersza jest bardzo prosto:

Doctrine::getTable('Photo')->find($id);