PHP Jet Framework

rychle, jednoduše, přímo, bezpečně, efektivně
čeština
Jak začít Dokumentace Ke stažení Kontakt Diskuze / Fórum Blog
Dokumentace

Práce se seznamy dat - Jet\Data_Listing

Nejprve mi dovolte obecný úvod a vysvětlení co Data_Listting je a proč je právě takový jaký je a na základě jakých poznatků z praxe jsme k tomu došel.

Nedílným prvkem online aplikací, zejména administrací všeho možného, či informačních systémů je práce se seznamy dat. Ať už je to seznam článků, seznam zboží, seznam uživatelů, seznam faktur a jiných dokladů, seznam událostí ... Stále jsou to seznamy (nebo tzv. data gridy) a je to stále se dokola opakující problém a opakující se funkce.

Roky jsem hledal cestu jak tuto funkcionalitu řešit správně. Pochopitelně jsem zabloudil do slepých uliček. Jedna z nich byla sofistikovaný javascript widget (konkrétně datagrid ze frameworku DoJo / Dijit), který si přes AJAX tahal json ze služby na serveru. Technicky se to zdálo "cool" a podobné řešení vidím i dnes v nových aplikacích často. Další slepou uličkou bylo ukládání stavu gridu / seznamu (například nastavení filtru a podobně) v session a podobné "vylomeniny". Nic z toho už bych nikdy neudělal. Není k tomu vlastně žádný důvod - možná krom nějaké snahy o kopírování chování desktopové aplikace. Ovšem online aplikace dnešních dnů je schopná dělat to samé co desktopová a k tomu spoustu užitečných věcí navíc, které dekstopové aplikace prostě neumí už z principu. Je tedy dobré emancipovaně dělat online aplikace s využitích všech jejich vlastností a možností. Jednou z těch úžasných vlastností jsou odkazy - URL adresy.

Proč zmiňuji v tématu seznamů a data gridů URL adresy? Protože právě zde jsou URL velice užitečné a vlastně důležité. Pokud je aplikace kterou děláte pro lidi primární pracovní nástroj a vlastně zdroj obživy (například e-shop), tak to velice rychle poznáte. Uživatelé a uživatelky totiž velice rychle objeví možnost poslat někomu odkaz na vyfiltrovaný seznam položek. Například pracovnice produktového oddělení vyfiltruje určité produkty a pošle odkaz (třeba mailem) na tento filtr kolegovy na marketingu s komentářem: "Tome, tady máš nachystané ty produkty na kamapň v příštím měsíci". Nebo jiná pracovnice může vyfiltrovat zásilky a poslat je někam s komentářem: "Toto jsou poškozené zásilky za minulý měsíc.". Nebo vy si můžete vyfiltrovat určité události z protokolu (ostatně v ukázkové aplikaci si to můžete vyzkoušet) a tak dále. Prostě toto je vlastnost, kterou uživatelé rychle objeví a především ocení. Uživatel nepotřebuje žádný JavaScript data grid (který je ve skutečnosti často pomalý a méně pohodlný než desktopová aplikace). To pro uživatele nemá hodnotu, nebo to má dokonce hodnotu zápornou. Uživatel potřebuje užitečné věci. A možnost poslat někomu odkaz na nějaké vyfiltrované položky je překvapivě hodně užitečná vlastnost. Proto jsem se naučil dělat seznamy (listování daty) právě takto a proto má Jet třídy, které tvorbu takových funkcí unifikuje a především usnadňuje.

Princip fungování

Zatím jsme si řekli co má Jet\Data_Listing řešit a jak a proč jsem k tomu došel. Teď je na čase si říct co to dělá a jak to funguje. Pokud jste to ještě neudělali, tak se prosím koukněte do administrace ukázkové aplikace a vyzkoušejte si třeba prohlížeč událostí. To je dobrý reprezentativní příklad práce se seznamem.

