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č.
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;
... a tím je seznam definován.
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();
}
}
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;
Co tedy musí povinně filtr umět:
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 generateFormFields( Form $form ): void
{
$field = new Form_Field_Input( 'event', 'Event:', $this->event );
$form->addField( $field );
}
public function catchForm( Form $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,
] );
}
}
}
- 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
$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)
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. |