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>
require_once ('config.php');<br>
<br>
require_once ('pclib/pcl.php');<br>
require_once ('pclib/db.php');<br>
<br>
<br>
$db = new db(CONNECTION_STR);<br>
$auth = new auth(APPNAME);<br>
$user = $auth->getuser();<br>
<br>
$app = new app(APPNAME);<br>
<br>
<br>
$app->tpl->values['MENU'] = $app->getmenu();<br>
$app->tpl->values['VERSION'] = VERSION_STR;<br>
$app->tpl->values['UNAME'] = $user['FULLNAME'];<br>
<br>
if (!$app->modul) $app->modul = 'uac';<br>
<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>
} <br>
$app->tpl->values['NAVIG'] = $app->getnavig();<br>
<br>
$app->out();<br>
<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>
<br>
<br>
}<br>
<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>
<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>
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>
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>
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>
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>
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 |