Je vidět že:

  • Nad samotným gridem s výpisem seznamu je formulář s různými filtry
  • Formulář filtrů samozřejmě využívá systém formulářů pro definici, validaci i vykreslení.
  • Formulář filtrů se odesílá jako POST, zachytává se, validuje a následně je sestavena URL s GET parametry, které finálně definují aktuální nastavení filtru.
  • V URL nejsou pouze GET parametry filtru, ale rovněž čísla stránky a taktéž indikace řazení (podle jakého sloupce a v jakém směru se má seznam řadit)
  • Tedy URL plně definuje aktuální nastavení seznamu a jakmile jí otevře jiný uživatel, dostane stejný seznam (pokud se data mezi tím nezmění - samozřejmě).
  • Pod filtrem je data grid - tedy UI prvek pro jehož vykreslení a základní obsluhu je využit systém UI a jeho prvku UI_dataGrid.
  • Automaticky se předpokládá že s daty se operuje pomocí DataModel a je využíván i stránkovač.
Je zřejmé, že Jet\Data_Listing je ve skutečnosti integrační prostředek, který využívá ostatní prvky platformy Jet a usnadňuje a sjednocuje tvorbu takovýchto rozhraní pro práci se seznamy. Tedy dále se budeme soustředit právě na onu integrační funkci a práci s formuláři, s DataModel a UI_dataGrid prosím si omrkněte v příslušných kapitolách, pokud jste tak ještě neučinili.

Vytvoření seznamu

Třída Jet\Data_Listing je abstraktní třída a má dvě abstraktní metody. Je tedy zřejmé, že každý konkrétní seznam bude reprezentováni svou třídou, která jej definuje a která tedy musí od Jet\Data_Listing dědit. Ukažme si rovnou jednu zcela konkrétní třídu z ukázkové aplikace a to právě seznam událostí z aplikačního modulu EventViewer.Admin: namespace JetApplicationModule\EventViewer\Admin;

use 
Jet\Data_Listing;
use 
Jet\DataModel_Fetch_Instances;

use 
JetApplication\Logger_Admin_Event as Event;

class 
Listing extends Data_Listing {

    protected array 
$grid_columns = [
        
'id'                  => ['title' => 'ID'],
        
'date_time'           => ['title' => 'Date time'],
        
'event_class'         => ['title' => 'Event class'],
        
'event'               => ['title' => 'Event'],
        
'event_message'       => ['title' => 'Event message'],
        
'context_object_id'   => ['title' => 'Context object ID'],
        
'context_object_name' => ['title' => 'Context object name'],
        
'user_id'             => ['title' => 'User ID'],
        
'user_username'       => ['title' => 'User name'],
    ];

    protected 
string $default_sort '-id';

    protected function 
initFilters(): void
    
{
        
$this->filters['search']         = new Listing_Filter_Search$this );
        
$this->filters['event_class']    = new Listing_Filter_EventClass$this );
        
$this->filters['event']          = new Listing_Filter_Event$this );
        
$this->filters['date_time']      = new Listing_Filter_DateTime$this );
        
$this->filters['user']           = new Listing_Filter_User$this );
        
$this->filters['context_object'] = new Listing_Filter_ContextObject$this );
    }

    protected function 
getList() : DataModel_Fetch_Instances
    
{
        return 
Event::getList();
    }


}
... a tím je seznam definován.

