Univerzita Karlova v Praze Matematicko-fyzikální fakulta
BAKALÁŘSKÁ PRÁCE
Tomáš Kafka Webový systém pro prodej předplacených služeb Katedra softwarového inženýrství Vedoucí bakalářské práce: RNDr. Jan Kofroň
Studijní program: Informatika, Programování 2008
Na tomto místě bych rád poděkoval vedoucímu práce RNDr. Janu Kofroňovi za vstřícný přístup a konzultace. Dále děkuji za podporu a trpělivost své přítelkyni i rodině.
Prohlašuji, že jsem svou bakalářskou práci napsal samostatně a výhradně s použitím citovaných pramenů. Souhlasím se zapůjčováním práce. V Praze dne 28. 5. 2008
Tomáš Kafka
2
Obsah 1
Úvod............................................................................................................................................... 6 1.1 1.2
2
Návrh ............................................................................................................................................. 7 2.1 2.2
3
Platforma ............................................................................................................................. 12 Architektura ........................................................................................................................ 12 Knihovna pro formuláře – MyForm ................................................................................ 15 Objektový wrapper pro tabulku – MyTable ................................................................... 20 Internacionalizace a lokalizace ........................................................................................ 21 Stavové informace – SessionData .................................................................................... 24 Platba.................................................................................................................................... 25 Testování ............................................................................................................................. 26 XHTML a přístupnost......................................................................................................... 28 Popis databáze .................................................................................................................... 30
ORM vrstva ................................................................................................................................. 34 4.1 4.2 4.3
5
Diagramy uživatelské interakce: Uživatel ........................................................................ 8 Diagramy uživatelské interakce: Administrátor ........................................................... 11
Implementace ............................................................................................................................ 12 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10
4
Motivace ................................................................................................................................ 6 Cíle .......................................................................................................................................... 6
MyActiveRecord vs. Propel ............................................................................................... 34 Konvence MyActiveRecord ORM vrstvy ......................................................................... 37 ORM a výkon ....................................................................................................................... 40
Závěr ............................................................................................................................................ 45 5.1 5.2 5.3 5.4
Shrnutí ................................................................................................................................. 45 Splnění cílů .......................................................................................................................... 45 Použité nástroje .................................................................................................................. 46 Použité knihovny třetích stran ........................................................................................ 47
3
Seznam ilustrací Ilustrace 1 Prodloužení a vznik nové registrace ............................................................................. 7 Ilustrace 2 Registrace a přihlášení ..................................................................................................... 8 Ilustrace 3 Seznam plateb a kupóny .................................................................................................. 9 Ilustrace 4 Výběr a placení služby ................................................................................................... 10 Ilustrace 5 Schéma administračního rozhraní .............................................................................. 11 Ilustrace 6 Legenda diagramů .......................................................................................................... 11 Ilustrace 7 Sequence diagram zpracování požadavku.................................................................. 14 Ilustrace 8 Příklad výstupu formulářové knihovny ...................................................................... 15 Ilustrace 9 Ukázka výstupu třídy MyTable..................................................................................... 20 Ilustrace 10 Ukázka výstupu třídy MyTable s omezením zobrazených sloupců ...................... 20 Ilustrace 11 Ukázka výstupu jako kartiček ..................................................................................... 21 Ilustrace 12 Podmnožina databázového schematu pro lokalizaci .............................................. 22 Ilustrace 13 Schéma platby přes Paypal ......................................................................................... 25 Ilustrace 14 Selenium IDE pro vytváření testů s probíhajícím testem....................................... 27 Ilustrace 15 Selenium Test Runner - javascriptová knihovna pro běh testů ............................ 27 Ilustrace 16 Zobrazení v plnohodnotném prohlížeči ................................................................... 28 Ilustrace 17 Zobrazení v prohlížeči Opera pro mobilní telefony ................................................ 29 Ilustrace 18 Zobrazení v témže prohlížeči bez CSS stylů ............................................................. 29 Ilustrace 19 Logické databázové schéma aplikace ........................................................................ 30 Ilustrace 20 Fyzické schéma databáze a integritní omezení ....................................................... 31 Ilustrace 21 Diagram tříd ORM vrstvy............................................................................................. 40 Ilustrace 22 Vlastní implementace joinu ........................................................................................ 41 Ilustrace 23 Příkazy nahrávající z databáze více než jeden objekt............................................. 42 Ilustrace 24 Příklad cachování dotazů ............................................................................................ 42 Ilustrace 25 Objektový graf výsledku dotazu (šipky představují reference na objekty) ......... 43
4
Název práce: Webový systém pro prodej předplacených služeb Autor: Tomáš Kafka (
[email protected]) Katedra (ústav): Katedra softwarového inženýrství Vedoucí bakalářské práce: RNDr. Jan Kofroň e-mail vedoucího:
[email protected] Abstrakt: Software jako služba je moderní způsob monetizace software, kde aplikace je hostována poskytovatelem a zpoplatněna měsíčními poplatky. Vyvinuli jsme platformu pro prodej předplacených služeb po internetu, jako webovou aplikaci na LAMP (Linux, Apache, MySQL, PHP 5) serveru. Dále byly vytvořeny znovupoužitelné objektové komponenty pro tvorbu, příjem a validaci formulářů a pro zacházení s tabulkami. Aplikace je internacionalizována a umožňuje i různé cenové plány pro různé země. Nakonec jsme analyzovali výkonnostní dopad použití ORM frameworků, porovnali dva současné PHP ORM frameworky, vybrali MyActiveRecord jako vhodnější pro práci, a benchmarkovali ho, abychom zjistili vliv SQL cachování na počet databázových dotazů. Naměřili jsme 29% zmenšení počtu dotazů. Klíčová slova: PHP, formuláře, ORM, výkon, počet SQL dotazů
Title: Web system for sale of prepaid services Author: Tomáš Kafka (
[email protected]) Department: Department of Software Engineering Supervisor: RNDr. Jan Kofroň Supervisor's e-mail address:
[email protected] Abstract: Software as a service is a modern model of software deployment where application is hosted as service and charged monthly. We have developed a platform for selling software service over the internet as a web application, working on LAMP (Linux, Apache, MySQL, PHP 5) server stack. Several reusable components were created to facilitate rendering and handling of forms and tables and wrap them with object API. Application is completely internationalized allowing even different price plans for different countries. Afterwards we analyzed the impact of ORM frameworks on application performance and compared two contemporary PHP ORM frameworks, choosing MyActiveRecord as more suitable, and benchmarked it in order to find the influence of using SQL query cache on minimization of number of database requests. A 29 % decrease of queries was measured. Keywords: PHP, forms, ORM, performance, SQL query count
5
1 Úvod 1.1 Motivace V dnešní době se mnoho komerčních internetových subjektů rozhoduje odstoupit od tradičního prodeje krabicového softwaru, placeného jednorázově, k prodeji softwaru jako služby, placené v pravidelných intervalech. Mimo jiné i proto, že čím dál tím více oblíbené internetové služby přinášejí nemalé provozní náklady, ať už na běh a konektivitu serverů, či na tým starající se o běh služby a její zlepšování. Trh se softwarem jako službou se tedy liší od trhu s krabicovým softwarem: V tom druhém se obvykle počítá s fixními náklady na vývoj, a poté, co firma produkt prodá, začne buď vyvíjet novou verzi (pokud si myslí, že o ni bude také zájem), anebo se vrhne na jiný projekt a u stávajícího poskytuje jen podporu a opravy chyb. Software prodávaný jako (webová) služba naopak, při dostatečně velké uživatelské základně, může průběžně financovat svůj vlastní vývoj, a zároveň umožňuje implementovat změny a přidávat nové funkce téměř v reálném čase. Jak říká i samotný název práce, věnoval jsem se implementaci internetového obchodu pro prodej softwaru jako služby.
1.2 Cíle Cíle této práce byly následující: • • •
Implementovat internetový obchod pro prodej softwaru jako služby. Vyvinout znovupoužitelné třídy užitečné i pro další webové aplikace. Analyzovat přínosy a negativa použití ORM vrstvy a změřit vliv cachování databázových dotazů.
Kapitola druhá se zabývá popisem požadované funkčnosti a návrhem uživatelského rozhraní. Kapitola třetí popisuje architekturu a implementaci práce a vytvořené znovupoužitelné knihovny. V kapitole čtvrté pak hodnotíme existující ORM vrstvy pro PHP, popisujeme vybranou vrstvu MyActiveRecord a měříme vliv cachování SQL dotazů.
6
2 Návrh Aplikace umožňuje provozovateli prodávat software jako službu, klientovi pak umožňuje si tyto služby objednávat a platit za ně. Jako účtovací jednotka byl vybrán kalendářní měsíc, předplatné ale může začít v jeho libovolný den. Registrace je interval, kdy má uživatel službu předplacenou, pokud si během tohoto intervalu dokoupí další časové období, registrace se prodlouží. To pak umožňuje zasílat např. e-maily o blížícím se ukončení předplatného jen jedenkrát před úplným vypršením předplatného.
Ilustrace 1 Prodloužení a vznik nové registrace
Uživatelské rozhraní Aplikace má dvě části, část klientskou a část pro administrátory systému. Do klientské části se lze přihlásit až po zaregistrování, přičemž registrace vyžaduje potvrzení e-mailové adresy (jako obrana proti zahlcení systému roboty). Po přihlášení vidí klient dostupné služby a to, zda je má objednané, a pod nimi je seznam realizovaných plateb – a to jak plateb které čekají na zaplacení, tak i plateb úspěšně zaplacených a fakturovaných. Administrátor pak může po přihlášení označit platbu jako zaplacenou (například platil-li klient přes bankovní účet), prohlásit platbu za nefakturovanou (například testovací účty pro recenzenty), smazat, či si zobrazit fakturu. Aplikace mu též zobrazuje statistiky. Podoba rozhraní byla odvozena z následujících diagramů:
7
2.1 Diagramy uživatelské interakce: interakce: Uživatel
Ilustrace 2 Registrace a přihlášení
8
Ilustrace 3 Seznam plateb a kupóny
9
Výběr služby
subscribe.php
Zobrazí se služby a jejich tarify
checkout.php Zaplatit
Vybrat způsob platby
Přidat službu do košíku DB: typ platby se změní z košík na objednáno
Potvrdit platbu DB: obsah košíku je neustále v databázi Způsob placení
account
PP
platba u PayPalu
Zákazník zaplatí na účet
Administrátor označí platbu jako zaplacenou
DB: typ platby se změní z objednáno na zaplaceno
paypal_confirm.php
Návrat na homepage
Ilustrace 4 Výběr a placení služby
10
2.2 Diagramy uživatelské interakce: interakce: Administrátor
Ilustrace 5 Schéma administračního rozhraní
Legenda
Ilustrace 6 Legenda diagramů
API pro prodávanou službu Aplikace obsahuje jednoduché API, které umožňuje přes http protokol ověřit, zda má uživatel službu momentálně předplacenu. Dotaz (HTTP GET): api.php?login=jmeno&pass=heslo&service=sl_mfdnes&query=registration_active Odpověď: 1 či 0
11
3 Implementace 3.1 Platforma Pro implementaci práce byla vybrána oblíbená platforma LAMP – Linux, Apache, MySQL a PHP, a to z důvodu nulových nákladů (veškerý software je open source), dobré dostupnosti (tato čtveřice je dostupná téměř na každém hostingu) a výborné podpory (o této platformě je na internetu dostupné obrovské množství informací).
Framework, nebo vlastní řešení? Pro vývoj webových aplikací se nabízí mnoho hotových frameworků, které umožňují ulehčit si práci, a programovat jen tu funkčnost, která je pro naši aplikaci specifická. Na druhé straně jsou ale často příliš těžkopádné pro menší aplikace a nutí své uživatele (programátory) přizpůsobit se svým konvencím. V neposlední řadě je zde problém s tím, že frameworků je mnoho, a každý se snaží prezentovat sám sebe jako kompletní řešení, a pokud v polovině vývoje zjistíme, že nám framework nevyhovuje, nezbude nám, než aplikaci přepsat do nějakého jiného. V takovém případě nám jsou znalosti získané z původního frameworku k ničemu, a musíme se učit nové API a konvence odznovu. V případě této práce byl dalším argumentem proti hotovému frameworku stav PHP platformy – na jaře roku 2007 velká řada hostingů z důvodu částečné nekompatibility poskytovala jen PHP verze 4, které prakticky nemělo podporu objektů, a PHP 5 bylo relativně mladé a jeho implementace objektů obsahovala chyby. Objektově napsané frameworky byly tou dobou většinou mladé, ve verzích nižších, než jedna, a často jim chyběla nějaká základní funkčnost. Nebylo také jasné, který framework má budoucnost, a který zanikne bez podpory. Je tedy pochopitelné, že jsem se nakonec rozhodl implementovat práci 'na zelené louce' a vypomoci si případně jen menšími specializovanými třídami, které dělají dobře jednu věc. Tento přístup byl navíc výhodný ze studijních důvodů, neboť pomohl k lepšímu náhledu na stávající frameworky.
3.2 Architektura Rozhodli jsme se použít architekturu Separated Presentation (1) – přístup podobný používanému modelu MVC – Model, View, Controller (2): This pattern is a form of layering, where we keep presentation code and domain code in separate layers with the domain code unaware of presentation code. This style came into vogue with Model-View-Controller architecture and is widely used. (1)
12
Tímto přístupem tedy pomocí objektů ORM vrstvy (a řádků v databázi) modelujeme objekty z reálného světa (platba, uživatel, kupón, cena, registrace), a jejich metodami vyjadřujeme vztahy mezi objekty a služby, které nabízejí svému okolí. Objekty přitom nic nevědí o prezentační vrstvě. Controller a View z MVC architektury jsou zde pohromadě v jednom souboru a to kvůli zjednodušení vývoje: V práci totiž každý PHP soubor v adresáři aplikace (tedy každé volané URL) odpovídá jedné podmnožině funkčnosti (například subscribe.php a plnění košíku službami), a tím tedy odpovídá jednomu Controlleru, a navíc, po zpracování vstupu vykreslí svůj výstup, který je také pro danou podmnožinu funkčnosti specifický – tím zase odpovídá jednomu pohledu (View). Domníváme se, že úplně oddělení View a Controlleru by jen zdvojnásobilo počet souborů aplikace a přidalo nutnost implementace routingu (3) – mapování volaných URL na jednotlivé controllery. Každý skript tedy vypadá takto – viz Ilustrace 7: • • • •
Na začátku se includují potřebné knihovny. Pak se ověří, zda je uživatel přihlášen (v případě že stránka nemá být veřejně přístupná). To mají na starosti metody SD::testLogin(), případně SD::testAdmin(). Následně se zpracuje uživatelův vstup a upraví Model (pokud například volá URL se smazáním položky z košíku, smažeme ji) – toto je Controller. Poté se vykreslí stránka – View: o Zavolá se funkce write_header() ze souboru commonheader.php. Ta zároveň zaregistruje write_footer() jako shutdown function – funkci, kterou PHP zavolá po ukončení skriptu. o Vykreslí se tělo stránky z aktuálního stavu Modelu (tedy například košík bez smazané položky) o Pokud bylo vypsáno záhlaví, vypíše se nyní zápatí a stránka se odešle klientovi.
13
Ilustrace 7 Sequence diagram zpracování požadavku
Uspořádání souborů: / /config /config.php /res /css /graphic /lang /inc /includes.php /commonlayout.php /multilingual.php /class /MyActiveRecord /lib
PHP soubory, jejichž URL přímo volá uživatel Konfigurační soubor aplikace – obsahuje veškerá nastavení aplikace Adresář se všemi pomocnými soubory: CSS styly aplikace veškerá grafika *.ini soubory s překlady textů uživatelského rozhraní PHP soubory s globálními funkcemi Všechny potřebné include direktivy HTML šablona Globální funkce pro překlad textů Veškeré funkční třídy aplikace Třídy ORM modelu Knihovny třetích stran
14
3.3 Knihovna pro formuláře – MyForm Nedílnou součástí každé webové aplikace jsou formuláře, a tak jsem se rozhodl implementovat znovupoužitelnou knihovnu pro nakládání s nimi. Tato knihovna je v souboru souboru res/inc/class/MyForm.php. MyForm je třída, která abstrahuje veškerou práci s formuláři, formuláři od definice až po naplnění daty od uživatele, validaci a vrácení hodnot. Každá položka má své pořadí, díky čemuž je možné sestavovat formulář v jiném pořadí, než ve ve kterém budou pole nakonec zobrazena. Jako parametry metod jsou použita asociativní pole – univerzální náhrada polí, kontejnerů a hashmap přítomná v PHP. Tento přístup je v programech v PHP obvyklý, jsou to vlastně pojmenované parametry, které mohou být uvedeny v libovolném pořadí, či úplně vynechány, pokud ve volané funkci byly definovány výchozí hodnoty.
MyForm +MyForm MyForm() +addProperty addProperty() +addField addField() +changeField changeField() +fieldExists fieldExists() +addChecks addChecks() +addOptions addOptions() +loadFromRequest loadFromRequest() () +loadFromAssocArray loadFromAssocArray loadFromAssocArray() +generateAssocArray generateAssocArray generateAssocArray() +clearAllRadioFields clearAllRadioFields() +validate validate() +isReceived isReceived() +didValidate didValidate() +getErrorList getErrorList() +addError addError() +setValue setValue() +generateForm generateForm() +getValues getValues() +getValues getValues() +getValuesSQL getValuesSQL() +generateNameValueArray generateNameValueArray generateNameValueArray() +generateNameValueArraySQL generateNameValueArraySQL generateNameValueArraySQL() +generateNameValueLog generateNameValueLog generateNameValueLog()
Myslím, že jde o dobrý přístup, jistým ujištěním je i fakt, že knihovna Zend_Form (4) asi největšího PHP frameworku Zend Framework, Framework, která vyšla v březnu 2008, používá téměř stejnou volací konvenci. konvenci
Příklad použití: register.php):: použití: registrační formulář (upraveno z register.php)
Ilustrace 8 Příklad výstupu formulářové knihovny
Nejdříve vytvoříme objekt formuláře: formulář : $form = new MyForm();
Můžeme přidat některé atributy celému formuláři – metodu odeslání, cíl, CSS třídu, styl, id, formid (id formuláře pro odlišení více formulářů na stránce):
15
$form->addProperty( array('class' => 'form_reg', 'method' => 'post', 'target' => 'login.php' ));
A pak už přidáváme jednotlivé položky: Spacer – udělá mezeru mezi položkami a vizuálně tak oddělí související bloky, pokud mu přidáme jméno (name), stane se nadpisem $form->addField( array( 'id' => 'spacer0', 'type'=> 'spacer', 'order' => 0, 'name' => l('reg.Required:') ));
Input – textové pole: bude povinné, omezíme jeho délku, přidáme mu název (zobrazí se před políčkem) a komentář – menším písmem za polem: $form->addField( array( 'id' => 'login', 'order' => 2 , 'name' => l('reg.Login'), 'type' => 'input', 'required' => true, 'comment' => l('reg.This will be your user name'), 'value' => '', 'max_length' => 60 ));
Položce formuláře také můžeme přidat validátory, které zkontrolují platnost uživatelem zadané hodnoty – v tomto případě použijeme validátor délky a alfanumeričnosti: $form->addChecks( 'login', array( 'isLong', 'isAlNum' ) );
Přidáme další pole pro e-mail, a dáme mu validátor emailové adresy: $form->addField( array( 'id' => 'email', 'order' => 4, 'name' => l('reg.E-mail'), 'required' => true, 'comment' => l('reg.Please enter valid e-mail address'), 'max_length' => 200 )); $form->addChecks( 'email', array( 'isEmail' ) );
Přidáme další spacer, tentokráte bez nadpisu. Atribut order nám umožňuje nadefinovat formulář i v jiném pořadí, než v jakém chceme mít položky na stránce, výsledná pole se totiž před vykreslením seřadí podle tohoto atributu. $form->addField( array( 'id' => 'spacer2', 'type'=> 'spacer', 'order' => 5, 'name' => '' ));
Password – přidáme pole pro heslo, opět musí být alfanumerické a dostatečně dlouhé. $form->addField( array( 'id' => 'heslo', 'order' => 6, 'name' => l('reg.Password'), 'required' => true, 'type'=>'password', 'value' => '', 'comment' => l('reg.Your password (at least 6 characters long)'), 'max_length' => 200)); $form->addChecks( 'heslo', array( 'isLong', 'isAlNum' ) );
16
Potvrzení hesla – validátory podporují parametry, tady řekneme, že potvrzení nesmí být prázdné, a musí být stejné, jako první heslo – odkážeme se na něj podle id. $form->addField( array( 'id' => 'heslo2', 'order' => 8, 'name' => l('reg.Password confirmation'), 'required' => true, 'type'=>'password', 'value' => '' , 'comment' => l('reg.Confirm your password by entering it again'), 'max_length' => 200)); $form->addChecks( 'heslo2', array( 'notEmpty', 'isSameAs', 'sameAs' => 'heslo' ) );
Spacer a submit – tlačítko pro odeslání: $form->addField(array( 'id' => 'spacer3', 'type'=> 'spacer', 'order' => 55 )); $form->addField( array( 'id' => 'confirm', 'type'=> 'submit', 'value' => l('reg.Register me'), 'order' => 60 ));
Nyní už máme nadefinovaný formulář, nahrajeme tedy jeho hodnoty z requestu (tím se přepíšou výchozí hodnoty): $form->loadFromRequest();
Nyní je třeba formulář zvalidovat – ověřit, že všechna pole vyhovují svým validátorům. To zařídí metoda validate, která zároveň přidá k polím nalezené chyby: $form->validate();
A toto je doporučené použití: metoda isReceived() vrátí true, pokud byl formulář odeslán uživatelem (při prvním použití tedy vrátí false, protože jsme formulář jen vykreslili, ale jeho hodnoty zatím uživatel nezměnil). Metoda didValidate() vrátí true, pokud validace nenašla žádnou chybu. if (($form->isReceived()) && ($form->didValidate())) {
Zde už víme, že můžeme přistupovat ke všem položkám formuláře podle id: $login = $form->getValue('login');
Jejich hodnoty můžeme získat i escapované, pro bezpečné vložení do SQL dotazů: $login_to_db = $form->getValueSQL('login'); } else {
Vygenerujeme HTML pro formulář (včetně případných chybových hlášek) a vypíšeme ho echo $form->generateForm(); }
17
Příklad použití: spolupráce s ORM vrstvou Častou situací je případ, kdy požíváme formulář k editaci objektu z databáze, v takovém případě je 1:1 vztah mezi formuláři a tabulkami. Snažil jsem se proto toto použití maximálně zjednodušit spoluprací formulářové knihovny a ORM vrstvy: Nadefinujeme formulář, kde id polí odpovídají názvům sloupců v databázi: $form = ...
Pomocí ORM vrstvy vytvoříme objekt uživatele a naplníme ho z databáze: $user = Uzivatel::FindByLogin("testuser");
Nyní lze formulář naplnit položkami vytvořeného objektu (přetypováním objektu na array dostaneme v php asociativní array, např. z user->jmeno bude user['jmeno'] a tak naplníme pole formuláře s id='jmeno'): $form->loadFromAssocArray( (array) $user );
Pak naplníme formulář z requestu (pokud uživatel odeslal formulář, přepíší se výchozí hodnoty) a zvalidujeme: $form->loadFromRequest(); $form->validate();
Pokud je validní… if (($form->isReceived()) && ($form->didValidate())) {
…naplníme uživatele – potomka ORM třídy díky metodě populate (pole chybějící ve formuláři si zachovají původní hodnotu): $user->populate( $form->generateAssocArray( array('jmeno','prijmeni','adresa_ulice','adresa_ulice_no', 'adresa_mesto','adresa_mesto_cast','adresa_psc', 'adresa_stat','adresa_zeme')));
A aktualizovaná data o uživateli uložíme zpět do databáze: $user->save(); }
Možná vylepšení Aby byla knihovna obecně použitelná například pro diskusní fóra, bylo by v budoucnu vhodné doplnit ochranu proti XSS – Cross site scriptingu (5), spočívající v tom, že by bylo 18
možné definovat filtr jako množinu přípustných tagů, a pak jednotlivým polím s textovým vstupem přiřadit jeden z filtrů. Pro zamezení XSS (přidání javascriptu, který pak může dle svého nakládat s uživatelovými cookies, či s jeho autentizací volat jiné části aplikace, než které uživatel zrovna používá) by pak bylo nutné každému tagu nadefinovat povolené atributy (například href pro a), a všechny ostatní filtrovat (například onLoad). V této práci ale není nikde potřeba zadávat textový vstup s html tagy a tak jsou tyto odstraněny nebo převedeny na entity < a >.
19
3.4 Objektový wrapper pro tabulku – MyTable Stejně jako formuláře, i tabulky jsou častou součástí webových aplikací a je výhodné k nim přistupovat jako k objektům. Proto byla napsána knihovna MyTable, která umožňuje jednoduše sestavovat a vykreslovat tabulky.
Příklad použití: Vytvoříme tabulku a přidáme nadpis:
MyTable +AddCaption() +AddAttribute() +AddAttributes() +GetEmptyRow() +CreateByColumns() +AddRow() +AddRows() +AddClass() +Clear() +ClearClass() +WriteTable() +WriteAsItems()
$table = new MyTable(); $table->AddCaption('Měsíce/Months');
AddAtributes přidá zobrazované sloupce - 'cz'=>'Česky' znamená, že v tomto sloupci budou zobrazena data s klíčem 'cz' a jméno sloupce bude 'Česky': $table->AddAttributes( array('no'=>'Číslo', 'cz'=>'Česky', 'en'=>'English') );
Přidáme dva řádky dat (a prvnímu nastavíme CSS třídu): $table->AddRow( array('no'=>1, 'cz'=>'Leden', 'en'=>'January', 'class'=>'selected') ); $table->AddRow( array('no'=>2, 'cz'=>'Únor', 'en' => 'February') );
A vypíšeme HTML tabulky: echo $table->WriteTable();
Ilustrace 9 Ukázka výstupu třídy MyTable
Při výpisu lze vybrat jen určité sloupce a dát jim vlastní legendu, pořadí sloupců tak lze i zaměnit: echo $table->WriteTable( array( 'cz'=>'Měsíc', 'no'=>'Číslo' ) );
Ilustrace 10 Ukázka výstupu třídy MyTable s omezením zobrazených sloupců
20
A můžeme též vypsat každý řádek zvlášť: echo $table->WriteAsItems();
Ilustrace 11 Ukázka výstupu jako kartiček
Třída MyTable je důležitou součástí vrstvy View, především díky omezení vypsaných sloupců, neboť lze jeden vygenerovaný seznam plateb zobrazit různými způsoby.
3.5 Internacionalizace a lokalizace lokalizace Internacionalizace je proces, ve kterém se aplikace upraví tak, aby podporovala specifika různých zemí a národů, oproti tomu lokalizace znamená samotné naplnění systému daty pro jeden vybraný jazyk/národ. V propojeném internetu se již nevyplatí být omezen na jednu cílovou zemi a jeden jazyk, i kdyby tento přístup zmenšil počáteční náklady, je pravděpodobné, že budeme chtít v systému použít další měnu, či jazyk, a dodatečná internacionalizace by pak znamenala mnohem větší dodatečné náklady. Aplikace tedy byla od počátku internacionalizována, podporuje lokalizované ceny, i texty uživatelského rozhraní.
Lokalizace cen Pokud prodáváme službu v různých zemích, je pravděpodobné, že budeme chtít nastavit různé ceny pro různé země, prodávat je v různých balících jednotek (například jen měsíční předplatné v jedné zemi a čtvrtletní či pololetní v zemi druhé), či některé služby poskytovat jen v některých zemích. To vše aplikace podporuje prostřednictvím tabulek popsaných na obrázku Ilustrace 12. Systém umožňuje nastavit různé ceny pro každou podporovanou kombinaci měny a počtu jednotek, tak lze lehce vytvářet 'baťovské' ceny v různých měnách zároveň.
21
Tato funkčnost je zajištěna tabulkami cenasluzby, cenasluzby, která představuje všechny podporované dvojice <služba žba; počet jednotek> jednotek a cenasluzbymena, cenasluzbymena, která udává cenu této dvojice v měně mena_id mena_id.
Ilustrace 12 Podmnožina databázového schematu pro lokalizaci
Lokalizace textů Administrátoři systému jej budou muset překládat do různých jazyků, proto je potřeba extrahovat všechny řetězce na jedno místo, kde jej budou moci administrátoři přeložit. Dále bylo třeba podporovat podporovat v textu parametry („Platnost do… "), jako nejvýhodnějším se jevilo použití formátu funkce sprintf().. l() Překladová funkce je kvůli jednoduchosti volání globální funkcí s názvem l(), touto outo funkcí jsou obaleny veškeré řetězce, které uvidí uživatel. To navíc umožňuje generování souboru s chybějícími překlady – pokud l() nenajde překlad zadané fráze, přidá frázi do seznamu chybějících překladů, a na konci vykreslování stránky pak lze volat volat funkci writeLangpack(), writeLangpack(), která do souboru, jehož název je v konstantě LANG_MISSING_FILE, vypíše řetězce k překladu, ve formátu jazykových souborů (takže LANG_MISSING_FILE, administrátorovi pak stačí doplnit přeložený text a zmergovat soubor s dosud hotovým překladem překladem). První část až do tečky („profil.“) říká, že překlad tohoto řetězce patří do sekce profil, tak je možno výsledný soubor uspořádat, anebo rozlišit stejné překlady v různém kontextu. Dále následuje řetězec ve formátu standardní funkce sprintf, a neomezený počet parametrů. parametrů. 22
Pomocí notace s číslem jde pak i přehodit pořadí parametrů ve funkci, aby překlad v jiném jazyce mohl respektovat gramatiku daného jazyka. lang() Tato funkce nám umožňuje vepsat překlady do kódu, nezávisle na souboru s překlady. Takto lze vytvořit například odkaz na obrázek s textem, který se musí v každém jazyce lišit. langNo() Tato funkce je podporou pro slova která potřebujeme měnit v závislosti na čísle, příkladem je například doba na kterou se služba objednává (1 měsíc vs. 5 měsíců). for ($i = 0; $i<6; $i++) { $cas = lang(array( 'en' => langNo($i,array(0=>'months', 1=>'month',2=>'months')), 'cs' => langNo($i,array(0=>'měsíců', 1=>'měsíc',2=>'měsíce',5=>"měsíců")) )); $cislovka = langNo($i,array( 0=>'nultý', 1=>'první', 2=>'druhý', 3=>'třetí', 4=>'čtvrtý', 5=>'pátý')); echo "$i $cas - $cislovka
"; }
Tento kód vypíše: Je-li nastavena čeština: 0 měsíců - nultý 1 měsíc - první 2 měsíce - druhý 3 měsíce - třetí 4 měsíce - čtvrtý 5 měsíců - pátý
Je-li nastavena angličtina: 0 months - nultý 1 month - první 2 months - druhý 3 months - třetí 4 months - čtvrtý 5 months - pátý
initLang() Na začátku skriptu je dále nutné zavolat funkci initLang(), která načte z globální proměnné $lang kód nastaveného jazyka a podle něj načte soubor s překlady. Ty jsou uloženy v .ini souboru, který umí PHP nativně parsovat do asociativní array, v podadresáři lang/zkratka_jazyka.ini, jeho kódování je UTF-8. Příklad souboru s překladem: [reg-email-txt] Hello %1$s, if you would like to finish the registration, please go to the following address: %2$s=Dobrý den %1$s, pokud si přejete dokončit registraci, navštivte prosím následující adresu: %2$s Thank you!=Děkujeme Vám.
23
Escapování: Všechny znaky budou použity tak, jak jsou, jedinými výjimkami je rovnítko a znak nového řádku, ty se budou escapovat jako \= a \n Použití: l("profil.rok narozeni: %2$d jmeno uzivatele: %1$s", "Pepa", "3.12.1975")
Popsané funkce se nacházejí v souboru res/inc/multilingual.php.
3.6 Stavové informace – SessionData Aby mohla stavová webová aplikace fungovat po nestavovém protokolu, potřebuje si mezi jednotlivými stránkami předávat informace. U popisované aplikace je potřeba mezi stránkami předávat minimálně uživatelské jméno, a roli, ve které je uživatel přihlášen (admin, či user). Toto je možné pomocí mechanismu session. V aplikaci jsme se rozhodli spoléhat na podporu sessions pomocí cookies, a pro usnadnění tohoto přístupu byla vytvořena jednoduchá třída reprezentující session jako singleton. Session je tedy objektem, kterému můžeme dynamicky přidávat datové položky, implementace pomocí magických metod (6) pak zajistí, že každá datová položka bude zapsána do session, a bude ji možno na další stránce přečíst. Vzniká nám tedy perzistentní objekt, který může držet stav aplikace. Použití magic methods: function __get($propName) { if (isset($_SESSION[$propName])) return $_SESSION[$propName]; } return false; }
function __set($propName, $value) { if (in_array($propName, $this->allowed_fields)) $_SESSION[$propName] = $value; }
Pak lze číst a zapisovat do session tímto způsobem: SessionData::_instance()->login = $username; $username = SessionData::_instance()->login;
24
Singleton v PHP: static function &_instance() { static $instance; if( !isset($instance) ) { $instance = new SessionData(); $instance->init(); } return $instance; }
3.7 Platba Aplikace umožňuje dva způsoby platby – bankovním účtem a přes službu PayPal. Platba přes účet je triviální, klientovi se při nákupu ukáže id platby jako variabilní symbol, platba se uloží do databáze jako nezaplacená, a až administrátor systému uvidí na svém bankovním účtu platbu s daným id, označí ji v administračním rozhraní jako zaplacenou. Složitější je pak platba přes PayPal, tam komunikace probíhá podle Ilustrace 13 a je použita metoda Payment Data Transfer dle (7).
Ilustrace 13 Schéma platby přes Paypal
25
3.8 Testování Testování webových aplikací Testování webových aplikací je specifickým problémem – zatímco objekty v desktopových aplikacích jsou většinou schopné samostatné funkčnosti bez ovlivnění vnějším světem, webové aplikace jsou úzce provázané s okolím, což podstatně zvětšuje množinu jejich stavů. Okolím zde myslíme například databázi, SMTP server, či klienta. Další věcí je volba jazyka – PHP je skriptovací jazyk, a tak chyba vyjde najevo až když se příslušný .php soubor interpretuje WWW serverem. Testování by nám tedy mělo zajistit, že se ‚proklikají‘ všechny stránky a chyby vyjdou najevo v testu. Abychom mohli reprodukovat testy objektů závislých na databázi, museli bychom zachytit i neměnný stav databáze a inicializovat ho před každým testem, to se v praxi ukazuje jako nepraktické. Přitom kód jednotlivých objektů je většinou triviální, chyby se ve webových aplikacích projevují většinou až při interoperabilitě objektů. V takovém prostředí se klasické unit testy ukazují jako nevhodné, a vhodnou volbou jsou testy integrační, které simulují uživatele a testují plnou funkčnost aplikace.
Selenium Pro integrační testy byl použit nástroj Selenium (http://selenium-core.openqa.org) pro testování webových aplikací, který umožňuje simulovat aplikaci vstup od uživatele z prostředí prohlížeče, následně lze pak testovat, zda stránka obsahuje očekávaný výstup. Aplikace se skládá z části Selenium IDE – plugin pro Firefox, který umožňuje nahrát scénář interakce a napsat testy, a dále je spouštět i editovat, a z části Selenium Core, což je balíček Javascriptových knihoven, které nahrajeme na server a ty potom umožňují provádět stejný test v různých prohlížečích, či provádět automatické testy. Více informací přináší (8). Takto byl simulován uživatelský vstup, ale ještě jsme potřebovali před testy inicializovat databázi (aby byly jednotlivé průběhy testů shodné), proto byl vytvořen ještě PHP skript, který před testem při svém zavolání nainicializuje databázi, a po skončení testu ji zase vrátí do původní podoby. Poslední částí testů jsou asserty – kontroly splnění podmínek. Selenium je velmi flexibilní, a tak lze kromě základní metody verifyTextPresent(), která ověří přítomnost textu ve stránce použít třeba verifyText(… ), která umožní kontrolovat obsah elementu zadaného xpath výrazem, DOM výrazem, či CSS selektorem. Tyto metody mají i protiklady (verifyTextNotPresent), kterými lze naopak ověřit nepřítomnost např. chybové hlášky. Více o lokátorech viz (9).
26
Ilustrace 14 Selenium IDE pro vytváření testů s probíhajícím testem
Ilustrace 15 Selenium Test Runner - javascriptová javascriptov knihovna pro běh testů
27
3.9 XHTML a přístupnost Aplikací ikací vygenerované stránky jsou validním XHTML 1.0, toho je dosaženo využitím samostatných tříd, které dbají na validitu svého výstupu. Zároveň nám pomáhá fakt, že v aplikaci nezobrazujeme formátovaný text zadaný uživatelem, nemusíme se tedy starat o validitu uživatelova formátování. Veškeré formátování je vytvořeno pomocí CSS, HTML výstup zachovává sémantiku a je tak dobře použitelný i v prohlížečích bez podpory, či s omezenou podporou CSS.
Ilustrace 16 Zobrazení v plnohodnotném prohlížeči
28
Ilustrace 17 Zobrazení v prohlížeči Opera pro mobilní telefony
Ilustrace 18 Zobrazení v témže prohlížeči bez CSS stylů
29
3.10 Popis databáze Databázové Databázové schéma schéma
Ilustrace 19 Logické databázové schéma aplikace
30
Ilustrace 20 Fyzické schéma databáze a integritní omezení
Celé fyzické schéma databáze je na ilustraci, následující popis se bude věnovat významným sloupcům tabulek (všechny tabulky mají povinná pole vyžadovaná ORM vrstvou) vrstvou)..
Uzivatel je_admin aktivovany aktivace_hash
bool hodnota, která rozlišuje administrátora od uživatele potvrdil uživatel svůj svůj účet odkazem v emailu? hash se spočítá při registraci, kód z mailu musí být stejný
Sluzba Zde jsou uloženy všechny služby nabízené v systému. nazev popis poradi zavisi_na
idf aktivni
název služby podrobnější popis služby zobrazený například při nákupu pořadí ve výpisu pro vytváření stromu závislostí, ukazuje na id nadřazené služby - pro situace kdy lze nějakou službu aktivovat jen zároveň s jinou (nebo poté co už si zákazník jinou aktivoval) jednoznačný textový identifikátor pro použití v programu umožňuje znemožnit objednání služby, aniž bychom ji mazali 31
Cenasluzby Zde jsou uloženy tarify pro služby, například že službu 1 lze předplatit na jednu, dvě a šest jednotek, službu 2 jen na jednu jednotku. sluzba_id pocet_jednotek
id služby počet jednotek k předplacení
Cenasluzbymena Tato tabulka umožňuje specifikovat různé ceny pro službu + období + měnu. cenasluzby_id mena_id castka
ukazatel do cenasluzby ukazatel na použitou měnu částka
Mena zkratka nazev_meny kurz pise_se_pred printf_format
zkratka měny v daném jazyce (Kč, USD) plný název v jazyce dané měny aktuální kurz ke koruně (kolik Kč za jednotku dané měny) 1 = zkratka se píše před částkou, jinak za ní (USD 2, 1 Kč) obvyklý formát pro výpis měny (12 Kč, USD 2.00)
Jazyk kod nazev mena_id
kód předávaný např. v url (cs, en) plný název (pro výběr jazyka) – česky, anglicky výchozí měna přiřazená jazyku (dolary pro angličtinu…)
Platba Platba označuje jeden „košík“ služeb, ať už jen vybraný, objednaný, nebo zaplacený, tzn. i zboží v košíku je jedna platba. uzivatel_id cas_vzniku cas_zaplaceni stav
kdo provedl platbu kdy byla platba vytvořena (každá platba je na začátku košíkem, tedy datum vytvoření košíku) kdy byla platba zaplacena = enum (kosik, podano, zaplaceno, uplatneno) kosik: platba je nákupním košíkem podano: uživatel potvrdil nákup, ještě nedošly peníze zaplaceno: uživatel uhradil zboží uplatněno: informace o objednaných službách byla úspěšně předána 32
castka
mena_id zdroj_platby poznamka cislo_faktury je_zdarma
backendu celková suma platby, po zaplacení se z fakturačních důvodů nesmí v žádném případě měnit (ani změní-li se cena služeb), proto si ji uložíme měna platby zdroj – paypal nebo account, případně i další pro poznámky čísla faktury musí následovat po sobě, a to jen u uhrazených plateb, ukládáme je proto zvlášť označuje platby, za které uživatel nemá platit, například testovací platby, či zkušební účty pro recenzenty, bonusové dárky zákazníkům – taková platba nemá fakturu
Polozka Polozka platba_id pocet_jednotek castka
platba, jejíž součástí je daná položka počet jednotek (není to odkaz do cenasluzby, protože takový počet jednotek už nemusí být v nabídce) částka zaplacená za položku, opět duplikace, protože ceny služeb se mohou měnit, vyfakturované částky ne
Kupon Tato tabulka je určena pro bonusové kupony z promo akcí – uživatel zadá kód (ze stránek, časopisu), a dostane zdarma nějakou službu na nějakou dobu. kod sluzba_id pocet_jednotek vyprsi aktivni
kód kuponu
kdy vyprší použitelnost daného kódu je tento kód aktivní (pokud není, nepomůže ani to, že datum vypršení ještě nenastalo)
Kupon_uzivatel Hlídá, aby si jeden uživatel neuplatnil stejný kód vícekrát – představuje vazbu m:n s atributy. uzivatel_id kupon_id datum_vytvoreni
odkaz na uživatele odkaz na kupon pro statistické účely
33
4 ORM vrstva Vývoj databázových aplikací ovlivnilo objektové paradigma stejně, jako zbytek programátorského světa. V době, kdy se stala aplikace objektovou reprezentací řešeného problému, klasická relační databáze reprezentovaná řádky neposkytovala stejnou úroveň abstrakce, a jediným řešení tohoto rozporu byla přeměna relační databáze na objektovou. Objektová databáze už může bez zbytečné konverze uchovávat stálé, perzistentní objekty v jejich původní podobě, a sloužit tak aplikaci jako perzistentní, transakční dotazovatelné úložiště objektů, ale objektové databáze jsou zatím stále doménou komerčních 'enterprise' řešení. Jako náhrady objektové databáze proto vznikly ORM (=Object-relational mapping) vrstvy, jejichž úkolem je slouží jako adaptér, který na jedné straně komunikuje s řádkově orientovanou relační databází, zatímco na straně druhé vytváří objektovému programovacímu jazyku iluzi objektově orientované databáze. Pro PHP existuje velké množství knihoven, které se liší svým rozsahem a funkcemi, které nabízejí. Momentálně je asi největším ORM balíkem pro PHP Propel (10), který zahrnuje všechny aspekty ORM, na druhou stranu ale je příliš komplexní pro malé projekty a jeho použití je netriviální. Rozhodli jsme se proto hledat dále a našli jsme knihovnu MyActiveRecord (11), která je naopak minimalistická, a ačkoliv klade omezující podmínky na jména tabulek a některých polí, u projektů ve kterých je použita od začátku tato omezení nevadí a naopak přinášejí zjednodušení.
4.1 MyActiveRecord vs. Propel Pokusíme se srovnat tyto dva rozdílné frameworky: Definice databáze: databáze: MyActiveRecord předpokládá již vytvořenou databázi respektující konvence knihovny, databáze je tedy zároveň předlohou datového modelu. K editaci databáze tedy můžeme využít širokou škálu standardních nástrojů pro MySQL. Stejně tak jednoduché jsou i následné úpravy modelu, MySQL zachovává data při úpravách tabulek. Propel: Schema databáze se definuje v XML, ze kterého následně Propel generuje SQL k vytvoření tabulek a PHP třídy. Tím pádem ale ztrácíme výhodu SQL nástrojů a dosavadní know-how, a vracíme se o desítky let zpět k editaci SQL v textovém editoru, navíc ve formátu, který rutinně neznáme. Stejná tabulka v XML je méně čitelná, než v SQL.
34
Reverse-engineering existující databáze je sice možný, následné úpravy databáze (která má na začátku práce zřídkakdy definitivní podobu) však již nejsou tak pohodlné a prodlužují čas implementace každé změny datového modelu. Dodatečné nástroje: nástroje: MyActiveRecord: MyActiveRecord: Nejsou. Propel: Je potřeba ovládnout několik nástrojů pro transformaci XML na SQL a PHP třídy. Míra abstrakce: MyActiveRecord: MyActiveRecord: Nízká, ale konzistentní. Propel: Vysoká, ale místy je abstrakce děravá (leaky abstraction (12)) - například při třídění záznamů nelze některá pole, jako třeba ORDER BY automaticky escapovat, a nemůžeme do nich tedy dávat data převzatá od uživatele, jinde se přitom o toto zabezpečení nestaráme a tak vzniká riziko, že se na tento případ zapomene a vznikne bezpečnostní díra. Zde se projevuje fakt, že čím vyšší abstrakci knihovna poskytuje, tím větší šance je, že tato abstrakce bude děravá a ke správnému zacházení s ní bude nutné pochopit všechny nižší úrovně abstrakce. Snadnost použití: Tvorba objektu MyActiveRecord: MyActiveRecord: $uzivatel = AR::Create('Uzivatel', array( 'jmeno' => 'Alois', 'heslo' => 'tajne' 'datum_registrace' => AR::DbDateTime(), 'aktivovany' => false) ); $uzivatel ->save();
Propel: $uzivatel = new Uzivatel(); $uzivatel->setJmeno('Alois'); $uzivatel->setHeslo('tajne'); $uzivatel->setDatum_registrace(date_create()); $uzivatel->save();
Jak vidíme, tento scénář je velmi podobný v obou knihovnách. Snadnost použití: Dotaz MyActiveRecord: MyActiveRecord: $authors = AR::FindAll( 'Author',
35
AR::Prepare("first_name = '%s' AND last_name <> '%s' ORDER BY last_name DESC", 'Sebastian', 'John') );
Propel: $criteria = new Criteria; $criteria->add( AuthorPeer::FIRST_NAME, 'Sebastian' ); $criteria->add( AuthorPeer::LAST_NAME, 'Nohn', Criteria::NOT_EQUAL ); $criteria->addDescendingOrderByColumn(AuthorPeer::LAST_NAME): $authors = AuthorPeer::doSelect($criteria);
Jak je vidět, Propelovská kritéria sice umožňují na pozadí dotaz převést do různého SQL pro různé podporované servery, na druhou stranu jsou ale zbytečně komplikované pro naprostou většinu běžných operací. Vývojáři Propelu si zde nadefinovali svůj vlastní Domain Specific Language (DSL (13)) pro výběr objektů z databáze, a tím pádem činí vývojářovu znalost SQL zbytečnou – uživatel se musí učit specifický jazyk, který stejně v jiném projektu bez Propelu nevyužije. Zároveň ale platí, že kdo vyvíjí databázové aplikace, musí samozřejmě ovládat SQL, a tak tento DSL stejně nikoho od učení SQL neušetří. Oproti SQL bylo ale potřeba napsat mnohem více kódu (což poskytuje více prostoru pro vznik chyby), zbytečně se mnohokrát opakuje stejný text (criteria, author), a ačkoliv bylo popsáno tolik řádek, není nijak zjevné, že mezi dvěma kritérii je logická spojka AND. Přístup Propelu je tedy navíc méně sémantický. Dynamičnost: MyActiveRecord: MyActiveRecord: Objekty jsou dynamicky generované za běhu podle aktuální definice tabulek, uživatel si tedy musí pamatovat jak se které pole objektu jmenuje, a pokud udělá překlep, dozví se o něm až chybou při testování. $user->name = "Josef";
Propel: Třídy jsou staticky generované, lze tedy proti nim dělat statickou analýzu a používat autocomplete v IDE, které to podporuje. Výhoda, zejména ve větších projektech je zjevná, je ale vyvážena těžkopádnější změnou datového modelu. $user->SetName("Josef");
36
Velikost - nejsou započítány uživatelem napsané metody tříd:: MyActiveRecord má kolem 600 řádků v jednom souboru. Propel: Runtime Propelu má přes 4000 řádků kódu (bez komentářů) v 38 souborech, což se může na výkonu aplikace negativně projevit. Závěr: Pro menší projekty nám vychází jako vhodnější knihovna MyActiveRecord, i díky tomu, že nebylo třeba navazovat na stávající databázové schéma, a tak nám nevadila nutnost respektovat konvence ORM vrstvy. Proti Propelu hovoří i fakt, že předem víme, že aplikace poběží na MySQL databázi, a tak těžkopádná univerzální databázová nadstavba Propelu přináší jen znetriviálnění jednoduchých operací.
4.2 Konvence MyActiveRecord ORM vrstvy 1. Jména tabulek jsou malými písmeny. Toto omezení vzniklo proto, že funkce get_class() v PHP 4 u objektů nezachovávala velká/malá písmena. 2. Jména tabulek neobsahují podtržítko až na výjimky popsané v těchto pravidlech. 3. Každá tabulka má sloupec id typu unsigned integer, auto-increment, unique. 4. vazby z polozka do platba typu 1:n jsou ve sloupci polozka.platba_id 5. Vazba mezi letadlo a letiste typu m:n je, i s atributy, reprezentována tabulkou letadlo_letiste (jména tabulek v abecedním pořadí oddělená podtržítkem), ta má povinné atributy id, letadlo_id, letiste_id stejného typu jako id. Další atributy vazby jsou vyjádřeny dalšími sloupci tabulky. Tabulky jsou svázány vazbami na svá id, id je přitom jednoznačný identifikátor řádku (toto omezení umožňuje zjednodušit ORM vrstvu o různé techniky, jejichž účelem je „vysvětlit“ ORM vrstvě jaké jsou vazby, takto jsou metadata o vazbách odvoditelné z DB schematu, a schema modelu je určeno aktuální databází). Vazby 1:n Odkazující tabulka obsahuje navíc sloupec s názvem
_id>. Pokud se tedy tabulka registrace odkazuje na tabulku uzivatel vazbou 1:n, bude v tabulce registrace sloupec uzivatel_id odkazující na sloupec id tabulky uzivatel. Uživatelovy platby následně zjistíme takto: $uzivatel = AR::FindById('Uzivatel');
37
$platby = $uzivatel->find_children('Platba'); //$platby[42] je ted objekt – platba s id=42
Vazby m:n Tato vazba je znázorněna vyhrazenou tabulkou _, která obsahuje sloupce id, tabulka1_id a tabulka2_id, přičemž v názvu tabulky musí být jména tabulek seřazená abecedně. Do spojovací tabulky je možné přidávat i další sloupce – atributy vazby. Příklad: $c4 = AR::Create('Auto', array( 'vyrobce'=>'Citroen', 'model'=>'C4' ) ); $ka = AR::Create('Auto', array( 'vyrobce'=>'Ford', 'model'=>'Ka' ); //pred vytvorenim vazby je nutno vazane objekty ulozit $c4->save(); $ka->save(); $jana = MyActiveRecord::Create('Ridic', array('jmeno'=>'Jana') ); $pepa = MyActiveRecord:Create('Ridic', array('jmeno'=>'Josef') ); $jana->save(); $pepa->save(); //vytvorime vazby $c4->attach($jana); $ka->attach($pepa); $ka->attach($jana); //co ridi pepa $pepa_ridi = $pepa->find_attached('Auto'); $jana_ridi = $jana->find_attached('Auto');
Zhodnocení Popsaná omezení se zdají na první pohled příliš přísná, zkušenosti z praxe ale ukazují, že i pro středně velké projekty je tento systém dobře použitelný. Výměnou za omezení názvu tabulek a sloupců získáváme výhodou, a to tu, že odpadají dodatečné soubory s mapováním objektů na tabulku obvyklé v jiných ORM frameworcích, a databáze je zároveň modelem. Veškeré změny se tedy dějí na jednom místě a jména tabulek vyjadřují i sémantiku jejich vazeb. Názvy metod Názvy metod MyActiveRecord používají následující konvenci: •
CamelCase (FindFirst) – statické metody třídy ActiveRecord, vracejí obvykle objekt nebo array objektů
38
•
lowercase_slova_oddelena_podtrzitkem (find_parent) – metody které má smysl volat jen na objektu
Rozhodl jsem se tuto konvenci zachovat, ale v metodách objektů, které už odpovídají tabulkám, jsem se rozhodl použít běžnější CamelCase konvenci použitou i ve zbytku projektu. Další metody Objekty – reprezentace objektů z reálného světa jsou potomky třídy MyActiveRecord, a je možné jim přidávat libovolné další metody. Takto lze přidat metody reprezentující reálné vztahy mezi objekty – například Platba-> update_total(), která přepočte cenu platby tím, že nahraje jednotlivé položky a zavolá na nich metodu get_price(). Třídu MyActiveRecord a od ní odvozené objekty popisuje Ilustrace 21.
Závěr Použití ORM vrstvy bylo jednoznačným přínosem, po doplnění o vlastní metody byla velká část aplikační logiky vyjádřena přirozenými voláními mezi objekty. Objektový přístup zjednodušil práci s databází a učinil aplikaci bezpečnější a odolnou proti injektování SQL.
39
Ilustrace 21 Diagram tříd ORM vrstvy
4.3 ORM a výkon Častým (a často oprávněným) argumentem proti ORM je negativní vliv na výkon, a to především na počet databázových dotazů. Ve webových aplikacích totiž většinou nejsou výpočetně náročné algoritmy, a tak je naprostá většina času strávená čekáním na databázi, výkon aplikace je tak nepřímo úměrný počtu databázových dotazů na jednu zobrazenou stránku. Kritika ORM vrstev je založena z velké části na faktu, že načítají vždy celé jednotlivé objekty (řádky databáze), nelze prostřednictvím ORM vybrat například výsledek operace JOIN – protože vzniklý objekt nemá ORM vrstva jak reprezentovat. 40
Ukažme si to na příkladu: Předpokládejme, že chceme sestavit seznam uživatelů a všech služeb, které má daný uživatel momentálně předplacené. S použitím MySQL nám bude stačit jediný dotaz, jehož výsledkem bude tabulka spojující všechny atributy uživatele, registrace a služby: SELECT * FROM uzivatel JOIN registrace ON registrace.uzivatel_id = uzivatel.id JOIN sluzba ON registrace.sluzba_id = sluzba.id WHERE registrace.platna = 1
Pokud budeme chtít stejného výsledku docílit pomocí ORM vrstvy, budeme muset načíst všechny uživatele, a potom vyhledat platné registrace a k nim přidružené služby pro každého zvlášť. Počet SQL dotazů s ORM vrstvou tedy bude v takovém případě přímo úměrný počtu uživatelů, de fakto si vlastně budeme onen JOIN implementovat sami:
Ilustrace 22 Vlastní implementace joinu
Protože jsou ale scénáře tohoto typu ve webových aplikacích velmi časté (výrobky v uživatelově košíku a data o nich, umělci daného žánru a jejich cédéčka, atd.), zamyslíme se nad jejich optimalizací. Prvním návrhem bude využití příkazů pro nahrání všech objektů, které vyhovují zadané podmínce:
41
Ilustrace 23 Příkazy nahrávající z databáze více než jeden objekt
Pořád se ale každá služba načítá několikrát, tomu ale můžeme zabránit pomocí cachování výsledků dotazů: ORM vrstva si bude pamatovat přijaté SQL dotazy a jejich výsledky, a pokud se uživatel zeptá na objekt, který jsme již načetli, nebudeme se ptát databáze, ale vrátíme mu ho z cache. Tím jsme v ukázkové situaci ušetřili více než polovinu dotazů, ale stále jsme se nepřiblížili efektivnosti původního řešení bez ORM a počet dotazů je přímo úměrný počtu uživatelů.
Ilustrace 24 Příklad cachování dotazů
Pokud by takovéto případy byly v aplikaci časté a počet dotazů by byl faktorem omezujícím škálování, znamenalo by to nutnost úpravy ORM vrstvy tak, aby podle zadaných kritérií uměla sama vytvářet SQL příkazy s JOINem, a jako výsledek by potom vracela ne jeden objekt, či seznam objektů, ale celou hierarchickou strukturu - viz Ilustrace 25. Taková vrstva by už ale byla velmi netriviální, spojovala by ale výhody obou přístupů. 42
Ilustrace 25 Objektový graf výsledku dotazu (šipky představují reference na objekty)
Benchmark V uvedeném příkladu bylo cachování dotazů významným zlepšením, je tomu tak ale i při nasazení v reálném světě? Na tuto otázku jsme se pokusili odpovědět benchmarkem, při kterém byly ORM vrstvou logovány vykonané databázové dotazy. Jako benchmarkované scénáře zde výborně posloužily integrační testy z nástroje Selenium. Testy navíc představují jednotlivé scénáře, které nastanou v ostrém provozu, proto se hodí pro měření. Test 1 Test 2 Test 3 Test 4
Smazání testovacího uživatele a jeho plateb, registrace uživatele, zkouška opakovaného zadání neplatných registračních údajů. Přihlášení uživatele, uplatnění kupónu, výběr služby, zaplacení a odhlášení. Přihlášení administrátora, vyhledání uživatelovy platby, označení platby za zaplacenou, odhlášení. Přihlášení administrátora, proklikání všech sekcí administrátorského rozhraní.
Výsledek Výsledek měření: měření: Po č et SQL do taz ů B ez c ac he
Test 1 Test 2 Test 3 Test 4 Cel k em
47 470 178 89 784
S c ac he
47 288 148 77 560
Průměrný po č et do taz ů na stránk u
Z l epšení
0% 39% 17% 13% 29%
Poče t s tr á n e k
6 12 12 8 38
B ez c ac he
7.8 39.2 14.8 11.1 20.6
S c ac he
7.8 24.0 12.3 9.6 14.7
Závěr: Vidíme, že největší úsporu – 39 % – přineslo cachování dotazů v předpokládaném nejčastějším scénáři – práci uživatele s jeho účtem (test 2). Porovnáním zaznamenaných sekvencí dotazů jsme zjistili, že úspora opravdu nastala tam, kde nebylo nutno nahrávat 43
stejný objekt odkazovaný z většího množství objektů, například měnu plateb, či jazyk uživatele. V ostatních testech byla úspora menší, a ve scénáři s registrací dokonce nulová, což se dalo očekávat, vzhledem k tomu, že se zde především kontroluje existence uživatele a nakonec se jednou vytvoří. Průměrná úspora v celé sérii testů byla 29%, což považujeme za dobrý výsledek, vzhledem k tomu, že mezi zbylými dotazy už jsme po prohlédnutí logu dotazů nenašli žádné, které by bylo možno odstranit bez značného zesložitění ORM vrstvy. Několik dotazů na každé stránce bylo způsobeno režií ORM vrstvy, a to dotazem SHOW TABLES, při vytváření nových objektů pak SHOW COLUMNS.
44
5 Závěr 5.1 Shrnutí V první kapitole jsme si uvedli problematiku software jako služby a byly stanoveny cíle práce. V druhé kapitole byly popsány scénáře interakce uživatele s aplikací, podle kterých byla nakonec aplikace navržena. Také zde byl popsán způsob, kterým prodávaná služba přes http protokol ověří, zda ji má uživatel zakoupenu. Třetí kapitola se do hloubky věnovala implementaci. V úvodu jsme se rozhodli implementovat aplikaci bez využití existujícího aplikačního frameworku, což se ukázalo v dané době jako dobrá volba vzhledem k nevyspělosti platformy založené na objektovém PHP. Představili jsme vytvořené znovupoužitelné knihovny pro formuláře a tabulky a ukázali, jak je použít, u třídy MyForm jsme si přitom povšimli, že s velmi podobným řešením přišel později i oblíbený Zend Framework. Zamysleli jsme se nad problematikou internacionalizace a následné lokalizace aplikace a představili implementované řešení. Shrnuli jsme problematiku předávání stavu aplikace mezi stránkami a představili si objektový wrapper na práci se session. Dále jsme vysvětlili systém úhrady plateb a popsali průběh finanční transakce přes systém PayPal. V další části jsme se věnovali testování webových aplikací a popsali si práci s integračními testy v nástroji Selenium. Konstatovali jsme, že stránky aplikace jsou XHTML 1.0 validní a přístupné i pro prohlížeče bez podpory CSS. Závěrem jsme zdokumentovali databázové schéma aplikace a význam jednotlivých atributů řádků tabulek. Čtvrtá kapitola rozebírala problematiku objektově relačního mapování. Nejdříve jsme porovnali dva ORM frameworky pro PHP a přiklonili se k použití minimalistické knihovny MyActiveRecord před enterprise frameworkem Propel. Pak jsme zkoumali vliv ORM vrstvy na výkon měřený v počtu SQL dotazů na vygenerovanou stránku a porovnali na příkladu složitější SQL dotaz, vyjádření téhož dotazu pomocí naivní ORM vrstvy, která přitom vygenerovala mnohem více SQL dotazů, snížení počtu dotazů nasazením cachování, a nakonec jsme se zamysleli nad návrhem ORM vrstvy, která by byla stejně efektivní jako neobjektové použití SQL. V závěru jsme provedli měření vlivu cache na integračních testech popisujících scénáře z reálného použití a konstatovali, že v takovém provozu může cache ušetřit 29% databázových dotazů.
5.2 Splnění cílů V rámci práce byl implementován fungující internacionalizovaný systém pro prodej softwaru jako služby, který se v praxi osvědčil při prodeji služeb VPN Anonymizeru (www.vpnanonymizer.cz) s více než 3000 registrovanými uživateli a fungoval ke spokojenosti uživatelů i administrátorů. 45
Byly vytvořeny znovupoužitelné třídy MyForm a MyTable, kde MyTable je objektový wrapper na zacházení s tabulkami a jejich vypisování do stránky, a MyForm je nástroj pro veškerou práci s formuláři. Pro účely aplikace je MyForm plně vyhovující, ale pokud bychom uživatele chtěli nechat zadávat formátovaný text, museli bychom přidat podporu filtrování volitelné podmnožiny HTML s pevně omezenými atributy, abychom předešli XSS zranitelnostem. Dále byla vytvořena sada funkcí pro internacionalizaci textů stránky, která dovoluje překládat i tvary slov závislé na číslovkách. Rozhodnutí vyvíjet aplikaci bez pevného základu nějakého velkého frameworku se ukázalo rozumným v době kdy vývoj začínal, od té doby se ale stav objektových PHP frameworků výrazně zlepšil, ty horší zanikly a několik dobrých získalo podporu vývojářů a přehouplo se z betaverzí ve funkční projekty. To se týká především Zend Frameworku, který se posunul z betaverze do nynější verze 1.5, a nabral podporu i velkých firem jako je Google či Microsoft. Během vývoje hrála negativní roli prokázala naše nedostatečná zkušenost s navrhováním takto velkého systému, především oddělení prezentační vrstvy a aplikační logiky mělo být důkladnější. Tomu by dopomohl i vývoj postavený na solidním MVC frameworku, jehož dobře navržená architektura by činila lehkým psát dobře rozdělený kód a naopak kladla překážky při míchání funkcí z různých vrstev. V plánu také byla podpora pluginů, například pro platební systémy, náročnost této funkčnosti byla bohužel na začátku vývoje podceněna, a kvůli nedostatku času bohužel na její implementaci nezbyl čas. Přitom je přínos této podpory otázkou, nutnost udržovat konzistentní a případně zpětně kompatibilní API pro pluginy by brzdila vývoj, a kvůli několika málo pluginům by bylo potřeba vytvořit správce pluginů, rozhraní pro pluginy kterým by vytvářely své vlastní tabulky v databázi, či spolehlivý způsob upgradování pluginů bez ztráty ji vytvořených dat.
5.3 Použité nástroje Pro vývoj bylo použito volně dostupné IDE Eclipse 3.3 (14) s pluginy: •
• •
PDT (PHP Development Tools) (15) – vývojové prostředí pro PHP se statickou analýzou kódu, auto-complete podle PHPdoc komentářů a s podporou debugování na serveru. Subclipse (16) – plugin pro práci se Subversion. Azzuri Clay Core (17) – plugin pro návrh databázových schémat.
Práce s ním byla velmi pohodlná, statická kontrola a doplňování PHP kódu podle typové informace z PHPdoc komentářů fungovala lépe, než v libovolném jiném volně dostupném editoru, a oceňujeme možnost vývoje celé aplikace v jednom prostředí, od psaní kódu, přes návrh CSS až po návrh databáze v grafickém editoru. 46
5.4 Použité knihovny třetích stran V práci byly použity tyto knihovny třetích stran: Název Autor URL MyActiveRecord Jake Grimley http://www.phpclasses.org/browse/package/2990.html ORM knihovna použitá v práci NSafeStream David Grudl http://nettephp.com/ Knihovna pro atomické operace se soubory nezávislá na OS Codeworx PHPMailer http://sourceforge.net/projects/phpmailer Technologies Objektová knihovna pro posílání mailů prostřednictvím SMTP serveru
47
Literatura 1. Fowler, Martin. Separated Presentation. MartinFowler.com. [Online] 29. 6 2006. http://www.martinfowler.com/eaaDev/SeparatedPresentation.html. 2. Wikipedia. Model-view-controller. Wikipedia. [Online] 25. 5 2008. http://en.wikipedia.org/wiki/Model-view-controller. 3. Zend. The Standard Router: Zend_Controller_Router_Rewrite. Zend Framework: Documentation. [Online] 2008. http://framework.zend.com/manual/en/zend.controller.router.html. 4. —. Zend_Form reference. Zend Framework: Documentation. [Online] 17. 3 2008. http://framework.zend.com/manual/en/zend.form.html. 5. Major, Martin a Selement, Pavel. Zranitelnost webových aplikací. [Online] 23. 4 2008. http://ondrej.jikos.cz/vyuka/swi117/zranitelnost-webovych-aplikaci.xhtml. 6. PHP.net. Magic Methods. PHP Manual. [Online] 2. 5 2008. http://php.net/oop5.magic. 7. Payment Data Transfer. PayPal. [Online] 2008. https://www.paypal.com/IntegrationCenter/ic_pdt.html. 8. Vávrů, Vlastimil. Selenium - mocná zbraň na akceptační testy. Vlastův blog. [Online] 16. 12 2007. http://vavru.cz/testovani-aplikaci/selenium-mocna-zbran-na-akceptacni-testy/. 9. OpenQA. Selenium documentation. Selenium. [Online] 2008. http://release.openqa.org/selenium-remotecontrol/0.9.0/doc/dotnet/html/Selenium.html. 10. Propel. Propel. [Online] 2008. http://propel.phpdb.org/trac/. 11. Grimley, Jake. MyActiveRecord - New Release. Made Media. [Online] 18. 11 2006. http://mademedia.co.uk/2006/10/09/myactiverecord-new-release/. 12. Spolsky, Joel. The Law of Leaky Abstractions. Joel On Software. [Online] 11 2002. http://www.joelonsoftware.com/articles/LeakyAbstractions.html. 13. Wikipedia. Domain-specific language. Wikipedia. [Online] 25. 5 2008. http://en.wikipedia.org/wiki/Domain-specific_programming_language. 14. Eclipse - an open development platform. [Online] 2008. www.eclipse.org. 15. PDT Project. [Online] http://www.eclipse.org/pdt/. 16. Subclipse. [Online] 2008. http://subclipse.tigris.org/. 17. Azzuri Clay - Database Modelling in Eclipse. [Online] 2008. http://www.azzurri.jp/en/software/clay/index.jsp. 48
Přílohy Obsah CD /Dokumentace
Uživatelská dokumentace a elektronická podoba této práce Návod na spuštění serveru a přihlášení k aplikaci
/Server
Předinstalovaný apache server s ukázkovými daty
/Source
Zdrojový kód aplikace
/Tests
Integrační testy aplikace pro Selenium
Ukázková instalace služby URL
http://bc.tomaskafka.com
Login uživatele
http://bc.tomaskafka.com/index.php Jméno: user Heslo: userheslo
Login administrátora
http://bc.tomaskafka.com/admin.php Jméno: admin Heslo: adminheslo
49