English
version 2.2.1

  PClib - PHP component library



28. 03. 2012

Rychlokurz - třída app

Vývoj netriviální aplikace vyžaduje používat jasně definovanou modulární strukturu, jednotné rozhraní (způsob komunikace) mezi moduly a jmennou konvenci - u chaoticky napsané aplikace brzy zjistíme, že je její udržování a rozšiřování o nové funkce neobyčejně pracné. Ukážeme si jednu z možných koncepcí. V jiných frameworcích se často používá MVC pattern a jednotlivé složky - moduly, šablony zobrazení, přístup k databázové tabulce, konfigurace atp. jsou reprezentovány objekty - my si ukážeme jednodušší a podle mě názornější neobjektový (nebo slabě objektový) přístup.
Jako příklad použijeme aplikaci na správu úkolů - taskman.
Jde se o podobný druh programu, který se používá například k reportování chyb, jako třeba bugzilla.

Budeme mít seznam projektů (projects) a každý z nich může obsahovat libovolný počet úkolů (tasks) v různých stavech - např. - nový,zpracovávaný,dokončený,zrušený.
Použití aplikace bude vypadat asi tak, že zadavatel (client) vytvoří nový úkol v příslušném projektu, programátor (developer) ho označí za zpracovávaný a v okamžiku, kdy je hotový, za dokončený.
Kdykoliv si bude možné prohlédnout seznam naplánovaných a dokončených úkolů.
V rámci tohoto tutoriálu nebude kompletní realizace programu, to není jeho cílem, ukážeme si jen schema tvorby takového programu a přitom se mimochodem naučíme používat pclib třídu app (aplikace).
Je samozřejmé, že reálný, v praxi užitečný program by musel obsahovat
ještě celou řadu rozšíření a vylepšení.

Naše aplikace bude mít čtyři moduly:

projects - správa projektů
tasks - správa úkolů ke konkrétnímu projektu
users - správa uživatelů s přístupem do taskmanu
uac - úvodní obrazovka/přihlášení/odhlášení popř. nastavení uživatelského účtu (např. změna hesla)

Ke správě přístupu použijeme autorizační systém auth a budeme mít tři role:

developer - má přístup do svých projektů, může měnit stav úkolů
client - má přístup do svých projektů, může vytvářet úkoly
admin - může zakládat a mazat projekty a uživatele, přiřazovat uživatel k projektům.

Datová struktura bude velmi přímočará.
Uživatelé budou uloženi v pclib tabulce AUTH_USERS a vedle ostatních tabulek
vyžadovaných pclib autorizačním systémem budeme mít jen tyto:

PROJECTS - projekty (ID,NAZEV,POPIS,STAV,OWNER_ID,LASTMOD,DT)
TASKS - úkoly (ID,PROJECT_ID,NAZEV,POPIS,STAV,OWNER_ID,LASTMOD,DT)
USER_PROJECT (USER_ID,PROJECT_ID) - udává do kterých projektů má který uživatel přístup.

- Každá tabulka obsahuje primární klíč ID (auto_increment), který jednoznačně identifikuje konkrétní projekt, uživatele nebo task.
- Každý task obsahuje odkaz na svůj projekt (PROJECT_ID)
- Projekty a úkoly mají odkaz na ID uživatele, který je vytvořil (OWNER_ID), dále datum založení a poslední změny (DT,LASTMOD) a STAV - což je číslo (tinyint) které až v šabloně přeložíme na textový název stavu, pomocí lookup tabulky.

Adresářová struktura našeho projektu může vypadat například takto:


/taskman
/pclib
/images
/modules
-projects.php
-tasks.php
-users.php
-uac.php
/tpl
-website.tpl
-prjlist.tpl
-prjform.tpl
-tasklist.tpl
-taskform.tpl
-userlist.tpl
-userform.tpl
-loginform.tpl
-welcome.tpl
index.php
config.php
taskman.css
taskman.js