Je důležité si všimnout následujícího:

  • protected array $grid_columns
    Tato vlastnost definuje jaké sloupce budou v data gridu - viz Jet\UI_dataGrid. Každý sloupec musí mít svůj identifikátor (klíč pole), titulek ('title') a může mít ještě atribut 'disallow_sort' (bool hodnota) pro vypnutí řazení dle daného sloupce.

    Titulek je automaticky předán překladači.

    Definice sloupců gridu nemusí být dána pouze touto definicí pomocí vlastnosti, ale je možné obdobnou definici předat pomocí metody setGridColumns. Je tedy možné sloupce definovat dynamicky. Například je možné implementovat funkcionalitu, kdy si uživatel sloupce sám vybírá, řadí, nebo si vybírá předdefinované pohledy a podobně.

    Ovšem vlastnost $grid_columns představuje výchozí definici sloupců.

  • protected string $default_sort
    Pomocí této vlastnosti je určeno výchozí řazení. Tedy podle jakého sloupce (hodnota je identifikátor jednoho z definovaných sloupců) a v jakém směru. Znak '-' na začátku určuje sestupný směr. Znak '+', nebo žádný znak určuje směr vzestupný.

    I definici výchozího řazení je možné ovlivnit metodou a to setDefaultSort.

  • protected function initFilters(): void
    Tato metoda inicializuje filtry. To je samostatné téma na které se koukneme dále.

  • protected function getList() : DataModel_Fetch_Instances
    Metoda slouží k faktickému načtení dat. Všimněte si prosím, že se nestará o filtrování, řazení, stránkování. O to se postará zbytek třídy. Úkolem této metody je "někde sehnat" instanci Jet\DataModel_Fetch_Instances a tak se postarat o načtení dat.

Filtry

K čemu by byl seznam bez filtrů ... Jak filtry inicializovat jsme si ukázali před okamžikem. Teď je koukneme na filtry samotné. Každý filtr je tvořen svou třídou (nebo anonymní třídou - koukněte například do modulu Content.Articles.Admin na třídu Listing), která dědí od abstraktní třídy Jet\Data_Listing_Filter a reprezentuje logiku daného filtru. Rovnou si jeden reálný filtr ukažme. Zůstaneme u prohlížeče událostí a koukneme se na filtr pomocí kterého je možné filtrovat seznam dle druhu události: namespace JetApplicationModule\EventViewer\Admin;

use 
Jet\Data_Listing_Filter;
use 
Jet\Form;
use 
Jet\Form_Field_Input;
use 
Jet\Http_Request;

class 
Listing_Filter_Event extends Data_Listing_Filter {

    protected 
string $event '';

    public function 
catchGetParams(): void
    
{
        
$this->event Http_Request::GET()->getString'event' );
        
$this->listing->setGetParam'event'$this->event );
    }

    public function 
generateFormFieldsForm $form ): void
    
{
        
$field = new Form_Field_Input'event''Event:'$this->event );

        
$form->addField$field );
    }

    public function 
catchFormForm $form ): void
    
{
        
$this->event $form->field'event' )->getValue();
        
$this->listing->setGetParam'event'$this->event );
    }

    public function 
generateWhere(): void
    
{
        if( 
$this->event ) {
            
$this->listing->addWhere( [
                
'event' => $this->event,
            ] );
        }
    }

}
Co tedy musí povinně filtr umět:

  • zachytávat své GET parametry - metoda catchGetParams
  • generovat definice formulářových prvků pro filtrační formulář - metoda generateFormFields
  • zachytávat hodnoty z filtračního formulář (který je v daný moment již zachycen a validován) - metoda catchForm
  • generovat dotaz - tedy where pro DataModel - metoda generateWhere
Všimněte si prosím, že filtr nastavuje seznamu své GET parametry, které potřebuje pro fungování: $this->listing->setGetParam'event'$this->event ); To je velice důležité.

Žádná jiná záhada v tom není. Ještě se sluší upozornit na abstraktní třídu Jet\Data_Listing_Filter_Search. Protože hledací filtr je věc která se neustále opakuje, tak je hledací filtr v Jet předpřipravený a stačí pouze implementovat generování where. Například takto: namespace JetApplicationModule\EventViewer\Admin;

use 
Jet\Data_Listing_Filter_Search;

class 
Listing_Filter_Search extends Data_Listing_Filter_Search {

    public function 
generateWhere(): void
    
{
        if( 
$this->search ) {
            
$search '%'.$this->search.'%';
            
$this->listing->addWhere([
                
'event *'        => $search,
                
'OR',
                
'event_class *' => $search,
                
'OR',
                
'event_message *' => $search,
            ]);
        }
    }
}

Použití seznamu v kontroleru

Opět se budeme držet našeho ukázkového prohlížeče událostí a ukážeme si příslušnou metodu kontroleru daného modulu: public function listing_Action() : void
{
    
$listing = new Listing();
    
$listing->handle();

    
$this->view->setVar'filter_form'$listing->getFilterForm());
    
$this->view->setVar'grid'$listing->getGrid() );

    
$this->output'list' );
}

Nic víc v kontroleru řešit nemusíte. Stačí vytvořit intanci, pak zavolat metodu handle a následně view předat definici filtračního formuláře, instanci UI data gridu a view vyrenderovat.

Ovšem pokud se rozhodnete například implementovat dynamickou definici sloupečků (tato možnost byla nastíněna výše), tak metody setGridColumns musíte použít před voláním metody handle.

Použití ve view

Jak celé uživatelské rozhraní zobrazit je poslední dílek co schází do skládačky. A zde mi dovolte takové "šalamounské" řešení, protože ve view není nic složitého co by bylo nutné v dokumentaci detailně rozebírat. Prosím koukněte se do ukázkové aplikace například na view skript ~/application/Modules/EventViewer/Admin/views/listing.phtml a tam se prosím inspirujte :-)

Konfigurace

Pro Data_Listing existuje systémová konfigurace SysConf_Jet_Data_Listing. Ta umožňuje nastavit:

  • Název GET parametru, který v seznamech určuje číslo stránky. (běžná hodnota: 'p')
  • Název GET parametru, který může určit kolik položek má být zobrazeno na stránce seznam. (běžná hodnota: 'items_per_page')
  • Název GET parametru, který určuje řazení seznamu. (běžná hodnota: 'sort')
  • Horní hranici počtu položek na jednu stránku seznamu. (za všech okolností však maximálně 500)
  • Výchozí množství položek seznamu na jedno stránku. (běžná hodnota: 50)
Systémovým nastavením (tedy jeho příslušnými metodami) můžete během inicializace změnit výchozí nastavení a tak globálně změnit všech chování všech seznamů ve vaší aplikaci. Což je právě smyslem systémové konfigurace :-)

Seznam metod třídy Jet\Data_Listing

Metoda Význam
abstract protected initFilters(
) : void
Metoda inicializuje filtry. Viz předchozí text.
abstract protected getList(
) : DataModel_Fetch_Instances
Metoda se postará o získání dat. Viz předchozí text.
public setGridColumns(
array $grid_columns
) : void
Nastavuje definice sloupců seznamu z venku.

Je nutné volat před voláním metody handle.
public getGridColumns(
) : array
Vrací definici sloupců seznamu.
public setDefaultSort(
string $default_sort
) : void
Nastavuje výchozí řazení z venku.

Je nutné volat před voláním metody handle.
public getDefaultSort(
) : string
Vrací nastavené výchozí řazení.
public handle(
) : void
Postará se o vše potřebné.
public setGetParam(
string $parameter,
mixed $value
) : void
Metoda je určená primárně pro komunikaci s filtry.

Nastavuje příslušný GET parametr.
public unsetGetParam(
string $parameter
) : void
Metoda je určená primárně pro komunikaci s filtry.

Ruší příslušný GET parametr.
public addWhere(
array $where
) : void
Metoda je určená primárně pro komunikaci s filtry.

Přidává část dotazu / část where.
public getURI(
) : string
Vrací URI (GET parametry v podobě řetězce) dle aktuální situace.
public getFilterForm(
) : Form
Vygeneruje filtrační formulář.
public getGrid(
) : UI_dataGrid
Vygeneruje UI dataGrid.
Předchozí kapitola
Jet\Data_Paginator_Exception
Další kapitola
Jet\Data_Listing_Filter