Adresář /pclib obsahuje knihovní soubory, v /modules jsou naše čtyři moduly, /images je určený k uložení případných obrázků a /tpl obsahuje všechny použité šablony.
Veškerý přístup k aplikaci se bude dít prostřednictvím souboru index.php, který slouží jako rozcestník k jednotlivým modulům. Soubor s css-styly a javaskriptem máme uložený v kořenovém adresáři /taskman.

Tato struktura se celkem osvědčila pro menší nebo středně velké projekty.
U větších projektů bychom asi chtěli nějak rozdělit podadresář /tpl, aby v něm nebylo nepřehledné množství šablon, popř. vytvořit samostatné adresáře /css a /js pro soubory kaskádních stylů a javascriptů, přidat další pomocné adresáře, adresáře pro jazykové mutace atd. atd.

* Pozn: V adresáři /tpl je pomocí .htaccess možné zakázat výpis zdrojového kódu šablon do prohlížeče.

V jiných frameworcích je tato adresářová struktura často mnohem rozsáhlejší, adresář má např. každý modul, přičemž tento obsahuje četné podadresáře a velké množství souborů - leckdy pro každou třídu jeden, ale já osobně dávám přednost jednodušší struktuře a malému množství souborů. Mám pocit, že pokud nejde o opravdu velký projekt (a co si budeme povídat, 99% projektů spadá do kategorie středně velkých nebo malých) tak příliš komplexní a obecný přístup věci spíš znepřehledòuje a komplikuje.

Zapomněli jsme zmínit soubor config.php, který obsahuje konfiguraci aplikace, zejm. nastavení AUTH_SALT a databázového připojení. Je dobré udržovat onfiguraci na jednom místě a izolovaně od zbytku kódu.
---
Srdcem naší aplikace budiž soubor index.php. V něm načteme pclib, konfiguraci, provedeme inicializaci databáze, autorizaci, inicializaci třídy app, přečteme řídící proměnné a zavoláme příslušnou akci modulu a nakonec i zobrazíme výsledný výstup.

<br>
* \file index.php<br>
 * \version taskman 0.0<br>
 */
<br>
<
br>
//nacteme konfiguraci s konstantami APPNAME,CONNECTION_STR,VERSION_STR atd.<br>
require_once ('config.php');<br>
<
br>
//nacteme soubory pclib<br>
require_once ('pclib/pcl.php');<br>
require_once (
'pclib/db.php');<br>
// (...) + dalsi potrebne soubory z adresare pclib, pro strucnost vynechavame<br>
<br>
//inicializace globalnich objektu<br>
<br>
$db = new db(CONNECTION_STR);<br>
$auth = new auth(APPNAME);<br>
$user $auth->getuser();<br>
<
br>
$app = new app(APPNAME);<br>
<
br>
//nacteni nekterych globalnich hodnot do hlavni sablony website<br>
<br>
$app->tpl->values['MENU']    = $app->getmenu();<br>
$app->tpl->values['VERSION'] = VERSION_STR;<br>
$app->tpl->values['UNAME']   = $user['FULLNAME'];<br>
<
br>
//defaultni modul je uac - zobrazi login formular po spusteni aplikace<br>
if (!$app->modul$app->modul 'uac';<br>
<
br>
//rozcestnik modulu<br>
switch ($app->modul) {<br>
  case 
'projects': require_once 'projects.php'; break;<br>
  case 
'tasks':    require_once 'tasks.php';    break;<br>
  case 
'users':    require_once 'users.php';    break;<br>
  case 
'uac':      require_once 'uac.php';      break;<br>
<
br>
  default:<
br>
  
$app->error(0'Modul nenalezen.');<br>
  break;<
br>
//end switch<br>
<br>
$app->tpl->values['NAVIG'] = $app->getnavig();<br>
<
br>
//vypiseme obrazovku aplikace<br>
$app->out();<br>
<
br>
//konec index.php<br>
<br>
 


* Pozn: Je dobrou zásadou předávat jako modul pouze id modulu a ne skutečný název souboru např. 'projects.php' a nevkládat ho jako require_once $app->modul;
*
Co se tady děje?
Nejprve vytvoříme objekt pro přístup k databázi a k autorizaci.
Tyto objekty budeme používat napříč aplikací a jsou proto globální. Také budeme potřebovat údaje o aktuálně přihlášeném uživateli, které budeme mít v globálním poli $user. Obsahuje řádek z tabulky AUTH_USERS. Jestliže není nikdo přihlášený bude tato proměnná prázdná.

Na dalším řádku vytvoříme, rovněž globálně, objekt $app. (Konstanta APPNAME je název aplikace: APPNAME = 'taskman'). Tento objekt se stará zejména o adresování jednotlivých operací aplikace, uživatelskou navigaci v programu, správu chyb a další.

Mimo jiné načítá při svém vytvoření globální šablonu aplikace, což je implicitně 'tpl/website.tpl' (lze to změnit).
Tato šablona je jakási obálka, do níž se vkládá další obsah. Většina webů má základní layout stejný a hlavička, patička apod. se příliš nemění.
Ke globální šabloně můžeme přistupovat prostřednictvím $app->tpl, což je normální objekt třídy tpl. Šablona může vypadat zhruba takto:

<br>
< ?
elements<br>
link LOGOUT route "uac/logout"<br>
? ><
br>
<!-- 
soubor website.tpl --><br>
<
html><br>
<
head><br>
<
title>Taskman</title><br>
<
link rel="stylesheet" href="taskman.css" type="text/css"><br>
<
script src="taskman.js" language="JavaScript"></script><br>
</head><br>
<body><br>
<div class="HEADER"><br>
Taskman | Přihlášený uživatel: {UNAME}<br>
</div><br>
<div class="MENU">{MENU}</div><br>
<div class="NAVIG">{NAVIG}</div><br>
<div class="CONTENT">{PRECONTENT}{CONTENT}</div><br>
<div class="FOOTER">Taskman version {VERSION} | {LOGOUT}</div><br>
</body><br>
</html><br>
 


Je to klasická šablona, hodnoty do ní vkládáme v index.php hned po vytvoření objektu $app. Tuto sekci můžete zcela vynechat, nebo si přidat vlastní parametry do šablony podle libosti. Obsahuje to, co je společné všem modulům a víceméně se to nemění. Vlastní obsah, vytvářený moduly, se bude vkládat do proměnné {CONTENT} a to v okamžiku zobrazení výstupu, což je až na konci index.php.

Možná vás zaujaly proměnné {MENU} a {NAVIG} - třída app umožòuje vytvářet aplikační menu (generuje ho jako stylovaný seznam <UL><LI> z databázové tabulky MENU) a takzvanou breadcrumb navigaci, ale k podrobnějšímu popisu se v tomto tutoriálu zřejmě nedostaneme. Příslušné dva řádky v index.php (s $app->getmenu() a $app->getnavig()) lze prostě vynechat a vytvořit si nějakou navigaci vlastnoručně.
Následně se dostáváme k rozcestníku modulů, který se větví podle proměnné $app->modul.
Odkud se tato proměnná bere?
Třída app používá k adresování akcí, které má program provést, takzvanou cestu (route), která má v základním tvaru podobu "modul/akce/id".

Příklady:


modul: projects
akce: add (založení nového projektu)
route: "projects/add"

modul: projects
akce: delete (smazání projektu)
id: 4 (ID projektu ke smazání v tabulce PROJECTS)
route: "projects/delete/4"

modul: uac
akce: login (přihlášení uživatele)
route: "uac/login"


Proměnné modul, action a id se standardně předávají v url, takže url smazání projektu bude mít podobu "index.php?modul=projects&action=delete&id=4". Objekt $app si při své inicializaci tyto údaje z url načte do proměnných $app->modul, $app->action a $app->id.
Všimněte si, že v šabloně website.tpl je odkaz pro odhlášení vytvořen pomocí route.
Všude, kde se v šablonách používá adresování pomocí url, lze použít tento atribut route, který zapisuje adresu způsobem podobným cestě v adresářové struktuře.

* Pozn: Pomocí souboru .htaccess můžeme vytvořit i "nice url" a zadávat pak cestu v podobě modul/akce/id i do adresního řádku prohlížeče. V budoucnu bude třída app zřejmě podporovat pohodlnější nastavení "nice url".

Na konci souboru index.php je kromě načtení breadcrumb navigátoru (lze vynechat)
jen příkaz $app->out(), který zobrazí kompletní html stránku. Do tohoto okamžiku se v celém programu nic nevypisuje.
Výjjimkou je případ zadání chybného jména modulu, v kterémžto případě se volá funkce $app->error(), která ihned ukončí program a zobrazí html s chybovým hlášením.
Lze použít i funkci $app->warning(), které pokračuje v provádění programu a varovné hlášení je vloženo do šablony jako {PRECONTENT}, takže se zobrazí nad vlastním obsahem stránky.

Nyní se podíváme podrobněji na jeden modul, např. projects, jelikož ostatní
moduly budou řešeny velmi podobně.

Modul projects musí implementovat následující akce:
- list : Zobrazení seznamu projektů
- add : Zobrazení přidávacího formuláře (nový projekt)
- edit : Editace konkrétního projektu (vyžaduje id projektu)
- insert : Vložení projektu do databáze
- update : Aktualizace projektu v databázi
- delete : Smazání projektu v databázi

<br>
* \file projects.php<br>
 * \version taskman 0.0<br>
 */
<br>
<
br>
$auth->testright('taskman/projects/enter');<br>
$app->bookmark(1'Projekty''projects');<br>
<
br>
switch (
$app->action) {<br>
<
br>
  
/* Zde bude vlastní zpracování jednotlivých akcí */<br>
<
br>
}<
br>
<
br>
//konec projects.php<br>
<br>
 


Na prvním řádku provedeme test, zda má přihlášený uživatel oprávnění vstupu do modulu projects. Jesliže ne, tato fukce inhned ukončí program s chybovým hlášením. Následující funkce slouží k vytváření breadcrumb navigace (lze vynechat, tuto funkci můžete vypustit i všude dál v příkladech) Dál už je jen větvení podle zvolené akce, jednotlivé větve si ukážeme zvl᚝.
V případě, že nebude zadaná žádná akce, budeme chtít provést defaultní akci, to jest výpis seznamu projektů. Seznam projektů je prostě grid, takže:

<br>
//Akce 'list'<br>
<br>
case 
'list':<br>
default:<
br>
  
$plist = new grid('tpl/prjlist.tpl''plist');<br>
  
$plist->setquery('select * from PROJECTS');<br>
  
$app->content $plist->html();<br>
break;<
br>
 


Je to klasický grid, v reálné aplikaci by patrně přibylo nějaké vyhledávání a filtr na projekty, které uživatel vidí, což z důvodu stručnosti nebudeme řešit. Zajímavá je jen poslední řádka, kde nastavujeme proměnnou $app->content. Vše co obsahuje tato proměnná, bude nakonec vloženo do proměnné master šablony {CONTENT} a zobrazeno na posledním řádku souboru index.php. Zde vložíme html kód gridu.

Obdobně postupujeme při zobrazení přidávacího a editačního formuláře. Tyto dvě akce používají stejnou šablonu formuláře (prjform.tpl). V této šabloně máme tři tlačítka na přidání,aktualizaci a smazání projektu, jménem {insert},{update}, {delete}. (Všimněte si, že identifikátor odpovídá příslušným názvům akce) Tato tlačítka jsou v šabloně nastavená jako neviditelná (s atributem noprint) a zobrazíme vždy jen některá z nich, podle oprávnění uživatele a zvolené akce.
Náčrt (neúplný) šablony formuláře:

<br>
form name "pform" route "projects/{GET.id}"<br>
input NAZEV required<br>
(...)<
br>
button insert lb "Přidat" noprint<br>
button update lb "Uložit" noprint<br>
button delete lb "Smazat" noprint<br>
<br>
(...)<br>
 


Po kliknutí na tlačítko např. "Uložit" se vygeneruje routa "projects/update/4" (za předpokladu, že byl formulář otevřen s id projektu = 4)

*Pozn: Všimněte si parametru {GET.id} v route formuláře. Tento příkaz převezme proměnnou id z url a vloží ji do action formuláře, takže id projektu nám zůstane k dispozici i po odeslání formuláře. Jinak bychom k protlačení id skrz formulář museli přidat pole input ID hidden.

<br>
//Akce 'add' (Zobrazeni formulare pridani projektu)<br>
case 'add':<br>
  
$app->bookmark(2'Nový projekt');<br>
<
br>
  
$pform = new form ('tpl/prjform.tpl''pform');<br>
  if (
$auth->hasright('taskman/projects/add'))<br>
    
$pform->enable('insert');<br>
  
$app->content $pform->html();<br>
break;<
br>
<
br>
//Akce 'edit' (Zobrazeni formulare editace projektu)<br>
case 'edit':<br>
  
$app->bookmark(2'Editace projektu');<br>
<
br>
  
$pform = new form ('tpl/prjform.tpl''pform');<br>
<
br>
  if (
$auth->hasright('taskman/projects/edit'))<br>
    
$pform->enable('update');<br>
  if (
$auth->hasright('taskman/projects/delete'))<br>
    
$pform->enable('delete');<br>
<
br>
  
$pform->values $db->select('PROJECTS'pri($app->id));<br>
<
br>
  
$app->content $pform->html();<br>
break;<
br>
 


Vidíme, že na základě toho, zda má uživatel oprávnění projects/add, resp. edit, delete zobrazujeme příslušná tlačítka formuláře. Editačního formulář před zobrazením naplníme dotazem z databáze. Nakonec vložíme html do {CONTENT}.

Po odeslání formuláře některým z tlačítek se volá akce insert, resp. update, delete.

<br>
//Akce 'insert' (Vlozeni noveho projektu do tabulky PROJECTS)<br>
case 'insert':<br>
  
$auth->testright('taskman/projects/add');<br>
<
br>
  
$pform = new form ('tpl/prjform.tpl''pform');<br>
  if (!
$pform->validate()) $auth->redirect('projects/add');<br>
<
br>
  
$pform->values['OWNER_ID'] = $user['ID'];<br>
  
$pform->values['LASTMOD']  = date('Y-m-d H:i:s');<br>
  
$pform->values['DT']       = date('Y-m-d H:i:s');<br>
<
br>
  
$id $pform->insert('PROJECTS');<br>
<
br>
  
$pform->deletesession();<br>
<
br>
  
$app->redirect('projects/edit/$id/s:1');<br>
break;<
br>
 


Tato akce vloží řádek s uživatelem vyplněnými daty do tabulky PROJECTS. Je dobrým zvykem provádět aktualizaci databáze v samostatné akci a ukončit ji přesměrováním, takže nedojde k zápisu do historie prohlížeče. K přesměrování používáme funkci $app->redirect($route). Podívejme se na akci řádek po řádku.

Nejprve otestujeme, zda má uživatel právo přidávat projekty. Pokud ne, ihned se ukončí program s chybou. Připomínám, že uživatel bez práva přidávat by se vůbec neměl do této akce dostat - nezobrazí se mu vůbec tlačítko "Přidat" - selhání testright() obyčejně znamená, že se děje něco nekalého /např. pokus o hack?/. Proto třída auth umožòuje zapsat selhání testright() do logu jako bezpečnostní varování.
V dalším kroku vytvoříme formulář a otestujeme, zda jsou data vložená uživatelem validní (vyplněná povinná pole ap.) Když ne, vracíme ho zpátky na akci přidání formuláře. (Formulář je uložený v session, proto zůstanou zapamatovány vyplněné hodnoty a uživateli se zobrazí chybová hlášení (v proměnné {errors} nebo {POLE.err}) Jestliže je vše OK, pokračujeme doplněním požadovaných hodnot, tj. ID uživatele, který projekt vytvořil, datum vytvoření a poslední aktualizace. Pokračujeme vložením formuláře do databáze, funkce vrátí ID vloženého záznamu (pole ID musí být autoinkrementální)
* Pozn: Mohli bychom provádět další kontroly, např. zda nedošlo k chybě při zápisu do databáze, / if ($db->errors) $app->error(0, $db->errors); / ale v praxi k takovým chybám nedochází a myslím, že si můžeme dovolit být poněkud lehkomyslní. U programů, kde je nutná spolehlivost i za extrémních okolností (dejme tomu účetnictví) by byl nutný opatrnější přístup a patrně použití databázových transakcí.

Nakonec vymažeme formulář ze session a přesměrujeme na zobrazení editačního formuláře s právě přidaným projektem. Mohli bychom pochopitelně po přidání přesměrovat i jinam, třeba na seznam projeků: "projects/list/s:1" - jak je libo.
Jistě jste si všimli kódu /s:1 v route - tento kód jednoduše přidá na konec url proměnnou ...&s=1 a používáme ho k informaci, že právě proběhlo uložení (save) formuláře.Můžeme pak na základě toho např. vypsat uživateli hlášku, že akce proběhla v pořádku.
Třeba přidáním této řádky do akce 'edit':

if ($_GET['s']) $pform->enable('b_save');

<br>
//Akce 'update' (Aktualizace projektu v tabulce PROJECTS)<br>
case 'update':<br>
  
$auth->testright('taskman/projects/edit');<br>
  if (!
$app->id$app->error(0'Nezadané číslo projektu');<br>
<
br>
  
$pform = new form ('tpl/prjform.tpl''pform');<br>
  if (!
$pform->validate()) $auth->redirect('projects/edit/'.$app->id);<br>
<
br>
  
$pform->values['LASTMOD']  = date('Y-m-d H:i:s');<br>
  
$id $pform->update('PROJECTS'pri($app->id));<br>
<
br>
  
$pform->deletesession();<br>
<
br>
  
$app->redirect('projects/edit/$app->id/s:1');<br>
break;<
br>
 


Není asi co dodat, akce je stejná jako insert. Při vstupu do ní vyžadujeme zadané id projektu ($app->id). Poznamenejme, že řídí proměnné $app->modul, $app->action, $app->id (další řídící proměnné lze získat příkazem $app->ident('jmeno_prom');) připouštějí pouze omezenou množinu znaků - tzv. pcl_ident, takže $app->id je bezpečné pro použití v dotazech a neumožòuje sql-injection.
<br>
//Akce 'delete' (Smazání projektu v tabulce PROJECTS)<br>
case 'delete':<br>
  
$auth->testright('taskman/projects/delete');<br>
  if (!
$app->id$app->error(0'Nezadané číslo projektu');<br>
  
$pform = new form ('tpl/prjform.tpl''pform');<br>
  
$pform->delete('PROJECTS'pri($app->id));<br>
  
$pform->deletesession();<br>
  
$app->redirect('projects/list/s:1');<br>
break;<
br>
 


Pochopitelně místo $pform->delete() bychom mohli zavolat $db->delete('PROJECTS', pri($app->id)) nebo i $db->query(...) a výsledek by byl stejný. Delete formuláře se liší pouze tehdy, pokud ve formuláři nahráváte nějaké soubory - tyto soubory jsou pak vymazány spolu se záznamem v databázi.
---

Další moduly by se řešily zcela analogicky, pouze u tasks musíme (při založení) předávat dodatečnou řídící proměnnou project_id, udávající do kterého projektu patří.

Modul uac implementuje akce 'login', 'logout' a 'welcome', což je defaultní akce, která zobrazí uvítací hlášení a přihlašovací formulář. Tento formulář se odešle do akce 'login', kde se zavolá funkce $auth->login(). Obdobně 'logout' volá funkci $auth->logout();

Tím tento, už beztak dlouhý, tutoriál ukončíme a pokud budete mít nějaké dotazy,
ptejte se v guestbooku!

« zpět