You are on page 1of 84

Spis treści

Testy konsumenckie w PHP Solutions


R wolucje mają to do siebie, że wprowadzają zmiany. Nieważne, czy owe
zmiany są tylko czasowe czy przyjmą się czy nie. Każda zmiana jednak
wprowadza powiew świeżości.
AKTUALNOŚCI 6
Krzysztof Trynkiewicz
Nowy zespół pracujący aktualnie nad naszym magazynem gwarantuje
Wam powrót do źródeł. I sporo zmian.
I tak, od następnego numeru pisma PHP Solutions zaczniemy publikację OPIS CD 10
cyklu testów konsumenckich. W każdym wydaniu będziemy zamieszczać re-
cenzje kliku produktów z tej samej grupy. Recenzje będą przygotowywane
przez użytkowników, którzy znają produkt od podszewki i korzystają z niego DLA POCZĄTKUJĄCYCH
wystarczająco długo, żeby znać nie tylko zalety ale też wady. Recenzje będą
Testy wydajności
obejmować nie tylko aspekty techniczne produktu, ale też jakość wsparcia,
i profilowanie aplikacji PHP 12
proces zakupu i wiele innych ważnych elementów.
Zapraszamy wszystkich chętnych do napisania recenzji. Prosimy o kon- Łukasz Witczak
takt z nami, przekażemy Ci wszystkie szczegóły. Łukasz omawia jak testować wydajność zarów-
no całej aplikacji jak i wybranych fragmentów
kodu. Uczy również jak jak znajdować wąskie
Zapraszamy do współpracy! gardła w systemie, przez które można zopty-
malizować aplikacje i w miarę niewielkim kosz-
Tabela 1. Kalendarium tematów testów konsumenckich 2007 tem podnieść wydajność całej witryny.

Tematy
Usługi hostingowe
Savant – pogromca Smarty? 24
Relokacja serwerów Tomasz Garbiak
Tomasz w swoim artykule opisuje jak korzystać
Sprzęt – Serwery
z systemu Savant – zorientowanego obiekto-
Łącza internetowe wo systemu, który wykorzystuje samo PHP ja-
Statystyki zewnętrzne ko język szablonów. Przedstawia również wady
i zalety tego zorientowanego obiektowo syste-
UPC mu wyszukujacego samo PHP jako język sza-
blonów.
Zapraszamy również firmy, które chciałyby, aby ich produkty lub usługi
zostały objęte naszymi testami.
BEZPIECZEŃSTWO
RSA w PHP:
chronimy dane przy użyciu
kryptografii asymetrycznej 32
Kamil Karczmarczyk
Sylwia Pogroszewska Kamil przedstawia działanie algorytmu asyme-
phpsolmag@software.com.pl trycznego RSA, który jest obecnie najpopu-
larniejszym algorytmem szyfrowania asyme-
trycznego,używanym powszechnie np. w han-
dlu elektronicznym czy też w celu podpisywa-
nia emaili. Autor wskazuje, jak przy jego użyciu
stworzyć system bezpiecznego logowania.
Nasz magazyn ukazuje się w trzech językach!
PROJEKTY
polskim niemieckim francuskim
XML_FastCreate 38
Guillaume Lecanu
Guillame pokazuje jak tworzyć prawidłowy kod
XML za pomocą XML_FastCreate, sposób doko-
nywania transformacji znaczków XML-a, spraw-
dzania DTD , wykrywania błędów składni i two-
rzenia dokumentów w XHTML-u.

Jeśli jesteś zainteresowany zakupem licencji na wydawanie naszych pism prosimy o kontakt:
Monika Godlewska monikag@software.com.pl tel.: 48 22 887 12 66, fax: 48 22 887 10 11

4 www.phpsolmag.org PHP Solutions Nr 6/2006


Spis treści

Pytania dotyczące Strona WWW/Forum


prenumeraty strona www: www.phpsolmag.org
tel. (22) 887 14 44 Tu znajdą Państwo informacje
DLA ZAAWANSOWANYCH e-mail: pren@software.com.pl dotyczące aktualnych i przyszłych
Software Wydawnictwo Sp. z o.o. numerów magazynu PHP Solutions.
Rozwiązywanie problemów
dział prenumeraty
przekrojowych z uzyciem IoC 44 ul. Bokserska 1 Forum: www.phpsolmag.org/newforum
Piotr Szarwas 02-682 Warszawa Zachęcamy do dyskusji na naszym
forum. Czekamy na propozycje
Piotr obrazuje rozwiązania niektórych proble- CD tematów, które chcieliby Państwo
mów przekrojowych, których nie można przy- tel. (22) 887 14 44
pisać do żadnej z warstw za pomocą konte- znaleźć w najbliższym numerze pisma.
e-mail: cd@software.com.pl
nera IoC- zwyczajnie konfigurowanej fabryki Zapraszamy także do wymiany
Software Wydawnictwo Sp. z o.o.
obiektów, która potrafi przywołać do życia ca- poglądów z innymi fanami PHP.
Defekty CD/DVD
łe ich drzewa. ul. Bokserska 1 Cena
02-682 Warszawa Prenumerata: 135 zł
Przyjazne URL-e w PHP, Przelew na konto nr:
Zamówienia 46 1440 1299 0000 0000 0391 8238
czyli zaprzęgamy mod_rewrite /Numery archiwalne Nordea Bank Polska S.A.
do pracy 50 tel. (22) 887 14 44 II Oddział w Warszawie
e-mail: pren@software.com.pl
Michał Gacki sklep on-line: www.shop.software.com.pl
Michał ilustruje zabezpieczenia dostępu do pli-
ków, pokazuje jak za pomocą Mod_Rewrite za- Kontakt z redakcją
mienić nawet największą plątaninę linków i pa- e-mail: redakcja@phpsolmag.org
rametrów na czytelne adresy WWW . Przybliża Software Wydawnictwo Sp. z o.o.
też podstawy wyrażeń regularnych. Redakcja PHP Solutions
ul. Bokserska 1
02-682 Warszawa
KASA DLA WEBMASTERA
Tajniki freelancingu 62 Listingi wszystkich opisywanych programów zostały zamieszczone na naszej stronie
internetowej www.phpsolmag.org/pl.
Krzysztof Trynkiewicz
Krzysztof kontynuuje artykuł o freelancingu. W
dzisiejszym numerze skupia się na szczegółach
definiowania zleceń i składania ofert zlecenio- PHP Solutions jest wydawany przez Software-Wydawnictwo Sp. z o.o.
dawcom. Pokazuje też uzyteczne praktyki sto- Redaktor naczelny: Sylwia Pogroszewska
sowane podczas tworzenia portfolio i resume do Asystent redaktora: Patrycja Wądołowska patrycja.wadolowska@software.com.pl
celów freelancingowych.
Kierownik produkcji: Marta Kurpiewska marta@software.com.pl
Projekt okładki: Agnieszka Marchocka
Skład i łamanie: Robert Zadrożny robert.zadrozny@software.com.pl

TECHNIKA Stali współpracownicy: Krzysztof Sobolewski krzysztof.sobolewski@gmail.com


Krzysztof Trynkiewicz chris.tynkiewicz@gmail.com
PHPUnit w praktyce 66
Dział reklamy: adv@software.com.pl
Marcin Staniszczak Prenumerata: Marzena Dmowska pren@software.com.pl
Nakład: 6 000 egz.
Marcin pokazuje jakstosować testy jednostkowe
za pomocą frameworka PHPUnit2w celu odna- Adres korespondencyjny: Software-Wydawnictwo Sp. z o.o.,
lezienia błędu w aplikacji składającej się z kilku- ul. Bokserska 1, 02-682 Warszawa, Polska
dziesięciu-kilkuset klas. tel. +48 22 887 10 10, fax +48 22 887 10 11
www.phpsolmag.org cooperation@software.com.pl

Dołączoną do magazynu płytę CD przetestowano programem AntiVirenKit firmy G DATA Software Sp. z o.o.
FELIETON 78
Redakcja dokłada wszelkich starań, by publikowane w piśmie i na towarzyszących mu nośnikach informacje
Michał Małecki i programy były poprawne, jednakże nie bierze odpowiedzialności za efekty wykorzystania ich; nie gwarantuje
także poprawnego działania programów shareware, freeware i public domain.
Uszkodzone podczas wysyłki płyty wymienia redakcja.
Wszystkie znaki firmowe zawarte w piśmie są własnością odpowiednich firm
ZAPOWIEDZI 82 i zostały użyte wyłącznie w celach informacyjnych.

Redakcja używa systemu automatycznego składu


Zapowiedzi artykułów, które planujemy następ- Do tworzenia wykresów i diagramów wykorzystano program firmy
nego wydania naszego pisma Osoby zainteresowane współpracą prosimy o kontakt: cooperation@software.com.pl

Druk: ArtDruk

Wysokość nakładu obejmuje również dodruki. Redakcja nie udziela pomocy technicznej w instalowaniu
i użytkowaniu programów zamieszczonych na płytach CD-ROM dostarczonych razem z pismem.
Sprzedaż aktualnych lub archiwalnych numerów pisma po innej cenie niż wydrukowana na okładce
– bez zgody wydawcy – jest działaniem na jego szkodę i skutkuje odpowiedzialnością sądową.

Pismo ukazuje się w następujących wersjach językowych:


polskiej , francuskiej , niemieckiej

PHP Solutions Nr 6/2006 www.phpsolmag.org 5


Aktualności

Achievo
PHP-GTK2
dostępny w wersji alfa
Wielkimi krokami zbliża się pierwsza stabil-
na wersja PHP-GTK2. W połowie lipca autorzy
A chievo to napisany w PHP system za-
rządzania projektami, który łączy cechy
typowego PM-a, CRM-a, HRM-a i PIM-a.
podziału pracy w zespole programistów, po-
zwala też na zarządzanie umowami zawar-
tymi z pracownikami i innymi wykonawca-
udostępnili wersję zeta projektu (którą potem
Możemy go łatwo wdrożyć do systemu ob- mi czy korzystanie z list zadań do wykona-
musieli z przyczyn technicznych przemianować
na alpha). Blisko połowa z 202 klas i 2 900 me- sługi klientów, oferując im podgląd postępu nia (TODO). Do dyspozycji mamy wiele plu-
tod została już ujęta w dokumentacji. Autorzy zamówionych prac w trakcie ich wykonywa- ginów, w tym kalendarz, notatnik i genera-
nie zdradzają planowanej daty wydania pierw-
szej stabilnej wersji PHP-GTK2, ale już teraz, w nia. Narzędzie działa na każdej platformie tor statystyk pracy. Na wsparcie techniczne
oparciu o wersje testowe, możemy tworzyć wła- oferującej PHP i MySQL, jest też wieloję- projektu składa się także forum i dedykowa-
sne aplikacje graficzne i poznawać jej zapowia-
daną funkcjonalność. zyczne i całkowicie darmowe. Sam Achievo na bugzilla. Opcjonalnie możemy poprosić o
http://gtk.php.net oferuje wiele udogodnień: generowane za płatną konsultację z deweloperami Achievo,
pomocą biblioteki GD2 statystyki czasu pra- zaś postęp prac obejrzymy na blogu auto-
Planeta PHP – wszystkie
cy, wykresy postępu rozwoju projektu, czy rów (http://www.achievo.org/blog)
newsy w jednym miejscu
PHP Planet prezentuje informacje powiązane z funkcję dodawania plików, które następnie
PHP, pochodzące z różnych źródeł dostępnych może przejrzeć i skomentować nasz kon- licencja: Open Source (własna)
w sieci. Treści nowości są pobierane z dziesią-
tek kanałów RSS najpopularniejszych blogów trahent. Achievo oferuje także wspomaganie http://www.achievo.org
oraz wortali poświęconych temu językowi. Do-
datkowo, na stronie projektu Planet Feed Re-
ader (http://planetplanet.org) znajdziemy odno-
śniki do innych witryn typu Planet: traktujących
o MySQL-u, eZ publish, czy PRADO.
http://planet-php.org

Symphony Framework
Symphony to nowy framework, udostępniany
na podstawie darmowej licencji MIT,komponu-
je,zachowuje, dzieli i wykonuje meta programy
( SHA99, LOR02). Mimo, że wersja 1.0 jesz-
cze się nie ukazała, już teraz projekt zaskakuje:
jest skierowany na aplikacje klasy Enterprise,
przy czym oferuje przyjazne linki, wsparcie dla
technologii AJAX i caching. Całość została na-
pisana w OOP PHP5 (architektura MVC). Pro-
jekt rozwija się z dnia na dzień, zaś jego bardzo
mocną stroną jest obszerny manual, wsparty fil-
mami w formacie QuickTime.
http://www.symphony-project.com

Wzorce projektowe dla PHP5


Na witrynie Patterns for PHP możemy zapo-
znać się z wieloma wzorcami projektowymi.
Przykłady są napisane pod kątem PHP5. Auto-
rzy reklamują, że nie znajdziemy tu opisów im-
plementacji wzorców z języka Java, a gotowe
rozwiązania, przygotowane ściśle dla develo- FeedCreator
perów PHP. Zaprezentowane wzorce są bardzo
ciekawe – warto poznać je, bowiem nie dla każ-
dego projektu najodpowiedniejszy jest MVC.
http://www.patternsforphp.com
W raz z rozwojem aplikacji webowych
nastała era czytników RSS. Sub-
skrybenci używają kanałów do czytania
phpDocumentor 1.3.0 stable informacji o nowościach w ich ulubionych
Zespół twórców phpDocumentora udostępnił wer-
serwisach internetowych – standardowy
sję 1.3.0 swojego produktu. Poprzednia stabilna
wersja tego przydatnego i popularnego narzędzia, sposób dostarczania newsów (na stronie
które ułatwia pisanie dokumentacji kodu PHP i głównej) ustępuje miejsca rosnącym w po-
jest standardem w tej dziedzinie, została oddana
do użytku w 2003 roku! Jak przyznają sami au- pularność rozwiązaniom. Czytniki RSS są
torzy, postęp w rozwoju projektu jest ogromny: implementowane także w komunikatorach
obecnie phpDocumentor w pełni obsługuje PHP5.
phpDocumentor należy do repozytorium PEAR. internetowych (ang. instant messaging ap-
http://pear.php.net/PhpDocumentor plication).
Dodanie czytnika RSS lub tworzenie tych zgodnych z PIE 1.0, OPML 1.0, Unix
Open Power Template
Po ponad roku kodowania udostępniona zosta- własnych kanałów wiadomości poszerzy mbox oraz Atom. Dodatkowo możemy
ła stabilna wersja 1.0.0 systemu szablonów o również funkcjonalność naszej witryny wygenerować plik w formacie HTML lub
nazwie Open Power Template. System ten po-
wstał jako podprojekt powstającego opensour- internetowej. Same kanały mają postać JavaScript. Chociaż projekt nie jest już
cowego forum dyskusyjnego (Open Power Bo- plików XML, jednak ich generacja może rozwijany przez głównego dewelopera, w
ard: http://openpb.net). Open Power Templa-
te został napisany w PHP5 przy zastosowaniu
przysporzyć trudności. Nie musimy jed- Internecie znajdziemy wiele implementa-
technik obiektowych. Obsługuje caching, kom- nak wyważać otwartych drzwi – z pomo- cji i rozszerzeń do tej klasy oraz jej doku-
presję gzip, posiada wbudowaną konsolę debu- cą przyjdzie nam rozpowszechniana na mentację i przykłady wykorzystania.
gującą oraz wsparcie dla wielojęzycznych sza-
blonów. Jest rozpowszechniany na zasadach licencji LGPL klasa FeedCreator. Skrypt
określonych w licencji LGPL. oferuje generację najpopularniejszych ty- licencja: LGPL
http://opt.openpb.net
pów wątków RSS: 0.9, 1.0, 2.0, ale także http://www.bitfolge.de/rsscreator-en.html

6 www.phpsolmag.org PHP Solutions Nr 6/2006


Aktualności

phpBB 3.0 Olympus Beta


net2ftp – klient FTP dla WWW Wielkimi krokami zbliża się premiera wersji 3.0

n
forum dyskusyjnego phpBB, noszącej nazwę
et2ftp to bardzo popularne rozwią- gów, szukać plików, używać załączonych
kodową Olympus. Autorzy projektu udostępnili
zanie, stosowane głównie na witry- do projektu edytorów HTML i PHP i two- już kilka wersji beta produktu. Spośród nowych
nach firm hostingowych. Jego funkcjonal- rzyć nowe pliki tekstowe. Aplikacja wspiera możliwości phpBB 3.0 warto wymienić tworze-
nie własnych kodów BBCode, usprawnione ke-
ność nie ustępuje najlepszym okienkowym transfer plików pomiędzy dwoma serwera- szowanie, powiadamianie o nowych wiadomo-
klientom FTP. Net2ftp umożliwia nawigację mi FTP. Projekt możemy łatwo wdrożyć do ściach przez Jabber lub XMPP oraz wysyłanie
treści tematów emailem. Obecna wersja działa
w katalogach, upload plików (także z auto- systemu CMS: na jego witrynie znajdziemy pod PHP4 i PHP5. Produkt jest dostępny na li-
matycznym rozpakowywaniem archiwów) moduły dla Mambo, Drupala i XOOPS-a. cencji GPL.
http://phpbb.com
oraz ich pobieranie. Pozwala też na archi- Łatwy w obsłudze system szablonów za-
wizację zbiorów do formatu ZIP, ich prze- pewnia prostą implementację do gotowe- Gnope
noszenie, kopiowanie i usuwanie, zmianę go layoutu naszej strony. Cały system zo- Wraz z nadejściem PHP-GTK2, Christian We-
iske rozpoczął prace nad projektem Gnope.
nazw plików i katalogów, użycie komen- stał przetłumaczony na ponad 10 języków i Efektem jest pakiet działający na wielu platfor-
dy CHMOD (zmiana uprawnień dostępu) łatwo możemy dodawać kolejne. mach, który ułatwia pisanie aplikacji PHP-GTK
oraz ich dystrybucję na najpopularniejsze plat-
czy podgląd kodu pliku w edytorze (wraz formy systemowe. Narzędzie to, w połączeniu
z podświetlaniem składni i numerowaniem licencja: GNU GPL z Glade, stanowi absolutny niezbędnik dewelo-
linii). Możemy też obliczać rozmiar katalo- http://www.net2ftp.com pera PHP-GTK. Projekt jest rozpowszechniany
na licencji LGPL.
http://www.gnope.org

Baza artykułów o PHP


Eioba to projekt, którego celem jest zgromadze-
nie w jednym miejscu artykułów krążących w
Sieci. W zasobach tego repozytorium znajdzie-
my m.in. pozycje traktujące o PHP i bazach da-
nych. Witryna dopiero nabiera wiatru w żagle,
ale już teraz jej zasoby są bogate. Możliwość
dodawania pozycji przez czytelników powoduje,
że projekt rokuje szybki rozwój. Funkcjonalność
strony obejmuje umieszczanie ocen, komenta-
rzy oraz korzystanie ze sprawnej wyszukiwarki.
http://www.eioba.com

Wikipedia dla każdego


MediaWiki to projekt pozwalający na założenie
własnej witryny stanowiącej internetową ency-
klopedię. Jest silnikiem słynnej Wikipedii (http:
//wikipedia.org). Autorzy MediaWiki oznajmili o

Mambo wciąż żyje rozpoczęciu nowej, stabilnej linii tego produktu


– 1.7. Główną zmianą w stosunku do poprzed-

W
nich edycji jest odejście od obsługi PHP4 i My-
raz z wydaniem Mambo o numerze
SQL-a 3.23.x. Lista wprowadzonych poprawek
4.5.3, w sierpniu 2005 roku, część jest imponująca – programiści Mediawiki dokła-
deweloperów tego CMS-a odeszła, aby dają wszelkich starań, by ich produkt był bez-
konkurencyjny. Mediawiki jest dostępne na li-
stworzyć Joomlę. Rozniosła się wieść, że cencji GNU GPL.
Mambo przestało istnieć, a na jego miej- http://www.mediawiki.org
sce tworzony był nowy Multisite CMS. Nic
Shoutbox w PHP i AJA X-ie
bardziej mylnego – projekt Mambo wciąż YShout to znane z wielu witryn (w tym forów
się rozwija, a ilość jego zwolenników ro- dyskusyjnych) rozwiązanie alternatywne dla
chata: shoutbox, czyli okienko, w którym (pra-
śnie. Mambo jest bezpieczną aplikacją: wie) na bieżąco możemy się komunikować z in-
bugi zostały naprawione, a dokumentacja nymi użytkownikami. Rewolucją w rozwiąza-
niach tego typu jest użycie technologii AJAX do
rozwinięta. Na stronie projektu znajdzie- odświeżania zawartości okienka – koniec z uży-
my m.in. tutoriale wykonane w technologii file użytkowników, czy systemy zarządza- ciem ramek! Można się spodziewać, że projekt
ten szybko znajdzie uznanie i zostanie zaimple-
Macromedia Flash, obrazujące wdrożenie nia reklamami. Mambo jest wyposażone
mentowany w najpoplarniejszych CMS-ach i fo-
i wykorzystanie systemu. Ilość szablonów i w potężny system keszujący, który skraca rach dyskusyjnych.
pluginów dla Mambo jest imponująca. czas generacji dokumentów, zachodzącej http://yurivish.com/yshout

Zalety Mambo: przyjazne URL-e, przy użyciu autorskiego systemu szablo- CMS na prywatną witrynę
wątki RSS, podziały na kategorie, zarzą- nów. Łącznie ilość modułów napisanych Flux jest nowym projektem typu CMS,
opartym na frameworku Popoon (http:
dzanie użytkownikami, łatwa integracja z pod Mambo jest bliska 300, dodatkowo w //www.popoon.org) i PHP5. Oferuje wsparcie
forami dyskusyjnymi, wielość wersji języ- repozytorium znajdziemy ponad 80 dar- dla wielu języków, posiada wbudowany edytor
kowych, profesjonalny edytor WYSIWYG mowych szablonów, 400 komponentów WYSIWYG, generator dokumentów PDF i wiele
innych. Korzysta z opartego na XSLT systemu
i ogromna, liczona w dziesiątkach tysię- i dziesiątki artykułów na temat systemu. szablonów; pozwala też na instalację wielu plu-
cy społeczność użytkowników, gwaran- Jeśli mamy uprzedzenia do Mambo po ginów, takich jak m.in. blog czy galeria. Nowo-
ścią w rozwiązaniach tego typu jest użycie tech-
tują pomoc i dostępność poszukiwanych rozłamie z 2005 roku, warto drugi raz się nologii AJAX w polu wyszukiwania – wyniki wy-
rozwiązań. Dodatkowe pluginy zawiera- przyjrzeć temu rozwiniętemu projektowi. świetlane są w trakcie pisania, co bardzo ogra-
nicza czas potrzebny na znalezienie pożąda-
ją m.in. systemy ankiet, fora dyskusyjne, nej informacji. Flux jest dostępny na zasadach
galerie, systemy uwierzytelniania w opar- Licencja: GNU GPL. określonych w licencji GNU GPL.
ciu o LDAP, kalendarze, rozszerzone pro- http://www.mamboserver.org http://www.flux-cms.org

PHP Solutions Nr 6/2006 www.phpsolmag.org 7


Aktualności

PHP w Wikibooks
Wikibooks to oparta na silniku MediaWiki wielo- MagpieRSS

P
języczna internetowa biblioteka swobodnie do-
rowadząc wortal często cytujemy in-
stępnych tekstów (m.in. książek). Dotyczy wie-
lu dziedzin (w tym informatyki) i jest tworzo- formacje o nowościach z zaprzyjaź-
na przez społeczność pasjonatów. W jej zaso- nionych źródeł. Dzięki wątkom RSS nie
bach znajdziemy m.in. ciekawy podręcznik PHP
oraz pozycje nt. technologii i rozwiązań takich, musimy już się męczyć z kopiowaniem
jak XHTML, CSS, XML Schema, CVS czy Sub- informacji na naszą stronę – możemy
version. Serwis rozwija się bardzo dynamicznie
– każdy podręcznik jest pisany przez kilku au- użyć darmowej, rozpowszechnianej na li-
torów i nic nie stoi na przeszkodzie, by dopisać cencji GNU GPL klasy MagpieRSS. Pro-
i swój rozdział.
jekt ten szybko zdobył sympatię dewelo-
http://wikibooks.org
perów PHP, głównie dzięki przejrzyste-
Biblioteka AJA X od Adobe mu kodowi i dużej szybkości działania.
Spry to AJAX-owy framework firmy Adobe.
Ułatwia on tworzenie aplikacji wykorzystują- Jego niewątpliwym atutem jest ilość ob-
cych AJAX-a, m.in. upraszczając wstawianie sługiwanych formatów wątków. Obecnie ły, co gwarantuje jego łatwe wdroże-
elementów dynamicznych do kodu HTML i w
dużej mierze zwalniając webmastera od ko-
są to: RSS 0.9, RSS 1.0, RSS 2.0 oraz nie oraz modyfikację. Na witrynie Mag-
nieczności programowania. Spry oferuje też Atom. pieRSS znajdziemy wiele odnośników
narzędzia graficzne pomocne w nawigacji i Nowatorskim rozwiązaniem uży- do aplikacji opartych o ten system, ta-
administracji na stronie WWW. Świetnie ob-
razują to przykłady dostępne na witrynie pro- tym w MagpieRSS jest zastosowanie kich jak: generator grafik PNG, agrega-
jektu, takie jak galeria ze skalowalnymi zdję- keszowania oraz nagłówków HTTP w tor wiadomości RSS, konwerter RSS
ciami, czy tabela, którą możemy sortować
– oczywiście, bez potrzeby odświeżania stro- celu rozpoznania, czy dokument zo- do JavaScript, czy specjalne edycje dla
ny. W połączeniu z PHP i bazą danych, Spry stał zmodyfikowany (ang. conditio- blogów Wordpress. Dodatkowo mamy
stanowi potężne narzędzie, pomocne zwłasz-
cza przy projektowaniu sklepów interneto-
nal GETs). Jakby tego było mało, ma- możliwość przejrzenia wielu artykułów
wych czy systemów klasy Enterprise. Frame- my możliwość korzystania z kompresji i tutoriali na temat użycia tej klasy.
work jest dostępny na licencji BSD. GZIP, uwierzytelniania HTTP oraz szy-
http://labs.adobe.com/technologies/spry
frowania transmisji za pomocą SSL. licencja: GNU GPL.
advAJA X 1.1 Cały projekt jest podzielony na modu- http://magpierss.sourceforge.net
Advanced AJAX to obiekt języka JavaScript,
pomocny przy tworzeniu dynamicznych witryn
internetowych korzystających z AJAX-a. Uła-
twia on m.in. ustawianie wartości argumen- Framework Prototype w praktyce
tów zapytania do serwera, pełną kontrolę nad
połączeniem klient-serwer i jego wznawianie
oraz korzystanie z pamięci podręcznej (ang.
cache). Możliwości advAJAX i specyfikę sa-
P rototype to darmowy framework dla
języka JavaScript, ułatwiający two-
rzenie aplikacji za pomocą technologii
mej technologii AJAX omówiliśmy w numerze
1/2006. Obecnie projekt rozwija się bardzo AJAX (a więc i PHP). Jego wszechstron-
dynamicznie; rośnie też jego dokumentacja
i przykłady zastosowań. Twórca advAJAX-a
ne zastosowania możemy zobaczyć na
pozwala zarówno na jego prywatny, jak i ko- stronie http://script.aculo.us. Ta ostatnia
mercyjny użytek. stanowi repozytorium przydatnych skryp-
http://advajax.anakin.us
tów, używanych przy tworzeniu aplikacji
Glade 3 klasy Enterprise, które z racji tego, że są
Po wielu miesiącach oczekiwania, Glade
– narzędzie pozwalające na łatwe, intuicyj-
rozbudowane i przesyłają dużo danych,
ne tworzenie graficznych interfejsów użytkow- generują poważne obciążenie serwera.
nika opartych na bibliotece GTK (dostępnej
Użycie technologii AJAX zmniejsza za- jektu o nazwie SortableFloats: użytkow-
również dla języka PHP dzięki PHP-GTK) do-
czekało się stabilnej wersji z linii 3. Znajdzie- potrzebowanie na dostęp do serwera, nik może zmienić kolejność elementów
my w niej mnóstwo nowych funkcji: obsługę gdyż – mówiąc w skrócie – dane są naj- na liście poprzez ich przeciąganie – od-
ikon, przenoszenie i możliwość skalowania
elementów GtkTable i GtkBox, nowe pale- pierw przenoszone do klienta (przeglą- wieczny problem z klikaniem “przesuń
ty kolorów, wspomaganie tworzenia pasków darki internetowej), a następnie przetwa- niżej”, występujący np. przy zmianie ko-
narzędzi (ang. toolbars) i menu oraz wiele in-
nych. Do działania, Glade 3 wymaga GTK+
rzane po jego stronie dopóki nie ulegną lejności wyświetlania wątków na forach
w wersji 2.8 oraz libxml2. Projekt jest rozpo- zmianie (można więc bez problemu np. dyskusyjnych powinien bezpowrotnie
wszechniany na licencji GNU GPL.
sortować zawartość tabeli, ale już mody- zniknąć.
fikacja zawartych w niej danych wymaga Rozwiązania zaprezentowane na
ich ponownego pobrania. script.aculo.us są stosowane m.in. na
W Prototype znajdziemy technikę stronach Apple, Digg, Feedburner, Base-
drag-and-drop, stosowaną w koszykach camp, czy Mailroom, a przede wszystkim
sklepów internetowych, automatyczne w bardzo innowacyjnym frameworku Ru-
uzupełnianie pól (używane w wyszuki- by on Rails.
warkach w celu dostarczenia wstępnych
wyników bez potrzeby przeładowania
strony), tabele, które możemy sortować
oraz mnóstwo efektów wizualnych (na- http://script.aculo.us
http://glade.gnome.org
jazdy, zanikanie, podświetlanie, itd.). Na http://prototype.conio.net
szczególną uwagę zasługuje demo pro- http://www.rubyonrails.org

8 www.phpsolmag.org PHP Solutions Nr 6/2006


Opis CD

2 filmy wideo: XML Development using Java oraz XML Based Web Applications
F irma KeyStone Learning przygoto-
wała specjalnie dla czytelników ma-
gazynu PHP Solutions następujące fil-
my: XML Development using Java oraz
XML Based Web Applications. Filmy
przydadzą się wszystkim deweloperom.
Pierwszy z filmów omawia zasto-
sowanie Javy w technologii XML. Au-
torzy wyjaśniają możliwości, które ofe-
ruje Java deweloperom. Dowiesz się w
jaki sposób zbudować wyszukany użyt-
kowy program szybciej i po niższych
kosztach.
Natomiast z filmu XML Based Web
Applications znajdziesz odpowiedź na
pytanie jaką rolę odgrywa XML w prze-
twarzaniu danych w formacie B2B w
aplikacjach internetowych. Życzymy
przyjemnego oglądania i nauki. Rysunek 1. XML Development using Java

Dodatkowe materiały

Z mieściliśmy również dodatkowe apli-


kacje:

• XAMPP – zestaw aplikacji umożliwiają-


cy uruchomienie serwera WWW i bazy
danych kilkunastoma kliknięciami. W
jego skład wchodzą: Apache, MySQL,
PHP 4.3.X i 5.0.X, Perl, FileZilla FTP
Server, phpMyAdmin, OpenSSL, Fre-
etype, Webalizer, mod_perl, Truck MM
Cache, mcrypt, SQlite, JpGraph, Mer-
cury Mail Transport System, PHPBlen-
der, PHP Compiler;
• Wampserver – odpowiednik pakietu
LAMP dla Linux i FAMP dla FreeBSD.
Jest to pakiet do bsługi witryn interne-
towych w środowisku MS Windows.
Zawiera m.in.: serwer Apache, język
skryptowy PHP, bazę danych MySQL i Rysunek 3. XAMP
oprogramowanie uzupełniające;
• PHP-Qt – Qt to przenośne bibliote-
ki dla języka C++. Klasy stanowią
ich znaczną część i służą do budo-
wy interfejsu graficznego dla pro-
gramów komputerowych. Qt jest do-
stępna dla wielu platform. Urządzeń
wbudowanych. Qt jest podstawą dla
wielofunkcyjnej przeglądarki interne-
towej Opera i uniksowego środowi-
ska KDE. Qt zawiera zhierarchizo-
wany system zdarzeń,technologie
programowania GUI i automatyczne
rozmieszczanie widżetów.
• Nowe e-booki: Auditing Your Web Si-
te Security, PHP Power Programming,
OASIS OpenDocument Essentials. Rysunek 4. Wamp

10 www.phpsolmag.org PHP Solutions Nr 6/2006


Dla początkujących

Testy wydajności
i profilowanie aplikacji PHP
Stopień trudności: lll
Łukasz Witczak

Każda aplikacja – webowa czy dowolna inna


charakteryzuje się określoną wydajnością.
Cecha ta staje się szczególnie istotna w świecie
aplikacji internetowych, z których korzystają
czasem tysiące użytkowników...

Z
większenie wydajności jest mo- uprawnienia administratora (środowisko
żliwe dzięki optymalizacji kodu. Linux) poleceniem:
Problem w tym, że dzisiejsze
aplikacje pisane w PHP rozrosły się pear install benchmark
do takich rozmiarów, iż ciężko przej-
rzeć cały kod i poprawić jego efektyw- PEAR::Benchmark składa się z trzech
ność. Znajdując wąskie gardło w syste- klas: Benchmark _ Iterate, Benchmark _
mie można je zoptymalizować i w mia- Profiler i Benchmark _ Timer.
rę niewielkim kosztem podnieść wy-
dajność całej witryny. W tym artyku- Benchmark_Timer
le pokażemy, jak testować wydajność Benchmark _ Timerto klasa pozwalająca
W SIECI zarówno całej aplikacji, jak i wybra- nam mierzyć czas, jaki upłynął między
nych fragmentów kodu oraz jak iden- kolejnymi tzw. markerami. Markery to
tyfikować te partie kodu, które są przy- miejsca pomiaru czasu, które ustawia-
l http://pear.php.net/package/
czyną największego narzutu czaso-
Benchmark – strona domowa
wego.
l
pakietu PEAR::Benchmark
http://www.linuxjournal.com/
Co należy wiedzieć...
article/7213 – artykuł o Powinieneś znać podstawy programo-
profilowaniu przy pomocy Testy wydajności wania proceduralnego i obiektowego
APD z PEAR::Benchmark w PHP5.
http://wiki.cc/php/Apd
Co obiecujemy...
l

– Wiki o APD Benchmark to pakiet z repozytorium PE-


l http://www.omniti.com/ AR (http://pear.php.net) zawierający kla- Pokażemy, jak testować wydajność ca-
~george/talks/Profiling- łej aplikacji oraz jej poszczególnych
sy umożliwiające testowanie wydajności
phpworks-2004.pdf fragmentów i nauczymy, jak identyfiko-
– PDF o profilowaniu aplikacji i profilowanie kodu napisanego w PHP. wać wąskie gardła każdego programu.
w PHP Pakiet ten można zainstalować mając

12 www.phpsolmag.org PHP Solutions Nr 6/2006


Profilowanie aplikacji PHP Dla początkujących

Rysunek 1. Wynik działania klasy Benchmark_Profiler

my wywołaniem metody setMarker(). fikację markerów w wynikach. Warto Pomiar czasu


Na Listingu 1 widzimy funkcję doSth(), również zaznaczyć, że wywołanie me- rejestracji użytkownika
której jedynym celem jest generowanie tod start() i stop() również powoduje Zawartość Listingu 3 jest już bardziej po-
obciążenia. powstanie markerów, których nazwy to dobna w działaniu do typowej aplikacji in-
Aby użyć klasy Benchmark _ Tim- odpowiednio Start i Stop. ternetowej. Mamy tam operacje na ba-
er, musimy ją dołączyć do bieżące- Metoda getProfiling() zwraca tabli- zie danych i na ciągach. Przedstawiony
go skryptu w sposób typowy dla klas ce z wynikami. Tablica ta (dla naszego skrypt odpowiada za rejestrację nowego
PEAR (zakładamy, że include _ path przykładu) wygląda jak na Listingu 2. użytkownika w naszym serwisie interne-
wskazuje położenie repozytorium PE- Tablica ta jest ponumerowana od 0 do towym. Darujemy sobie tworzenie formu-
AR na dysku lokalnym). Następnie n-1, gdzie n to liczba markerów (pamię- larza i przykładowe dane, które normal-
wywołujemy konstruktor klasy Bench- tajmy, że wywołanie start() i stop() nie wprowadziłby użytkownik, zapiszemy
mark _ Timer. Jak już wiemy, zadaniem również liczy się jako marker). Każdy do zmiennych. Wcześniej jednak musi-
funkcji doSth() jest generowanie ob- element tej tablicy to kolejna tablica za- my dołączyć do skryptu niezbędne klasy
ciążenia, co uzyskujemy poprzez wy- wierająca cztery elementy: name – na- i funkcje: Benchmark/Timer.php posłuży
konywanie czasochłonnych operacji, zwa markera, time – uniksowy znacz- do pomiaru wydajności, filter.php (Listing
takich jak tworzenie tablicy składają- nik czasu zapisany w momencie wy- 4) zawiera funkcje filtrujące numer telefo-
cej się z 1000 elementów i jej sorto- wołania markera, diff – czas w se- nu i komentarze, a w generators.php (Li-
wanie według kluczy w porządku od- kundach, jaki upłynął miedzy obec- sting 5) deklarujemy funkcję generującą
wrotnym. Po utworzeniu tej funkji po- nym a poprzednim markerem (w przy- losowe hasło.
zostaje nam już tylko uruchomienie padku Start jest to znak „-”, ponieważ Po załadowaniu odpowiednich plików
pomiaru czasu: $benchmark->start();. nie było wcześniejszego pomiaru) oraz tworzymy nową instancję klasy Bench-
Od tego momentu będzie mierzo- total – całkowity czas, jaki upłynął od mark _ Timer i uruchamiamy pomiar cza-
ny czas, jaki upłynął między kolejny- wywołania pierwszego markera do bie- su. Ustawiamy zmienne symulujące da-
mi markerami aż do momentu zakoń- żącego. ne użytkownika. Następnie filtrujemy nu-
czenia pomiarów wywołaniem $bench- No dobrze, ale po co nam tyle mar- mer telefonu tak, żeby zawierał same cy-
mark->stop();. kerów do pomiaru czasu wykonania fry wywołując funkcję telephone($phone)
Tuż przed i po wywołaniu doSth() jednej funkcji? Markery stosujemy, aby oraz oraz oczyszczamy komentarz
ustawiamy nasze markery wywołu- oznaczyć miejsca w kodzie, dla których ze znaczników HTML mogących po-
jąc: $benchmark->setMarker( 'Marker mierzymy czas wykonania i dla jednej służyć do ataku XSS: $comment =
#1' );. Argumentem metody setMark- funkcji wystarczyłoby wywołać metody comments($comment). Następnie generuje-
er() jest nazwa markera – może ona start() i stop(). My jednak chcemy zi- my losowe hasło ($pass = passGen()), łą-
być dowolna, ale dobrze jest nadawać dentyfikować najwolniej działające par- czymy się z bazą danych i umieszczamy
nazwy ułatwiające późniejszą identy- tie kodu w większym skrypcie. w niej przykładowe dane (tabela bazo-

Rysunek 2. Wynik profilowania skryptu

PHP Solutions Nr 6/2006 www.phpsolmag.org 13


Dla początkujących Profilowanie aplikacji PHP

danowa, na której wykonujemy te opera-


cje, została przedstawiona na Listingu 6). Listing 2. Efekt wykonania skryptu z listingu 1
Na koniec skryptu wyświetlamy zmienne
array(4) {
$phone i $comment zawierające przefiltro- [0]=> array(4) {
wane dane wejściowe w celu sprawdze- ["name"]=> string(5) "Start"
nia, czy są poprawne. Oczywiście nie za- ["time"]=> string(19) "1153494385.64062800"
pominamy o podaniu wyników pomiarów ["diff"]=> string(1) "-"
["total"]=> string(1) "-"
poprzez wywołanie var _ dump( $bench-
}
mark->getProfiling()). Pomiędzy wspo- [1]=> array(4) {
mnianymi operacjami wstawiamy punkty ["name"]=> string(9) "Marker #1"
pomiaru czasu i sprawdzamy wyniki. Li- ["time"]=> string(19) "1153494385.64066500"
sting 7 przedstawia rezultaty, jakie uzy- ["diff"]=> string(8) "0.000037"
["total"]=> string(8) "0.000037"
skałem na mojej maszynie testowej. Naj-
}
pierw widzimy efekt filtrowania zmiennych [2]=> array(4) {
zawierających telefon i komentarz, a na- ["name"]=> string(9) "Marker #2"
stępnie czasy wykonania poszczególnych ["time"]=> string(19) "1153494385.64189600"
części kodu. ["diff"]=> string(8) "0.001231"
["total"]=> string(8) "0.001268"
}
Benchmark_Profiler [3]=> array(4) {
Wyniki z Listingu 7 są trudne w analizie: ["name"]=> string(4) "Stop"
zwłaszcza, jeżeli ustawimy wiele pułapek ["time"]=> string(19) "1153494385.64191900"
mierzących czas. Mamy jednak do dyspo- ["diff"]=> string(8) "0.000023"
["total"]=> string(8) "0.001291"
zycji klasę Benchmark _ Profiler, która po-
}
wie nam, jaki jest procentowy udział po- }
szczególnych sekcji skryptu w czasie jego
wykonania. Na Listingu 8 przedstawiamy Listing 3. Typowe operacje w aplikacjach internetowych
zmodyfikowany kod z Listingu 3.
require 'Benchmark/Timer.php';
Przyjrzyjmy się bliżej zmianom w tym require './filter.php';
kodzie. Przede wszystkim ładujemy teraz require './generators.php';
plik z klasą Benchmark _ Profiler i tworzy- $benchmark = new Benchmark_Timer();
$benchmark->start();
// przykładowe dane z formularza
Listing 1. Prosty przykład $phone = '(33) 455-44-55';
wykorzystania klasy Benchmark_ $comment = 'Some comments <script> ... </script>, Some comments <script> ... </script>
Timer <a href="www.somedangerouslink.com"> Safe text <a href="javascript:alert("XSS")">
ClickMe!</a> - <body onload=alert("vulnerable")> Safe text <iframe src=http://
<?php www.notsafe.com/script.html>';
require 'Benchmark/Timer.php'; $benchmark->setMarker( 'Variables set' );
$benchmark = new Benchmark_Timer(); // filtrujemy telefon
function doSth(){ $phone = telephone( $phone );
$tmp = range(0, 999 ); $benchmark->setMarker( 'Phone filtred' );
krsort( $tmp ); //filtrujemy komentarze
return $tmp; $comment = comments( $comment );
} $benchmark->setMarker( 'Comments filtred' );
// rozpoczynamy pomiar // generujemy losowe hasło
$benchmark->start(); $pass = passGen();
// ustawiamy marker #1 $benchmark->setMarker( 'Password generated' );
$benchmark->setMarker( // wykonujemy operacje na bazie danych
'Marker #1'); $conn = mysql_connect( 'localhost', 'root', '' );
// wywołujemy f-cję doSth() mysql_select_db( 'phpsolmag', $conn );
$result = doSth(); $benchmark->setMarker( 'Connection established' );
// ustawiamy marker #2 $sql = 'INSERT INTO users(`pass`, `addDate`, `phone`, `comment`)
$benchmark->setMarker( VALUES( "'. $pass .'", "'. date("Y-m-d H:i:s") .'", "'. $phone .'",
'Marker #2'); "'. $comment .'" )';
// kończymy pomiar mysql_query( $sql );
$benchmark->stop(); $benchmark->setMarker( 'Inertion done' );
$benchmark->stop();
// wyświetlamy informacje uzyskane // wynik filtrowania
// podczas testu echo 'Phone: '. $phone . '<br />';
echo '<pre>'; echo 'Comments: '. $comment;
var_dump($benchmark-> // wynik pomiarów
getProfiling()); echo '<pre>';
echo '</pre>'; var_dump( $benchmark->getProfiling() );
?>

14 www.phpsolmag.org PHP Solutions Nr 6/2006


Profilowanie aplikacji PHP Dla początkujących

my instancję tej klasy. Podobnie jak po- Analizując wyniki widzimy, że naj- Nakazuje ona uruchomić funkcję o
przednio, pomiary rozpoczynamy i koń- więcej czasu zajmuje połączenie się z nazwie podanej jako drugi argument
czymy metodami start() i stop(). Tutaj bazą danych i wstawienie nowego re- i z parametrem przesłanym jako trze-
podobieństwa z poprzednim przykładem kordu w tabeli bazodanowej. Nieste- ci argument. Liczbę iteracji (wywo-
się kończą, bowiem w przypadku Bench- ty, operacje wejścia/wyjścia są najbar- łań) określa pierwszy argument. My
mark _ Profiler zamiast markerów używa- dziej kosztowne, stąd dobrą metodą na testujemy funkcję com ments() z argu-
my sekcji. Oprócz tego, że mierzony jest zwiększenie szybkości działania apli- mentem $com ment 1000 razy. Wywo-
globalny czas wykonania między wywo- kacji jest keszowanie (ang. caching). W łanie get() zwraca nam tablicę, która
łaniami start() i stop(), osobne pomia- naszym przypadku nie możemy użyć tej zawiera czas wykonania kodu w każ-
ry dokonywane są dla sekcji oznaczo- ostatniej techniki, gdyż przyspiesza ona dej iteracji (klucze są ponumerowane
nych metodami enterSection() i leave- odczyt, a my dodajemy dane do bazy. wg kolejnych liczb naturalnych) oraz
Section() oznaczającymi początek i ko- Jeśli bardzo zależy nam na wydajności, czas średni (klucz mean) i liczbę itera-
niec sekcji. to możemy się zająć funkcjami filtrują- cji (klucz iterations). Ważną rzeczą
Argumentem każdej z tych metod cymi nr telefonu i komentarz, co powin- podczas przygotowywania testu jest
jest nazwa sekcji, którą ustalamy we- no dać nam kilka procent zysku na cza- dobór liczby iteracji. Abyśmy mogli
dług naszych upodobań. W przypad- sie wykonania. Przyjrzyjmy się więc bli- mówić o wiarygodnych testach, mu-
ku pomiaru czasu wykonania funkcji żej funkcjom telephone() i comments(). simy przeprowadzić co najmniej 1000
bądź metod można zastosować sztucz- cykli. Kod z Listingu 10 wyświetla tyl-
kę, która wyłącza z pomiarów czas po- Benchmark_Iterate ko czas średni, bo on jest dla nas naj-
trzebny na samo wywołanie funkcji i po- Czas wykonania skryptu i jego poszcze- istotniejszy:
nowne zwrócenie sterowania do miej- gólnych części zależy od wydajności
sca wywołania funkcji lub metody. W maszyny, na której odbywa się test oraz Mean time: 0.000047s for 1000
tym celu możemy wewnątrz funkcji za- jej obciążenia. Łatwo zauważyć, że cza- iterations of telephone()
deklarować, że zmienna zawierająca sy te różnią się za każdym wywołaniem, Mean time: 0.000141s for 1000
obiekt klasy Benchmark _ Profiler jest ale najczęściej oscylują wokół jakiejś iterations of comments()
globalna i utworzyć sekcje wewnątrz tej wartości, z rzadka odbiegając znaczą-
funkcji (Listing 9). Wynik działania testu co do normy. Aby pozbyć się tych nie- Teraz mamy bardziej wiarygodne wy-
uzyskamy wywołując metodę display() dogodności, powinniśmy wykonać test niki i możemy przystąpić do optymali-
pod koniec skryptu, a rezultat przedsta- w pętli i uśrednić uzyskane czasy. Kla- zacji kodu funkcji comments() (Listing
wiamy na Rysunku 1. sa Benchmark _ Iterate pozwala na wy- 11). Choć techniki optymalizacyjne nie
Patrząc na Rysunek 1 od razu za- konywanie funkcji lub metody w pę- są przedmiotem tego artykułu, to musi-
uważymy kolumnę oznaczoną znakiem tli o zadanej liczbie iteracji. Na Listin- my tu wspomnieć o kilku sposród nich,
„%”, która ilustruje procentowy udział gu 10 pokazujemy przykład wykorzysta- aby wiedzieć, jak uzyskać wzrost wy-
każdej operacji w czasie wykonania nia tej klasy wobec funkcji filtrującej ko- dajności. Nasza funkcja widoczna na
skryptu między start() a stop(). Ostat- mentarze. Listingu 4 wykorzystuje do swego dzia-
ni wiersz oznaczony jako Global ozna- Jak widać, nie jest to skomplikowa- łania wyrażenia regularne za pomocą
cza cały skrypt (a w zasadzie cześć po- ne: po stworzeniu instancji klasy Bench- funkcji ereg _ replace(). Pewnie więk-
między początkiem a końcem pomia- mark _ Iterate uzyskujemy dostęp do szość z Was wie, iż ereg _ replace()
rów), więc zajmie on 100% czasu wyko- jej metod służących testom i wyświe- jest zdecydowanie wolniejsza niż per-
nania. Kolumna #calls pokazuje, ile razy tlaniu wyników. Kluczowa jest następu- eg _ replace() – możemy więc wyko-
dana sekcja była wywołana, calls zawie- jąca linia: rzystać tę szybszą. Ponieważ jednak
ra nazwy sekcji, jakie zostały wywołane mamy tylko pozbyć się tagów, więc nie
wewnątrz sekcji odpowiedniej dla każde- $benchmark->run( 1000, 'comments', potrzebujemy w ogóle używać wyra-
go wiersza wyników. $comment ); żeń regularnych: wystarczy wbudowa-

Listing 4. Funkcje filtrujące Listing 5. Funkcja generująca


losowe hasło
<?php
function telephone( $telephone ){ <?php
$telephone = ereg_replace( " ", "", $telephone ); function passGen($passLength=10,
$telephone = ereg_replace( "-", "", $telephone ); $startChr=33,$endChr=126){
$telephone = ereg_replace( "\(", "", $telephone ); $password = '';
$telephone = ereg_replace( "\)", "", $telephone ); while ( $passLength-- ){
return $telephone; $password.=chr(mt_rand(
} $startChr,$endChr));
function comments( $comment ){ }
$comment = ereg_replace( "<[^>]*>", "", $comment ); return $password;
return $comment; }
}
?> ?>

PHP Solutions Nr 6/2006 www.phpsolmag.org 15


Dla początkujących Profilowanie aplikacji PHP

na do PHP funkcja strip _ tags(), któ-


ra powinna być znacznie szybsza niż te Listing 7. Wynik wykonania skryptu z Listingu 3
pierwsze. W skrypcie z Listingu 10 do- Phone: 334554455
dajemy plik z poprawioną funkcją filtrują Comments: Some comments ... , Some comments ... Safe text ClickMe! - Safe text
(filter2.php).Na Listingu 12 przedstawia- array(8) {
my zmodyfikowany test wydajności i po- [0]=> array(4) {
["name"]=> string(5) "Start"
równanie funkcji comments() przed i po
["time"]=> string(19) "1153815327.15625200"
optymalizacji. ["diff"]=> string(1) "-"
Porównując wyniki uzyskałem o ["total"]=> string(1) "-"
ponad 60% krótszy czas wykonania }
funkcji com ments(). Jest to dowód na [1]=> array(4) {
["name"]=> string(13) "Variables set"
to, że lepiej jest używać funkcji wbu-
["time"]=> string(19) "1153815327.15628900"
dowanych w PHP, niż samemu two- ["diff"]=> string(8) "0.000037"
rzyć rozwiązania o podobnej funkcjo- ["total"]=> string(8) "0.000037"
nalności. Wniosek ten nie jest nowy }
i często można spotkać podobne w [2]=> array(4) {
["name"]=> string(13) "Phone filtred"
podręcznikach PHP. Jednak PHP za-
["time"]=> string(19) "1153815327.15635200"
wiera ponad 2000 funkcji wbudowa- ["diff"]=> string(8) "0.000063"
nych (napisanych w C) i czasem moż- ["total"]=> string(8) "0.000100"
na pominąć tę, której potrzebujemy. }
Profilowanie i testy wydajności zmu- [3]=> array(4) {
["name"]=> string(16) "Comments filtred"
szają nas do poszukiwania bardziej
["time"]=> string(19) "1153815327.15654000"
optymalnych rozwiązań tam, gdzie ["diff"]=> string(8) "0.000188"
przyniesie to największy przyrost wy- ["total"]=> string(8) "0.000288"
dajności. }

Poprawianie Listing 8. Użycie Benchmark_Profiler na przykładzie kodu z Listingu 3


dokładności wyników <?php
Profilowanie przy pomocy programów require 'Benchmark/Profiler.php';
działających w przestrzeni użytkowni- require './filter_profiler.php';
ka (w naszym przypadku napisanych require './generators.php';
$profiler = new Benchmark_Profiler();
w PHP) ma zasadniczą wadę, którą jest
$profiler->start();
przekłamanie wynikające z narzutu cza- $phone = '(33) 455-44-55';
sowego na wykonanie testu. Postaramy $comment = 'Some comments <script> ... </script>,
się oszacować ten narzut, aby potem Some comments <script> ... </script>
odjąć go od wyniku profilowania. W tym <a href="www.somedangerouslink.com"> Safe text
<a href="javascript:alert("XSS")">ClickMe!</a> - <body
celu napiszemy własną klasę dziedzi-
onload=alert("vulnerable")> Safe text
czącą po Benchmark _ Iterate. Na Li- <iframe src=http://www.notsafe.com/script.html>';
stingu 13 przedstawiamy nową klasę My- $phone = telephone( $phone );
Iterate. Nadpisujemy w niej tylko dwie $comment = comments( $comment );
metody oryginalnej klasy: run() i get().
// tworzymy sekcje
W run() najpierw wywołujemy metodę
$profiler->enterSection('passGen');
klasy macierzystej: $pass = passGen();
$profiler->leaveSection('passGen');
parent::run( $iterationsCount,
$functionName, $arguments); // tworzymy sekcje
$profiler->enterSection('DB connection');
$conn = mysql_connect( 'localhost', 'root', '' );
Listing 6. Tabela 'users' z bazy mysql_select_db( 'phpsolmag', $conn );
'phpsolmag' $profiler->leaveSection('DB connection');
$sql = 'INSERT INTO users(`pass`, `addDate`, `phone`, `comment`)
-- Baza danych: 'phpsolmag' VALUES( "'. $pass .'", "'. date("Y-m-d H:i:s") .'", "'.
-- Struktura tabeli dla 'users' $phone .'", "'. $comment .'" )';
CREATE TABLE 'users' ( $profiler->enterSection('DB insertion');
'ID' int(11) NOT NULL, mysql_query( $sql );
'pass' varchar(16) NOT NULL, $profiler->leaveSection('DB insertion');
'addDate' datetime NOT NULL, $profiler->stop();
'phone' int(11) NOT NULL,
'comment' text NOT NULL, // wyświetlamy wynik
PRIMARY KEY ('ID') $profiler->display();
) TYPE=MyISAM AUTO_INCREMENT=115;

16 www.phpsolmag.org PHP Solutions Nr 6/2006


Dla początkujących Profilowanie aplikacji PHP

Rysunek 3. Wynik po zmianie flagi na „–u”


z wcześniej przygotowanymi argumentami, Na Listingu 14 pokazujemy wykorzysta- wynikami widać, że udział czasu przefil-
a następnie w pętli obliczamy narzut czaso- nie tej klasy na przykładzie naszej funkcji trowania komentarzy spadł poniżej jedne-
wy, jaki powoduje wykonanie pomiarów: do filtrowania komentarzy. go procenta. Tym samym wydajność całej
Przy powtórzonych testach okaza- aplikacji wzrosła o kilka procent. To zbyt
$benchmark->setMarker( '_start_'.$i ); ło się, że zoptymalizowana funkcja com- mało, żeby poczuć satysfakcję, ale pa-
$benchmark->setMarker( '_stop_'.$i ); ments() działa nawet 75% szybciej (bez miętajmy, że nasz przykład jest specjal-
eliminacji narzutu czasowego pomiarów nie uproszczony w celach dydaktycznych.
Wynik zapamiętujemy w zmiennej składo- było to ponad 60%). Widać więc, że sa- Rzeczywista aplikacja ma więcej kodu,
wej $overhead. Metody setMarker() wy- me testy mogą częściowo zafałszować co daje nam więcej szans na znalezienie
glądają znajomo, ponieważ ich działa- rezultaty, więc dobrze jest to uwzględnić większego zysku czasu wykonania.
nie jest identyczne, jak w przypadku klasy w wyniku końcowym.
Benchmark _ Timer. Jest to w zasadzie ta Zalety i wady pakietu Benchmark
sama metoda, gdyż klasa Benchmark _ It- Wynik Instalacja pakietu Benchmark jest bardzo
erate dziedziczy z klasy Benchmark _ Tim- Sprawdźmy teraz, jak bardzo wzrosła wy- łatwa; często stanowi on element stan-
er. Przejdźmy do metody get(): używa- dajność całej aplikacji po dokonanych dardowej instalacji PHP, co pozwala nam
my jej aby uzyskać wyniki testów, więc naj- zmianach. Na Listingu 15 pokazujemy szybko i bezproblemowo wykorzystać je-
pierw wywołujemy tę samą metodę dla kla- wykorzystanie klasy Benchmark _ Profil- go potencjał.
sy Benchmark _ Iterate: er w celu zmierzenia wydajności aplika- Podstawową wadą klas Benchmark
cji ze zmodyfikowanym kodem. Porównu- jest wspomniany wcześniej dodatkowy
$result = parent::get(); jąc uzyskane rezultaty z wcześniejszymi narzut czasowy podczas testów, który

a później od uzyskanego wyniku odej-


Parametry skryptu pprofp
mujemy wcześniej oszacowany narzut:
Składnia pprofp jest następująca:
$result['mean'] -= $this->overhead;
pprofp <opcje> <plik>

Listing 9. Przykład wyłączenia Oto opcje sortowania:


czasu samego wywołania funkcji na
przykładzie z Listingu 4 -a: sortowanie wg nazw procedur,
-l: sortowanie wg liczby wywołań procedur,
<?php
-r: sortowanie wg rzeczywistego czasu spędzonego w procedurach.
function telephone($telephone){
global $profiler; -R : sortowanie wg rzeczywistego czasu spędzonego w procedurach z uwzględnieniem
$profiler->enterSection( wywołań procedur potomnych,
'telephone'); -s: sortowanie wg czasu systemowego spędzonego w procedurze,
$telephone=preg_replace( -S: sortowanie wg czasu systemowego spędzonego w procedurach z uwzględnieniem
"(\D)", "", $telephone ); wywołań procedur potomnych,
$profiler->leaveSection( -u: sortowanie wg czasu użytkownika spędzonego w procedurach,
'telephone'); -U : sortowanie wg czasu użytkownika spędzonego w procedurach z uwzględnieniem
return $telephone;
wywołań procedur potomnych,
}
-v: sortowanie wg średniego czasu spędzonego w procedurach,
function comments($comment){
-z: Sortowanie (domyślne) wg czasu systemowego i użytkownika spędzonego w pro-
global $profiler;
$profiler->enterSection( cedurach.
'comments');
$comment = strip_tags( Opcje wyświetlania:
$comment );
$profiler->leaveSection( -c: wyświetlenie czasu rzeczywistego w całym drzewie wywołań.
'comments'); -i: wyłączenie z raportu funkcji wbudowanych w PHP,
return $comment; -O <cnt>: określa maksymalna liczbę wyświetlanych procedur (domyślnie 15),
} -t: wyświetla skompresowane drzewo wywołań,
?> -T: wyświetla nieskompresowane drzewo wywołań.

18 www.phpsolmag.org PHP Solutions Nr 6/2006


Profilowanie aplikacji PHP Dla początkujących

Rysunek 4. Czas wykorzystania procesora przez poszczególne funkcje


może zafałszować wyniki. Istnieje jed- PHP i opisujemy ją w ramkach Instala- Zmienna zend _ extension wskazuje lo-
nak jeszcze inna wada korzystania z cja pod Windows oraz Instalacja pod Li- kalizację APD, zaś apd.dumpdir określa
programów profilujących, które działa- nuksem. Po pomyślnej instalacji powinni- miejsce na dysku, w którym będą zapisy-
ją w przestrzeni użytkownika. Zapew- śmy zobaczyć informacje o APD wywołu- wane wyniki profilowania.
ne zauważyliście, ile dodatkowych linii jąc phpinfo().
kodu musimy dopisać w różnych miej- Instalacja APD pod Windows
scach aplikacji, aby dało się ją prze- Instalacja APD pod Linuksem Instalacja APD pod Windowsd zależy
testować. Wielkość dodatkowego ko- Aby skorzystać z APD pod Linuksem, mu- głownie od wersji PHP, z którą APD ma
du jest różna w zależności od tego, co simy mieć zainstalowany pakiet php-de- współpracować. Najprostsza jest instala-
chcemy konkretnie zbadać (pojedyn- vel, który zawiera podstawowe narzędzia cja dla PHP5, którą tu opiszemy.
czą funkcję, całość czy część aplika- do tworzenia rozszerzeń PHP. W więk- Pierwszym, co musimy zrobić, jest po-
cji) i od wybranego pakietu testujące- szości dystrybucji Linuksa zainstalujemy branie ze strony http://pecl4win.php.net/
go (istnieją inne, niż te zawarte w PE- go używając apt-get: ext.php/php_apd.dll binarnej wersji biblio-
AR). Te wady są (przynajmniej czę- teki (musimy wybrać odpowiedni plik wer-
ściowo) eliminowane przez programy apt-get php-devel sji PHP) oraz jej skopiowanie do katalo-
profilujące, które wykorzystują rozsze- gu, w którym PHP szuka rozszerzeń. Fol-
rzenia ZEND. potem możemy zainstalować samo APD: der ten jest określony przez zmienną ex-
tension _ dir w pliku php.ini. Następnie
APD (Advanced PHP pear install APD w sekcji, w której dodajemy rozszerze-
Debugger) nia, musimy dopisać: extension=php _
APD jest pakietem PECL (http:// Teraz w pliku php.ini w sekcji [Zend] dopi- apd.dll, a w sekcji [Zend]:
pecl.php.net), czyli rozszerzeniem PHP. szemy ustawienia:
Śledzi on uruchomienia i zakończenia zend_extension_debug_ts =
zarówno funkcji użytkownika, jak i tych [Zend] "x:\lokalizacja\apd\php_apd.dll"
wbudowanych w PHP. Ponadto APD za- ... apd.dumpdir = x:\tmp
pisuje informacje o wywołaniach in- zend_extension = /lokalizacja/apd.so apd.statement_trace = 0
clude i require. Instalacja pakietu za- apd.dumpdir = /katalog/z/plikiem/sledzenia
leży od systemu operacyjnego i wersji apd.statement_trace = 0 Zmienna zend _ extension _ debug _ ts
określa lokalizację APD, a apd.dump-
Listing 10. Wykorzystanie klasy Benchmark_Iterate wobec funkcji filtrującej dir – katalog, w którym będą zapisywane
komentarze wyniki profilowania. Musimy jeszcze zdo-
być plik pprofp, który jest skryptem PHP
<?php
i służy do analizowania tych wyników.
require 'Benchmark/Iterate.php';
Plik ten znajduje się w pakiecie źródło-
require './filter.php';
$benchmark = new Benchmark_Iterate(); wym, który można pobrać ze strony http:/
$comment = 'Some comments <script> ... </script>, Some comments <script> ... /pecl.php.net/package/apd.
</script> Instalacja APD dla PHP4 jest nieco
<a href="www.somedangerouslink.com"> Safe text <a
bardziej skomplikowana, a jej opis w języ-
href="javascript:alert("XSS")">ClickMe!</a> - <body
ku angielskim można znaleźć pod adre-
onload=alert("vulnerable")> Safe text <iframe src=http://www.notsafe.com/
script.html>'; sem: http://wiki.splitbrain.org/php:apd.
// uruchamiamy test
$benchmark->run( 1000, 'comments', $comment ); APD w akcji
$commentResult = $benchmark->get();
Po pomyślnej instalacji APD chce-
my wypróbować jego działanie. Zmo-
// wyswietlenie wyniku
echo '<p>Mean time: ' .$commentResult['mean'] . 's for ' .$commentResult[ dyfikujemy więc prosty przykład z Li-
'iterations'].' iterations of comments()</p>'; stingu 1. Zmiany, które prezentujemy
?> na Listingu 16 polegały na usunięciu
wszelkich operacji związanych z kla-

PHP Solutions Nr 6/2006 www.phpsolmag.org 19


Dla początkujących Profilowanie aplikacji PHP

są Benchmark _ Timer i dodaniu nowej łego skryptu. Dalej znajduje się tabela na). Ostatni wiersz naszej tabeli opisu-
funkcji apd _ set _ pprof _ trace() na podająca bardziej szczegółowe informa- je funkcję main(), a przecież na naszym
początku skryptu, które rozpoczyna cje, jak procentowy udział danej funk- listingu nie było funkcji o takiej nazwie!
śledzenie działania skryptu. APD po- cji w czasie wykonania całego skryp- Skąd więc się ona wzięła? Otóż main
siada tez kilka innych funkcji, ale my tu, rzeczywisty czas, jaki minął (kolum- oznacza tu cały skrypt od momentu roz-
wykorzystamy jedynie tę wymienioną. na Real), czas spędzony na wykony- poczęcia śledzenia.
Jak widać, aby wykonać profilowanie, waniu kodu użytkownika przez proce-
wymagana jest tylko minimalna mo- sor (kolumna User) oraz czas spędzony Przykład z życia wzięty
dyfikacja kodu aplikacji: wystarczy, że na wywołaniach systemowych (kolum- Nasz przykład jest dość prosty, więc
wywołanie na początku głównego pli- na System). W kolumnie Calls zapisana czasy wykonania są minimalne. Weź-
ku naszej aplikacji dodamy apd _ set _ jest informacja o liczbie wywołań danej my więc większą aplikację: nie będzie-
pprof _ trace(). Po wykonaniu skryp- funkcji (nazwa funkcji to ostatnia kolum- my jej tworzyć od zera, tylko skorzy-
tu wynik profilowania zostaje zapisany
w pliku o nazwie pprof.xxxxx.yy, gdzie Listing 12. Porównanie wydajności funkcji przed i po optymalizacji.
xxxxx to PID procesu serwera, który
uruchomił profilowany skrypt, a yy to <?php
require 'Benchmark/Iterate.php';
kolejny numer porządkowy. Katalog, w
require './filter.php';
którym ten plik zostanie zapisany, jest require './filter2.php';
określony przez opcjonalny parametr $benchmark = new Benchmark_Iterate();
funkcji apd _ set _ pprof _ trace (). $comment = 'Some comments <script> ... </script>,
Jeżeli ten ostatni nie został zde- Some comments <script> ... </script>
<a href="www.somedangerouslink.com"> Safe text <a
finiowany, to położenie tego folderu
href="javascript:alert("XSS")">ClickMe!</a> - <body
określa zmienna apd.dumpdir z pliku onload=alert("vulnerable")> Safe text <iframe src=
php.ini. Struktura pliku pprof jest opi- http://www.notsafe.com/script.html>';
sana w dokumentacji, ale odradzamy $benchmark->run( 1000, 'comments', $comment );
własnoręczne analizowanie jego za- $commentResult = $benchmark->get();
$benchmark->run( 1000, 'comments2', $comment );
wartości, ponieważ istnieje do tego
$comment2Result = $benchmark->get();
proste i efektywne narzędzie – skrypt
PHP o nazwie pprofp. echo '<p>Mean time: '.$commentResult['mean'] . 's for '.$commentResult[
Wywołujemy go z linii poleceń w 'iterations']. ' iterations of comments()</p>';
katalogu, w którym się on znajduje,
$percent = round( (1-($comment2Result['mean']/$commentResult['mean']))*100, 1 );
podając ścieżkę dostępu do pliku z
echo '<p>Mean time: '.$comment2Result['mean'].'s ('. $percent .'% faster) for '.
wynikami: $comment2Result['iterations'].' iterations of comments2()</p>';
?>
php pprofp –z /tmp/profiling/pprof.02596.8
Listing 13. Klasa MyIterate
Spowoduje to przeanalizowanie poda-
<?php
nego jako parametr pliku, utworzone- require_once 'Benchmark/Iterate.php';
go wcześniej przez funkcję apd _ set _ class MyIterate extends Benchmark_Iterate{
pprof _ trace(). Program pprofp ma private $overhead;
szereg flag i opcji opisanych w Ramce public function run(){
$arguments = func_get_args();
Parametry skryptu pprofp. Nam najbar-
$iterationsCount = array_shift( $arguments );
dziej przydadzą się flagi –z i –r. Wyni- $functionName = array_shift( $arguments );
kiem interpretacji naszego pliku jest ta- $arguments = array_shift( $arguments );
bela pokazana na Rysunku 2. parent::run( $iterationsCount, $functionName, $arguments );
Na Rysunku 2 pokazujemy kilka $benchmark = new Benchmark_Iterate();
for ( $i=0; $i<$iterationsCount; $i++ ){
istotnych informacji. Na górze widać
$benchmark->setMarker( '_start_'.$i );
podstawowe dane, takie jak lokalizacja $benchmark->setMarker( '_stop_'.$i );
testowanego pliku i czas wykonania ca- }
$result = $benchmark->get();
$this->overhead = $result['mean'];
Listing 11. Zoptymalizowana funkcja }
comments() public function get(){
$result = parent::get();
<?php
$result['mean'] -= $this->overhead;
function comments2( $comment ){
return $result;
$comment = strip_tags($comment);
return $comment; }
} }
?> ?>

20 www.phpsolmag.org PHP Solutions Nr 6/2006


stamy z gotowego systemu portalowe-
go, którym jest PHP-NUKE. W Inter-
necie łatwo można znaleźć opis jego
instalacji, załóżmy więc, że mamy już
wszystko zainstalowane i skonfiguro-
wane. Nie będziemy dokonywać żad-
nych zmian w aplikacji, dodamy tylko
na początku pliku index.php linijkę:

apd_set_pprof_trace();

Następnie wywołamy w przeglądarce


tak zmodyfikowany plik i przeanalizujemy
uzyskane wyniki poleceniem:

php pprofp –r /lokalizaja/pliku/pprof

Na mojej maszynie testowej najwięcej


czasu zajęło wykonanie funkcji mysql _
query() oraz file(), co jest normal-
ne, ponieważ wejście/wyjście jest wą-
skim gardłem każdej aplikacji. Można-
by znacznie przyspieszyć te operacje
keszując wyniki zapytań wysyłanych do
bazy danych: wiele interfejsów bazoda-
nowych posiada taką funkcjonalność,
pozwalającą na korzystanie z wcze-
śniejszych zapytań.
Sprawdźmy teraz, które procedury
trwały najdłużej (bez uwzględniania pod-
procedur):

php pprofp –u /lokalizaja/pliku/pprof

Wynik powinien być podobny do przed-


stawionego na Rysunku 3.
Spójrzmy na pierwsze cztery funk-
cje, których wywołanie zdominowa-
ło czas wykonania całego skryptu.
Znowu znajdujemy tu operacje dostę-
pu do bazy danych. Funkcja addslah-
es(), wywoływana zapewne ze wzglę-
dów bezpieczeństwa, jest koniecz-
na. Spójrzmy teraz na trzecią na li-
ście funkcję readdir() – została ona
wywołana 633 razy, co wielokrotnie
przekracza liczbę katalogów z plika-
mi aplikacji. Gdyby to była nasza apli-
kacja, to moglibyśmy każdorazowo za-
pamiętac wynik odczytania zawartości
katalogu w zmiennej, zamiast wielo-
krotnie wywoływać readdir() dla tych
samych katalogów. Kolejną funkcją na
naszej liście jest eregi _ replace() i
jak już wspomnieliśmy, można ją za-
stąpić szybszym odpowiednikiem –
preg _ replace().
Pomińmy teraz czas potrzeby na
wykonanie operacji wejścia/wyjścia i

PHP Solutions Nr 6/2006


Dla początkujących Profilowanie aplikacji PHP

sprawdźmy, ile poszczególne funkcje


Listing 14. Wykorzystanie klasy MyIterate zabrały czasu samego procesora:
<?php
require 'MyIterate.php'; php pprofp –z /lokalizaja/pliku/pprof
require './filter.php';
require './filter2.php'; Wyniki przedstawiamy na Rysunku 4. Te-
$benchmark = new MyIterate();
raz pierwsze miejsce zajmuje ini _ set().
$comment = 'Some comments <script> ... </script>, Some comments <script> ... </script>
<a href="www.somedangerouslink.com"> Safe text Żeby sprawdzić, czy rzeczywiście jest po-
<a href="javascript:alert("XSS")">ClickMe!</a> - trzebna i gdzie występują wywołania do
<body onload=alert("vulnerable")> Safe text tej funkcji, możemy się posłużyć opcją –t,
<iframe src=http://www.notsafe.com/script.html>'; która wyświetli nam drzewo wywołań funk-
$benchmark->run( 1000, 'comments', $comment );
cji. Przeanalizowaliśmy pozostałe funk-
$commentResult = $benchmark->get();
$benchmark->run( 1000, 'comments2', $comment ); cje spośród najbardziej czasochłonnych
$comment2Result = $benchmark->get(); oprócz define(), która pojawiła się wyso-
echo '<p>Mean time: ' . $commentResult['mean'] . 's for ' . ko dopiero w tej klasyfikacji. Jest ona wy-
$commentResult['iterations'] . ' iterations of comments()</p>'; woływana 294 razy i choć każde jej użycie
$percent = round( (1-($comment2Result['mean']/$commentResult['mean']))*100, 1 );
niesie ze sobą minimalny narzut czasowy,
echo '<p>Mean time: ' .
$comment2Result['mean'] . 's ('. $percent .'% faster) for ' . to jednak podsumowując jej wszystkie wy-
$comment2Result['iterations'] . wołania możemy stwierdzić, że „pożarła”
' iterations of comments()</p>'; ona aż 11,1% czasu pracy procesora. Je-
?> śli korzystamy z klas, możemy zamiast tej
funkcji użyć stałych klasy definiowanych
Listing 15. Sprawdzamy jak bardzo wzrosła wydajność naszej aplikacji.
poleceniem const, co jest znacznie szyb-
<?php sze niż stosowanie define().
require 'Benchmark/Profiler.php';
require './generators.php';
require './filter_profiler.php';
Podsumowanie
require './filter2_profiler.php';
Nic nie zastąpi dobrych praktyk programi-
$profiler = new Benchmark_Profiler(); stycznych oraz doświadczenia samego pro-
$profiler->start(); gramisty. Jednak jesteśmy tylko ludźmi i cza-
$phone = '(33) 455-44-55'; sem popełniamy błędy, które potem trzeba
$comment = 'Some comments <script> ... </script>, Some comments <script> ... </script>
naprawić. Czasem trzeba poprawiać cudze
<a href="www.somedangerouslink.com"> Safe text <a href="javascript:alert(
"XSS")">ClickMe!</a> - <body onload=alert("vulnerable")> Safe text
błędy. Niedostateczna wydajność aplikacji
<iframe src=http://www.notsafe.com/script.html>'; (jeśli nie jest spowodowana słabym sprzę-
$phone = telephone( $phone ); tem) zazwyczaj jest winą błędu programisty
$comment = comments2( $comment ); lub projektanta systemu. Wiele z tych błę-
$profiler->enterSection('passGen');
dów można wychwycić wykorzystując profi-
$pass = passGen();
$profiler->leaveSection('passGen');
lowanie. Wskaże nam ono te miejsca w ko-
$profiler->enterSection('DB connection'); dzie, które najbardziej opłaca się zoptyma-
$conn = mysql_connect( 'localhost', 'root', '' ); lizować, ponieważ dadzą stosunkowo du-
mysql_select_db( 'phpsolmag', $conn ); ży przyrost wydajności małym kosztem (nie
$profiler->leaveSection('DB connection');
trzeba analizować całej aplikacji). Mam na-
$sql = 'INSERT INTO users(`pass`, `addDate`, `phone`, `comment`)
VALUES( "'.$pass .'", "'.date("Y-m-d H:i:s").'", "'.$phone.'", "'.$comment.'" )';
dzieje, że udało mi się Wam pokazać, w ja-
$profiler->enterSection('DB insertion'); ki sposób zidentyfikować wolne partie kodu.
mysql_query( $sql ); Pamiętajmy, aby testów dokonywać w od-
$profiler->leaveSection('DB insertion'); izolowanym środowisku, bez zbędnych pro-
$profiler->stop();
cesów i najlepiej z odłączoną siecią, aby nic
$profiler->display();
?>
nie zafałszowało wyników testów. n

Listing 16. Prosty przykład użycia APD

<?php O autorze
apd_set_pprof_trace();
function doSth(){ Łukasz Witczak
$tmp = range( 0, 999 ); Autor jest studentem piątego roku Wy-
krsort( $tmp ); działu Informatyki Politechniki Szczeciń-
return $tmp;
skiej. Programowaniem w PHP zajmu-
}
je się od 5 lat. Oprócz PHP, programu-
je jeszcze w C/C++ i Javie.
// wywołanie funkcji doSth()
$result = doSth();
Kontakt z autorem:
?>
lwitczak@wi.ps.pl

22 www.phpsolmag.org PHP Solutions Nr 6/2006


Na CD
Przetestuj dowolne skrypty
bez instalacji!

HITY
2 kursy wideo KeyStone
- XML Developing Using Java
Programy - XML Based Web Applications
PHP-Qt
XAMP
Wampserwer E-BOOKS
Auditing Your Web Site Security
PHP Power Programming
OASSIS OpenDocument Essentials
na naszej stronie internetowej pod adresem www.phpsolmag.org/pl

na naszej stronie internetowej pod adresem www.phpsolmag.org/pl


Wszystkie listingi z artykułów zostały zamieszczone

Wszystkie listingi z artykułów zostały zamieszczone


Dla początkujących

Savant – pogromca Smarty?


Stopień trudności: lll
Tomasz Garbiak

Na rynku systemów szablonów dla PHP niepo-


dzielnie króluje Smarty. Użytkownicy wysoko
cenią sobie jego niezawodność i duże moż-
liwości. Smarty stało się tak powszechne, że
niemal kompletnie zapomniano o jego wadach:
jest dość powolny, niezbyt łatwo rozszerzalny,
a do tego wymaga od użytkownika opanowania
nowej składni.

W
ymienionych wad pozbawio- lacji. Jeżeli skompilowanego pliku jeszcze
ny jest Savant – zorientowany nie ma, bądź jest on nieaktualny, odbywa
obiektowo system wykorzystu- się proces kompilacji.
jący samo PHP jako język szablonów. Zo- Stosując Smarty piszemy w myśl pa-
baczmy zatem, czym Savant tak napraw- radygmatu (wzorca) MVC (model-view-
dę różni się od Smarty. Spójrzmy na frag- controller). Szablony, czyli warstwa pre-
ment kodu z Listingu 1, gdzie przedstawi- zentacji, są kompletnie oddzielone od
liśmy przykładowy szablon stworzony w warstwy logiki aplikacji, co ma czynić je
Smarty. (szablony i całe aplikacje) bardziej przej-
Po kolei: Smarty najpierw wsta- rzystymi i niejako łatwiejszymi do napi-
wi odpowiednią wartość zmiennej $ti- sania – teoretycznie szablony będzie
tle, następnie, korzystając z pętli foreach mógł stworzyć grafik, który nigdy wcze-
przejdzie przez tablicę asocjacyjną $par- śniej nie miał do czynienia z PHP. Wadą
W SIECI agrahps wstawiając wartości klucza ti- takiego rozwiązania jest oczywiście do-
tle w HTML-owe znaczniki <h2>, zaś war-
tości klucza content w znaczniki <p>. Nie
• http://www.phpsavant.com/ Co należy wiedzieć...
– strona domowa Savanta ma tu ani śladu po kodzie PHP. W syste- Czytelnik powinien znać podstawy PHP
• http://smarty.php.net/ – mie Smarty szablony są kompilowane do i HTML. Przyda się też znajomość
strona domowa Smarty
tego języka. Smarty.
• http://www.phppatterns.com/
docs/design/templates_and_ Oczywiście nie za każdym razem –
template_engines – ciekawe
informacje o szablonach
system sprawdza najpierw, czy dla dane- Co obiecujemy...
Z artykułu dowiesz się, jak korzystać z
• http://www.sitepoint.com/ go szablonu nie istnieje już skompilowa-
article/smarty-php-template- systemu Savant i czy warto się na nie-
ny plik, a jeśli tak, to czy szablon nie zo- go przesiąść.
engine – artykul o Smarty
stał zmieniony od czasu ostatniej kompi-

24 www.phpsolmag.org PHP Solutions Nr 6/2006


Savant Dla początkujących

datkowa praca, jaką musi wykonać inter-


preter języka kompilując szablony, czy Listing 1. Przykładowy szablon w Smarty
choćby sprawdzać, czy zostały skompi-
<h1>{$title}</h1>
lowane. Wpływa to oczywiście w jakimś {foreach item=item from=$paragraphs}
stopniu na szybkość działania aplikacji <h2>{$item.title}</h2>
i obciążenie serwera. Wielu programi- <p>{$item.content}</p>
stów uważa zatem, że wobec tego lepiej {/foreach}

po prostu w szablonach używać PHP.


Listing 2. Język PHP jako system szablonów
Swoją drogą język PHP w swej najprost-
szej formie można uznać za swego ro- <h1><?php echo $title ?></h1>
dzaju system szablonów. Przecież chy- <?php foreach($paragraphs as $item): ?>
<h2><?php echo $item['title'] ?></h2>
ba każdy z nas zaczynał jego naukę od
<p><?php echo $item['content'] ?></p>
prostych skryptów, podobnych do tego z <?php endforeach; ?>
Listingu 2.
I w ten sposób uzyskaliśmy ten sam Listing 3. Szablon w systemie Savant
efekt, który dałoby nam Smarty i – na
<h1><?php echo $this->title ?></h1>
pierwszy rzut oka – o wiele mniejszym
<?php foreach($this->paragraphs as $item): ?>
kosztem. Uzyskany kod faktycznie jest <h2><?php echo $item['title'] ?></h2>
całkiem czytelny. Ale problemy z jego <p><?php echo $item['content'] ?></p>
przejrzystością zaczną się dopiero, kiedy <?php endforeach; ?>
wymieszamy go z rozbudowanym kodem
HMTL. Widać zatem, że Smarty będzie
całkiem praktycznym rozwiązaniem przy już za chwilę. Póki co, warto podkreślić, Oczywiście formuła $this->vari-
nieco większych projektach. ze w Zend Frameworku tworzonym przez able _ name ma zastosowanie tylko dla
firmę Zend, system szablonów również zmiennych przypisanych wcześniej do
Savant będzie korzystał bezpośrednio z języ- obiektu Savanta (o tym później). Do
Przedstawiony wcześniej fragment kodu ka PHP, a w pracach nad nim udział brał zmiennych lokalnych, utworzonych już w
PHP jest bardzo podobny do szablonu Paul M. Jones, twórca Savanta. samym szablonie odwołujemy się w zwy-
Savanta. Nie ulega wątpliwości, że sys- kły sposób. W powyższym przykładzie ta-
tem ten znalazł już wielu zwolenników, Szablony Savanta ką zmienną jest $item.
a swój ogromny sukces zawdzięcza po- Savant to system zorientowany obiekto- Co ciekawe, Savant nie zmusza
niekąd frameworkowi Ruby On Rails. Ja- wo, więc chcąc skorzystać w szablonie z nas do korzystania ze zmiennej $this.
ko jedną z największych zalet Ruby'ego jakiejś zmiennej, powinniśmy odwoływać Wystarczy w tym celu ustawić w je-
od początku wymieniano fakt, że wszę- się do niej poprzez obiekt klasy Savant. go konfiguracji (patrz Instalacja) opcję
dzie, a więc także w szablonach, uży- Jako że szablony same stają się w pew- exctract na wartość true. Nie jest to
wa się jednego i tego samego języka. nym momencie zmiennymi tego obiektu, jednak zalecane – stracimy trochę cen-
Czy takie rozwiązanie jest lepsze o sys- wystarczy użycie słowa $this. Spójrzmy nego czasu oraz pamięci RAM, a sam
temu Smarty? Odpowiemy na to pytanie na prosty szablon Savanta na Listingu 3. kod stanie się mniej czytelny (trudniej

������� ����������
���������������� ������
������ ����������

���� ��������������������� ��������� ���

��� ���
���

���
���������������������� ���������������������

Rysunek 1. Schemat działania systemu Smarty

PHP Solutions Nr 6/2006 www.phpsolmag.org 25


Dla początkujących Savant

będzie odróżnić, które zmienne w sza-


blonie są lokalne, a które przekazywa- Benchmarki – Savant szybszy od Smarty
ne z warstwy logiki). Czy faktycznie Savant jest szybszy niż Smarty? Testy wydajności udzielają na to pytanie
jednoznacznej odpowiedzi: TAK.
Zmierzyliśmy, ile razy na sekundę serwer zdoła wyświetlić strony wygenerowane
Zalety korzystania przy pomocy obydwu systemów szablonów – Rysunek 3. W przypadku systemu Smarty
z PHP w szablonach, zrobiliśmy pomiary dla skompilowanych i nieskompilowanych szablonów. Sprawdziliśmy
czyli Savant vs. Smarty również wyniki dla dla różnej ilości jednoczesnych zapytań (od 1 do 1000). Za każdym
razem znaczną przewagę miał Savant. Brak kompilacji tylko w niewielkim stopniu wpły-
Zanim na dobre zaczniemy przygodę
nął na rezultat Smarty (Tabela 1).
z praktycznymi przykładami, przybliżmy
sobie najważniejsze zalety korzystania
z PHP w szablonach. Stosując samo PHP, Ramce Bezpieczeństwo). Savant to zde- Instalacja
zyskujemy przede wszystkim wzrost szyb- cydowanie lżejszy system. Smarty ze swo- Zacznijmy od wyboru wersji. Możemy
kości działania strony (Ramka Benchmar- imi 45 pluginami wydaje się być przy nim zdecydować na wersję 2.x napisaną jesz-
ki). Nie musimy także uczyć się nowej prawdziwym kombajnem z takimi funkcja- cze dla PHP4, bądź na wersję 3 stworzo-
składni, a dodatkowo nie jesteśmy ograni- mi jak choćby obsługa keszowania. Savant ną z myślą o PHP5. Obie oferują te sa-
czeni do zaledwie dwóch pętli sterujących nie posiada podobnej usługi – zamiast tego me możliwości, wersja 3 różni się od po-
dostępnych w Smarty. Możemy na przy- jego twórca zaleca korzystanie z zewnętrz- przedniej innym zestawem wbudowanych
kład skorzystać z pętli while lub do-while. nych bibliotek takich jak PEAR::Cache_Li- pluginów oraz nieco zmienionym syste-
Nie chcielibyśmy jednak, aby użyt- te, ewentualnie rozszerzeń do PHP zapew- mem filtrowania. Przede wszystkim jed-
kownik mający możliwość zmiany sza- niających keszowanie kodu pośredniego nak korzysta w pełni z wprowadzonych w
blonów, mógł wpływać na kod strony. Tu (np. biblioteka APC). Na końcu dokumen- PHP5 nowych cech obiektowych i działa
przewagę ma Smarty – kompilacja sza- tacja – korzystanie z PHP w szablonach bez problemów nawet pod najbardziej re-
blonów umożliwia kontrolę tego, co się daje możliwość umieszczenia w nich ko- strykcyjnymi ustawieniami obsługi błędów
w nich znajduje. Dlatego też twórcy Sa- mentarzy, a tym samym tworzenia auto- (E_ALL & E_STRICT). Wadą nowszej
vanta zdecydowali się na dodanie do matycznej dokumentacji za pomocą po- wersji jest to, że jest jeszcze ciągle rozwi-
niego funkcjonalności pozwalającej do- pularnego phpDocumentora czy Doxyge- jana, dlatego mogą pojawić się w niej po-
łączać własne kompilatory (szczegóły w na. Plus dla Savanta. ważne błędy itp. Oczywistą zaletą wersji
2.x jest spora ilość dodatków i dojrzałość.
Tabela 1. Wyniki testów (w liczbie odpowiedzi na sekundę) My skupimy się na wersji 3.
Liczba Smarty (szablony Smarty (szablony Instalacja polega na ściągnię-
jednoczesnych Savant nie skompilowane skompilowane ciu pakietu i skopiowaniu go w odpo-
zapytań wcześniej) wcześniej) wiednie miejsce. Plik można pobrać ze
1 25,12 16,91 16,95 strony http://www.phpsavant.com/yawiki/
10 24,90 16,79 16,82 index.php?area=Savant3. Należy go na-
100 24,81 16,72 16,76 stępnie rozpakować i zawartość katalogu
Savant3-3.0.0(plik Savant3.php oraz pod-
1000 21,49 15,60 15,73
katalog Savant3) umieścić w wybranym
Średnia 24,08 16,51 16,57
przez siebie miejscu, dostępnym dla uży-
wanego przez nas serwera WWW, najle-
piej tam, gdzie mamy ustawioną zmienną
konfiguracyjną include _ path.
������� Można też, jeżeli się ma taką możli-
���������������� ������
������ wość, zainstalować Savanta korzystając
z instalatora PEAR. W tym celu wystar-
czy wykonać polecenie:

pear install

��� http://phpsavant.com/
���� ��������������������� Savant3-3.0.0.tgz.

Warstwa logiki
���
Zacznijmy od warstwy logiki, a więc od
skryptu, w którym wykonywane będą
właściwe operacje i który będzie przeka-
����������
zywał dane do szablonów. Na początek
����������
stwórzmy obiekt Savanta.

require_once 'Savant3.php';
Rysunek 2. Schemat działania systemu Savant $savant = new Savant3();

26 www.phpsolmag.org PHP Solutions Nr 6/2006


Dla początkujących Savant

Jeżeli chcemy, możemy jako parametr dla


konstruktora przekazać tablicę asocjacyj-
Bezpieczeństwo
Szablony Savanta są zwykłymi plikami PHP, dlatego powinniśmy w większym stop-
ną zawierającą opcje konfiguracyjne. Nie niu zadbać o ich bezpieczeństwo. Podobnie jak innych naszych skryptów, tak i szablo-
ma sensu ich tu teraz wszystkich oma- nów Savanta nie możemy udostępniać anonimowym użytkownikom. Mogą jednak zaist-
wiać, zwłaszcza, że bardzo rzadko bę- nieć przypadki, kiedy chcielibyśmy to zrobić – jeżeli na przykład prowadzimy serwis da-
jący możliwość zakładania własnych blogów, a tym samym tworzenia i edycji szablonów.
dą nam one potrzebne. Opcja extract,
Wyjściem jest tutaj stosowanie szablonów kompilowanych. Można to zrobić w Savantcie
o której mówiliśmy już wcześniej, umoż- korzystając z zewnętrznych kompilatorów. Przykładowy kompilator dołączony standardo-
liwia pozbycia się konieczności odwo- wo do wersji 2.x już w dużym stopniu podnosi bezpieczeństwo Savanta. Jeżeli przestawi-
ływania się do zmiennych przez formu- my go w tryb restrykcyjny, będziemy mogli określić, jakie funkcje i statyczyne metody mo-
gą być wywoływane z poziomu szablonów. Więcej o tym opowiemy na końcu artykułu.
łę $this->variable _ name. Możemy też
Pamiętajmy, aby postronny użytkownik nie miał dostępu do naszego systemu plików,
ustawić ścieżki do folderów z szablonami. a więc np. błędem byłoby uzależnianie nazwy używanego szablonu od wartości klucza
Domyślnie Savant przyjmuje, że szablony w tablicy $ _ GET.
znajdują się tam, gdzie skrypt PHP, który W ramach zabezpieczenia się przed atakami XSS Savant oferuje szerokie możliwo-
ści filtrowania wyświetlonych zmiennych. Służy do tego głównie funkcja eprint(), o któ-
z nich korzysta. Znacznie wygodniejszym
rej również powiemy w dalszej części tego artykułu.
rozwiązaniem będzie utworzenie osobne-
go katalogu tylko dla szablonów, tak aby
łatwiej było zarządzać projektem. W tym pracę, jeśli w jakimś momencie będzie- elementów, przykładowa treść oraz stop-
celu wykorzystamy zmienną konfigura- my musieli dodać nową ścieżkę do już ist- ka z informacjami o autorze. W rzeczywi-
cyjną $ _ config['template _ path']. Ma niejących. Na szczęście możemy skorzy- stej aplikacji te dane byłyby najpewniej po-
ona postać tablicy, zawierającej ścieżki stać z metody addPath(), która działa nie- bierane z bazy danych, tu zrezygnujemy z
do katalogów, w których system ma szu- co lepiej. Przyjmuje te same argumenty tak skomplikowanych procedur i wpiszemy
kać szablonów. Wartość tej tablicy może- co setPath(), a więc najpierw określamy je na sztywno do skryptu (patrz Listing 5).
my ustawić za pomocą konstruktora kla- typ ścieżki (template), a następnie poda- Następnie dane musimy przekazać
sy, przekazując mu tablicę z konfiguracją jemy jedną lub więcej (korzystając z tabli- Savantowi korzystając z metody assign,
dla klucza template _ path. Innym sposo- cy) ścieżek. co widać na Listingu 6. Niekiedy koniecz-
bem jest skorzystanie z metody setPath() Jeżeli nasz szablon znajduje się w ne będzie przekazanie do szablonu refe-
(większość pozostałych zmiennych kon- podkatalogu do już ustalonej ścieżki, nie rencji, a nie kopii zmiennej. Zadanie to re-
figuracji ma podobne, właściwe dla sie- musimy poszerzać listy katalogów o tą alizuje metoda assignRef():
bie metody), w której pierwszym parame- lokalizację. Wystarczy podczas wyboru
trem jest typ ustalanej ścieżki. Typ przyj- szablonu (na ogół w momencie wywoła- $ref = 'ref';
mować może dwie wartości: resource, je- nia metody display()) dopisać ten katalog $savant->assignRef('ref', $ref);
żeli chcemy ustalić położenie pluginów, przed nazwą pliku:
czy też innych dodatków do Savanta lub Przejdźmy teraz do najważniejszego,
template, jeżeli chcemy określić położe- $savant->display(' czyli wybrania szablonu i wyświetlenia
nie szablonów. Drugi parametr może być podkatalog/plikSzablonu.tpl.php');. jego zawartości. Służy do tego metoda
łańcuchem znaków (jeżeli ustalamy poje- display():
dyńczą ścieżkę) lub tablicą, jeżeli chcemy Mając już ustalone ścieżki, możemy zająć
mieć kilka katalogów z szablonami. się przygotowaniem danych, które przypi- $savant->display('template.tpl.php');
Oba wymienione sposoby mają tą szemy do szablonu. Przyjmijmy, że two-
wadę, że nadpisują dotychczasową za- rzymy bardzo typową stronę, np. blog, w W ten sposób mamy już wszystko, czego
wartość wartości klucza template _ path którym znajdzie się nagłówek z tytułem nam trzeba od strony logiki. Nasz skrypt
w tablicy $ _ config. Może to nam utrudnić strony i jej opisem, menu złożone z kilku wygląda teraz tak jak na Listingu 7.

����
Szablon
– warstwa widoku
������
����
Brakuje nam już tylko samego szablo-
���� nu. Kod gotowego szablonu można zo-
���������������������

���� ������ ������ baczyć na Listingu 8.


���� Konstrukcję if-else stosujemy po
to, by nie wyświetlać odnośnika do bie-
����
żącej strony.
���� Metoda eprint() służy do „bezpiecz-
��� nego” wyświetlania zmiennych. Świetnie
���
nadaje się do obrony przed atakami XSS
i znajduje zastosowanie głównie przy ob-
���
słudze formularzy. Jej działanie polega na
��� odpowiednim przefiltrowaniu danych przed
������ ��������������������� �����������������������
wyświeleniem za pomocą wybranej funk-
Rysunek 3. Prównanie wydajności Savanta i Smarty cji. Domyślnie jest to funkcja htmlspecial-

28 www.phpsolmag.org PHP Solutions Nr 6/2006


Savant Dla początkujących

chars(), można jednak w zależności od po-


Listing 4. Kilka sposobów na określenie położenia katalogu z szablonami trzeb samemu określić, jakie funkcje, czy
$savant = new Savant4(array('template_path' => metody mają być stosowane. Najprościej
array('/sciezka/do/katalogu/z/szablonami/'))); zrobić to bezpośrednio w szablonie poda-
jąc wybrane funkcje jako kolejne parame-
$savant->setPath('template', array('sciezka/do/katalogu/z/szablonami/', try dla metody eprint(). Parametry są typu
'sciezka/do/innego/katalogu/z/szablonami/'));
callback: mogą to więc być nie tylko nazwy
$savant->addPath('template', 'nowa/sciezka/do/katalogu/z/szablonami/'); funkcji, ale także tablice, w których pierw-
szym elementem jest statyczna klasa bądź
Listing 5. Plik warstwy logiki,który przekaże wartości zmiennych do szablonu obiekt, a drugim metoda dla podanej kla-
sy lub obiektu. Jeżeli chcemy ustalić listę
$title = 'Page about Savant';
$description = 'Example page that shows the possibilites of Savant 3';
stosowanych funkcji wspólną dla wszyst-
kich wywołań metody eprint, powinniśmy
$menu = array('home' => 'index.php', 'tutorial' => 'tutorial.php', skorzystać z metody setEscape(). Podob-
'contact' => 'contact.php'); nie jak eprint(), przyjmuje ona jako para-
metry nazwy wybranych funkcji wymienio-
$currentPage = 'home';
ne po przecinku. Savant wydaje się zatem
$content = 'Nunc purus odio, imperdiet eget, pharetra quis, consequat quis, elastyczniejszy w tym zakresie. W systemie
nulla. Phasellus at felis. Sed ac erat et est lacinia rhoncus. Maecenas urna Smarty, aby dodać własną funkcję, musimy
sapien, euismod vitae, aliquet sit amet, scelerisque vitae, velit. Curabitur napisać wtyczkę do systemu.
quis urna. Pellentesque habitant morbi tristique senectus et netus et malesuada
Warto wspomnieć również o Savan-
fames ac turpis egestas. Integer in risus ac orci lacinia volutpat.';
towskich filtrach. Ich działanie jest podob-
$info = 'Copyright 2006. Tomek Garbiak'; ne do metody eprint(), tyle że nie doty-
czy pojedyńczych zmiennych, ale całości
Listing 6. Przekazywanie wartości zmiennych szablonowi wyświetlanej strony – czyli wygenerowa-
ny przez Savanta wynik jest filtrowany za-
$savant->assign('title', $title);
$savant->assign('description', $description); nim zostanie wysłany do klienta. Lista funk-
$savant->assign('menu', $menu); cji filtrujących jest przechowywana w tabli-
$savant->assign('currentPage', $currentPage); cy $ _ config pod kluczem filters i można
$savant->assign('content', $content); ją zmienić metodami setFilters() (nadpi-
$savant->assign('info', $info);
suje dotychczasową zawartość tablicy) lub
Listing 7. Warstwa logiki w całej okazałości addFilters() (dodaje filtr do listy). Podob-
nie jak w przypadku eprint() czy setEs-
require_once 'Savant3.php'; cape(), tak i tu kolejne funkcje można poda-
wać po przecinku jako parametry dla tych
$savant = new Savant3();
$savant->addPath('sciezka/do/katalogu/z/szablonami/');
metod. Funkcje te mogą być standardowy-
mi funkcjami PHP, jednak wraz z Savantem
$title = 'Page about Savant'; dostarczona jest też abstrakcyjna klasa Sa-
$description = 'Example page that shows the possibilites of Savant 3'; vant3 _ Filter, która służyć ma do tworze-
nia własnych filtrów. Tym zagadnieniem nie
$menu = array('home' => 'index.php', 'tutorial' => 'tutorial.php',
'contact' => 'contact.php');
będziemy się tu zajmować, warto jedynie
wspomnieć, że tworzenie filtrów jest podob-
$content = 'Nunc purus odio, imperdiet eget, pharetra quis, consequat quis, ne do tworzenia pluginów, o których powie-
nulla. Phasellus at felis. Sed ac erat et est lacinia rhoncus. Maecenas urna my za chwilę.
sapien, euismod vitae, aliquet sit amet, scelerisque vitae, velit. Curabitur
Smarty w tym zakresie oferuje po-
quis urna. Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Integer in risus ac orci lacinia volutpat.';
dobną funkcjonalność, dodatkowo po-
zwalając na filtrowanie szablonów
$info = 'Copyright 2006. Tomek Garbiak'; przed i po kompilacji.

$savant->assign('title', $title);
Kilka szablonów
$savant->assign('description', $description); na jednej stronie
Obecnie mamy wspólny szablon dla cało-
$savant->assign('menu', $menu); ści wyświetlanej strony. Chcielibyśmy jed-
nak, aby inne strony wyglądały inaczej za-
$savant->assign('content', $content);
chowując jedynie wspólny nagłówek i stop-
$savant->assign('info', $info);
kę. Jak to zrobić? Szablon można podzie-
lić na mniejsze fragmenty i umieścić je w
$savant->display('template.tpl.php'); osobnych plikach, tak jak na Listingach 10
i 11. Aby umieścić te fragmenty w głównym

PHP Solutions Nr 6/2006 www.phpsolmag.org 29


Dla początkujących Savant

szablonie należy skorzystać z metody tem- resource. Otworzywszy plik Savant3_Plu- Napiszmy zatem własną wtyczkę, któ-
plate i instrukcji include, tak jak pokazano gin_ahref.php przekonamy się, że plugin ra wstawi do szablonu adres strony, z ja-
to na Listingu 12. Innym, nieco bardziej ele- to w zasadzie klasa dziedzicząca po klasie kiej wszedł użytkownik. W tym celu tworzy-
ganckim sposobem jest uzyskanie metodą Savant3 _ Plugin posiadająca zaledwie jed- my w katalogu Savant3/resources plik o na-
fetch() wyniku wyświetlenia mniejszych ną metodę o nazwie takiej jak nazwa plu- zwie Savant3_Plugin_referer.php, w którym
szablonów i przypisanie ich do zmiennych. ginu (pominąwszy początkową część: Sa- umieszczamy kod jak na Listingu 14. Mamy
Cała ta operacja odbywa się oczywiście w vant3 _ Plugin). Savant używa tu magicz- za sobą najtrudniejszy fragment, czyli stwo-
pliku warstwy logiki. W głównym szablonie nej metody _ _ call(), która odpowiada za rzenie szkieletu klasy naszej wtyczki. Pozo-
wstawiamy już same zmienne: zachowanie się klasy podczas próby wy- staje już tylko napisanie prostej funkcji, która
wołania nieistniejącej funkcji. W takim przy- zwróci odnośnik do strony, z której użytkow-
$header = $savant->fetch( padku Savant będzie próbował znaleźć od- nik wszedł do naszego serwisu – przykłado-
'header.tpl.php'); powiedni plugin (jego nazwa określana jest wa implementacja widoczna jest na Listingu
$savant->assign(' na podstawie nazwy metody) i wywołać da- 15. Gotowe! Teraz wystarczy w szablonie
header', $header); ną metodę. wpisać <?php echo $this->referer() ?> i
$footer = $savant->fetch(
'footer.tpl.php');
Listing 8. Szablon w Savancie
$savant->assign(
'footer', $footer); <?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://
www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
Drobnej modyfikacji wymagać teraz bę- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
dzie szablon główny – jego kod przedsta- <head>
wia Listing 13. Sposób postępowania wy- <title><?php echo $this->title ?></title>
gląda tu zatem podobnie jak w Smarty. <meta name="description"
content="<?php $this->eprint($this->description) ?>" />

Pluginy, </head>
<body>
czyli rozbudowywanie <h1><?php echo $this->title ?></h1>
Savanta <ul><?php foreach($this->menu as $key => $val): ?>
Ogromną zaletą Savanta jest możliwość <li><?php if ($key != $this->currentPage): ?>
<a href="<?php echo $val ?>"><?php echo $key ?></a>
jego łatwej rozbudowy dzięki sprawnemu
<?php else: echo $key; endif; ?></li>
systemowi pluginów. Zanim jednak napi- </ul>
szemy własne rozszerzenie, przyjrzyjmy <p>
się, jaka jest zasada ich działania. Plugi- <?php echo $this->content ?>
ny to obiekty, które można wywołać w sza- </p>
<address><?php echo $this->info ?></address>
blonie celem automatyzacji tworzenia pew-
</body>
nych powtarzających się elementów, np. </html>
aby łatwiej było umieścić na stronie odno-
śnik. Taką możliwość daje dostarczony z Listing 9. Przykład użycia funkcji eprint()
Savantem plugin ahref, którego używa się
<input type="text" name="sampleInput"
wpisując do szablonu następujący kod:
value="<?php $this->eprint($this->variable, strip_tags,
htmlspecialchars, array($someObject, $method)) ?>" />
<?php echo
$this->ahref('Text', Listing 10. Szablon nagłówka
'http://some.dummy.address.com') ?>,
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://
co w efekcie daje nam taki oto rezulat: www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<a href="http://some.dummy. <head>
<title><?php echo $this->title ?></title>
address.com">Text</a>
<meta name="description"
content="<?php $this->eprint($this->description) ?>" />
Kod pluginu znajdziemy w katalogu </head>
Savant3/resources. W tym miejscu będzie- <body>
my umieszczać nasze własne pluginy. Moż- <h1><?php echo $this->title ?></h1>
<ul><?php foreach($this->menu as $key => $val): ?>
na zdecydować się na inną lokalizację, ale
<li>
wówczas będziemy musieli podać Savanto- <?php if ($key != $this->currentPage): ?>
wi odpowiednią ścieżkę posługując się zna- <a href="<?php echo $val ?>">
nymi już metodami setPath() lub addPath(). <?php echo $key ?></a><?php else: echo $key; endif; ?>
Jedyna różnica w ich wykorzystaniu pole- </li>
</ul>
gać będzie na tym, że tym razem pierw-
szym parametrem będzie nie template a

30 www.phpsolmag.org PHP Solutions Nr 6/2006


Savant Dla początkujących

uzyskamy zamierzony efekt. Pisanie podob- mamy tam do czynienia ze znacznie więk- Kompilacja w Savancie
nych pluginów dla systemu Smarty również szą ilością możliwych opcji (obsługa keszo- Wróćmy do wykorzystania w Savancie ze-
nie należy do rzeczy trudnych, jednakże wania, dynamiczna rejestracja pluginu, itp.). wnętrznych kompilatorów. Włączenie kom-
pilatora sprowadza się do napisania kilku
Listing 11. Szablon stopki linijek kodu, tak jak na Listingu 16.
W pierwszym kroku ładujemy pliki
<address><?php echo $this->info ?> zarówno Savanta, jak i kompilatora. Na-
</address>
stępnie tworzymy instancję kompilato-
ra i ustalamy opcje konfiguracyjne – wy-
Listing 12. Szablon główny starczy podać ścieżkę do katalogu, w któ-
rym będą przechowywane skompilowane
<?php include $this->
pliki. Trzeba pamiętać o tym, że serwer
template('header.tpl.php') ?>
<p> musi mieć możliwość zapisu w tym ka-
<?php echo $this->content ?> talogu, trzeba mu więc nadać odpowied-
</p> nie prawa. Kompilator umożliwia korzy-
<?php include $this-> stanie w szablonach ze składni zbliżonej
template('footer.tpl.php') ?>
do tej znanej ze Smarty, a więc na przy-
</body>
</html> kład, aby wyświetlić zmienną, należy wpi-
sać {$variable}.
Listing 13. Szablon główny, tym razem nagłówek i stopka jako zmienne Jeśli kompilator ustawimy w tryb re-
strykcyjny, co odbywa się przez nadanie
<?php echo $this->header ?>
<p>
parametrowi $strict wartości true, w sza-
<?php echo $this->content ?> blonach będzie można korzystać tylko z
</p> tych funkcji i statycznych metod, które po-
<?php echo $this->header ?> damy w parametrach $allowedFunctions i
</body>
$allowedStatic. Funkcjonalność ta nie jest
</html>
jeszcze w pełni sprawna, dlatego domyślnie
Listing 14. Szkielet dla naszego pluginu jest wyłączona. Trzeba pamiętać, że przed-
stawiony kompilator jest jedynie przykładem
class Savant3_Plugin_referer extends Savant3_Plugin { możliwości systemu Savant, nie zaś w pełni
public function referer() {
działającym produktem.
}
}
Podsumowanie
Listing 15. Pełny kod pluginu Czy wobec wszystkich przedstawionych
tu wad i zalet Savanta warto dla niego po-
public function referer() {
if (!$_SERVER['HTTP_REFERER']) {
rzucić Smarty? Odpowiedź zależy w dużej
return ''; mierze od indywidualnych upodobań pro-
} else { gramisty i współpracujących z nim grafików.
$html = '<a href="'; Jeżeli potrzebny jest nam lekki i elastycz-
$html .= $_SERVER['HTTP_REFERER'];
ny system, Savant będzie idealny. Jeżeli
$html .= '">You came from this page</a>';
return $html;
potrzebujemy systemu bezpieczniejszego,
} z mnóstwem możliwości, to na dzień dzi-
} siejszy lepiej pozostać przy Smarty. Bar-
dzo prawdopodobne jest jednak, że wkrót-
Listing 16. Włączenie zewnętrznego kompilatora ce Savant doścignie swojego konkurenta i
require_once 'Savant2.php'; jego funkcjonalności nie będzie można ni-
require_once 'Savant2/Savant2_Compiler_basic.php'; czego zarzucić. n

$compiler = & new Savant2_Compiler_basic();


$compiler->compileDir = '/tmp';
O autorze
$tpl = & new Savant2();
$tpl->setCompiler($compiler); Tomek Garbiak łączy pracę jako pro-
gramista w firmie MagneticPoint ze stu-
Listing 17. Podnoszenie bezpieczeństwa przez włączenie trybu restrykcyjnego diowaniem informatyki na Politechni-
w kompilatorze ce Szczecińskiej. Wolny czas i pienią-
dze najchętniej poświęca podróżowaniu
$compiler->strict = true; i chodzeniu na koncerty.
$compiler->allowedFunctions = array('htmlspecialchars',
'echo', 'print', 'strip_tags'); Kontakt z autorem:
tgarbiak@magneticpoint.com

PHP Solutions Nr 6/2006 www.phpsolmag.org 31


Bezpieczeństwo

RSA w PHP: chronimy nasze


dane przy użyciu kryptografii
asymetrycznej
Stopień trudności: lll
Kamil Karczmarczyk

Zabezpieczenie aplikacji PHP przed włamaniami


to nie wsystko: jeżeli przesyłamy dane czystym
tekstem, zawsze może się znależć ktoś,
kto je przechwyci podsłuchując transmisję
w Internecie. Aby zadbać o bezpieczeństwo
danych, musimy więc sięgnąc po kryptografię
asymetryczną RSA, która daje obecnie
największą pewność jego ochrony i zastosować
ją zarówno po stronie serwera, jak i klienta.

N
ieodłączną częścią każdego me- haśle. Algorytm ten (w naszym przy-
chanizmu logowania użytkowni- padku zaimplementowany w języku Ja-
ków jest przesyłanie hasła wpi- vaScript i działający na przeglądarce
sanego na komputerze klienta (w przy- internetowej) zamienia hasło na spe-
padku aplikacji webowych – w przeglą- cjalny hash, podczas generowania któ-
darce internetowej) na serwer, na którym rego używany jest również tajny klucz.
(przeważnie w bazie danych) przechowy- Następnie, otrzymany skrót jest wysy-
wane są skróty MD5 haseł (zob. Ramka łany do serwera, gdzie – podobnie jak
Podstawy MD5). Ponieważ te skróty są przy sprawdzaniu hasła przesyłanego
jednokierunkowe i odtworzenie haseł na czystym tekstem – jest porównywany
ich podstawie nie jest możliwe, więc kra- z hashami zapisanymi w bazie danych.
W SIECI
dzież bazy nie da sprawcy wielu możli- Korzystając z tej metody wyeliminuje-
wości włamania. Poważnym problemem my ryzyko podsłuchu (nie przesyłamy
1. http://pear.php.net/package/ jest natomiast wspomniane już przesyła-
Crypt_RSA/ – klasa nie hasła jako czystego tekstu: wystarczy,
Crypt_RSA (PHP)
aby intruz podsłuchał transmisję pomiędzy
Co należy wiedzieć...
2. http://pear.php.net/package/ Należy znać podstawy programowania
Crypt_Blowfish/ – klasa klientem a serwerem (np. za pomocą snif- w PHP oraz AJAX. Przydatna będzie też
Crypt_Blowfish (PHP)
fera), a będzie mógł bez problemu doko- podstawowa wiedza na temat kryptografii.
3. http://ohdave.com/rsa/ – Im-
plementacja RSA (JS) nać agresji.
4. http://en.wikipedia.org/wiki/ W poprzednim numerze PHP So- Co obiecujemy...
RSA – Opis algorytmu RSA
lutions, w artykule Kryptografia w PHP Pokażemy zasadę działania algorytmu
5. http://en.wikipedia.org/wiki/
Blowfish_(cipher) – Opis
asymetrycznego RSA i zademonstruje-
prezentowaliśmy rozwiązanie tego pro-
algorytmu Blowfish my, jak przy jego użyciu stworzyć system
6. http://advajax.anakin.us/ blemu przy użyciu algorytmu HMAC- bezpiecznego logowania.
– Obiekt advancedAJAX MD5 po stronie klienta na wpisywanym

32 www.phpsolmag.org PHP Solutions Nr 6/2006


RSA w PHP Bezpieczeństwo

samego hasła, lecz jego skrót), ale mo-


żemy jej użyć wyłącznie wobec już ist- RSA: Zasada działania
niejących haseł, których skróty zostały RSA to pierwszy (wynaleziony w 1977 roku) i obecnie najpopularniejszy algorytm szyfro-
wania asymetrycznego, używany powszechnie np. w handlu elektronicznym czy w celu
utworzone i zapisane w bazie na ser- podpisywania emaili. Zasadę działania algorytmu RSA przedstawiamy na Rysunku 1. Je-
werze. W jaki sposób więc przeprowa- go idea (jak każdego szyfru asymetrycznego) polega na użyciu dwóch k luczy: publiczne-
dzimy proces rejestracji nowego kon- go i prywatnego. Oba z nich są generowane przez odbiorcę wiadomości, po czym klucz
ta na serwerze? Musimy w końcu prze- publiczny jest jawnie przesyłany nadawcy wiadomości, może też być zamieszczony np. na
stronie WWW do pobrania. Nadawca szyfruje tekst za pomocą klucza publicznego i wysy-
słać na serwer informacje o nowym ha- ła go odbiorcy. Zaszyfrowany tekst można z kolei odszyfrować tylko kluczem prywatnym,
śle: z oczywistych powodów nie chce- który posiada odbiorca. Nie wnikając w teorię matematyczną, która leży u podstaw RSA,
my tego robić przy użyciu czystego warto zapamiętać, że klucz publiczny składa się z dwóch części: wykładnika publicznego
tekstu; przesyłanie skrótu takiego ha- (ang. public exponent) i modułu (ang. modulus), a klucz prywatny – z wykładnika prywat-
nego (ang. private exponent) i tego samego modułu – te informacje będą nam potrzebne
sła również nie uchroni nas przed nie- później, gdy będziemy korzystali z klas PEAR-owych do obsługi RSA.
bezpieczeństwem podsłuchu: ktoś, kto
w tym momencie przechwyci ten skrót
(rozpozna, że chodzi o dodawanie no- Tworzymy system JavaScript). W pierwszym przypadku,
wego konta np. po danych wysyłanych bezpiecznego logowania do szyfrowania za pomocą RSA posłuży
z formularza), będzie mógł go potem dla aplikacji „Notatki nam klasa Crypt_RSA z repozytorium
zwyczajnie wykorzystać przy logowa- osobiste” PEAR (pear.php.net). Po stronie klienta
niu się na serwerze. Szyfrowanie da- Pokażemy teraz, jak wykorzystać algorytm użyjemy natomiast implementacji algo-
nych za pomocą klucza symetryczne- szyfrujący RSA w PHP, tworząc system rytmu RSA w języku JavaScript umiesz-
go (np.3DES lub twofish) również nie bezpiecznego logowania dla aplikacji czonej na stronie http://ohdave.com/rsa/.
wchodzi w grę, gdyż taki klucz musiał- Notatki osobiste, która będzie służyła jako Musimy z niej pobrać pliki: BigInt.js, Bar-
by być znany zarówno klientowi, jak i nasz osobisty notatnik online. rett.js oraz RSA.js. Wymianę danych
serwerowi, co oznacza konieczność je- będziemy obsługiwać przy pomocy
go transmisji przez Internet oraz czy- Założenia technologii AJAX, a konkretnie projektu
ni go podatnym na podsłuch. Jedy- Chcemy, aby nasza aplikacja posiadała advAJAX (zarówno o tym projekcie, jak
nym sensownym rozwiązaniem proble- system logowania poszczególnych użyt- i o technologii AJAX pisaliśmy w nume-
mu zabezpieczenia hasła będzie uży- kowników oraz możliwość dodawania no- rze 1/2006). Pobieramy więc plik adva-
cie asymetrycznego algorytmu szy- wych kont. Każdy posiadacz konta będzie jax.js ze strony http://advajax.anakin.us
frującego RSA. Algorytm ten działa w mógł wyświetlać swoje notatki, a także do- i zabieramy się do pracy.
oparciu o dwa klucze: publiczny (który dawać nowe. Cała komunikacja pomiędzy
jest ogólnie dostępny) i prywatny (do- klientem a serwerem będzie szyfrowana. Rejestracja kont
stępny wyłącznie na serwerze). Wię- Obsługą procesu rejestracji nowych kont
cej informacji na temat algorytmu RSA Przygotowania zajmą się trzy pliki:
zamieściliśmy w Ramce RSA: Zasada Tworząc nasz system bezpiecznego
działania. logowania skorzystamy z gotowych Ÿ register.php – będzie odpowiadał za
implementacji algorytmów szyfrowania, wyświetlenie formularza zakładania
zarówno tych działających po stronie konta wraz z wygenerowanym wcze-
Podstawy MD5 serwera (w PHP), jak i klienta (w języku śniej kluczem publicznym,
MD5 jest algorytmem haszującym, zwa-
nym również jednokierunkową funkcją
skrótu. W wyniku jego działania, w
oparciu o podstawione dane powstaje
unikalny 128-bitowy skrót. Odtworze-
nie danych na jego podstawie nie jest
możliwe. Unikalność i jednokierunko-
wość działania MD5 umożliwia jego za-
stosowanie np. przy sprawdzaniu haseł
(na serwerze zamieszczane są jedynie
skróty haseł, aby uniemożliwić prze-
chwycenie samych haseł w razie kra-
dzieży danych), podpisywaniu plików
– umieszczanym w wielu repozytoriach
plikom towarzyszą skróty MD5: oblicza-
jąc skrót pobranego pliku i porównując
go z tym zamieszczonym na serwerze
możemy sprawdzić, czy archiwum nie
jest uszkodzone (to samo da się zasto-
sować np. przy porównywaniu zawar-
tości wypalonej płyty CD lub DVD z jej
obrazem, choć trwa to dość długo).
Rysunek 1. Schemat działania RSA

PHP Solutions Nr 6/2006 www.phpsolmag.org 33


Bezpieczeństwo RSA w PHP

Ÿ register.js – będzie w nim następo-


Listing 1. Plik register.php – generowanie kluczy i wyswietlenie formularza wało szyfrowanie danych zawartych
w formularzu oraz ich wysyłanie na
<?php
serwer,
session_start(); Ÿ register2.php – w tym pliku nastą-
pi odszyfrowanie danych i założenie
// generowanie pary kluczy konta.
require_once 'Crypt/RSA.php';
$key_pair = new Crypt_RSA_KeyPair(512);
$public_key = $key_pair->getPublicKey(); Generowanie kluczy
$private_key = $key_pair->getPrivateKey(); Spójrzmy na Listing 1, na którym za-
mieściliśmy plik register.php. Zacznie-
$enc_exp = $public_key->getExponent(); my od wygenerowania pary kluczy
$dec_exp = $private_key->getExponent();
(publicznego i prywatnego) poprzez
$modulus = $public_key->getModulus();
utworzenie obiektu $key_pair klasy
// zapamiętanie danych do późniejszego dekodowania Crypt_RSA_KeyPair oraz użycie jej me-
$_SESSION['rsa']=serialize($key_pair); tod getPublicKey() i getPrivateKey().
Oba wygenerowane klucze stanowią
// konwersja danych do szyfrowania na kod szesnastkowy (heksadecymalny)
osobne obiekty, z których następnie
$enc_exponent = bin2hex($enc_exp);
$mod = bin2hex($modulus); wydobywamy oba wspomniane wcze-
śniej (zob. Ramka RSA: zasada dzia-
?> łania) wykładniki (getExponent()) oraz
<html> moduł (getModulus()). Następnie se-
<head>
rializujemy (serialize()) i zapisujemy w
<title>MyNotes - registration</title> sesji obiekt $key_pair do późniejszego
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/> wykorzystania oraz konwertujemy klucz
<script type="text/javascript" src="advajax.js"></script> publiczny (a właściwie jego wykładnik i
<script type="text/javascript" src="BigInt.js"></script> moduł) z postaci binarnej na wartość
<script type="text/javascript" src="Barrett.js"></script>
szesnastkową (bin2hex()), ponieważ
<script type="text/javascript" src="RSA.js"></script>
algorytm RSA, z którego będziemy ko-
<!-- skrypt obsługujący formularz za pomocą AJAX --> rzystać po stronie przeglądarki, wyma-
<script type="text/javascript" src="register.js"></script> ga podania danych dotyczących klucza
<script type="text/javascript"> właśnie w takim formacie. Po wykona-
var enc_exp = "<?=$enc_exponent?>";
niu tych operacji przechodzimy do ge-
var modulus = "<?=$mod?>";
</script> nerowania formularza dodawania kon-
ta (kod HTML). Jedynym dynamicznym
</head> elementem, który umieścimy w tym ko-
<body onload="updateObjects()"> dzie, będzie osadzony w prezentowa-
<b>MyNotes - Registration</b><br/>
nym pliku fragment skryptu JavaScript,
<form method="post" action="register2.php" id="registerForm"> który odpowiada za dołączenie plików:
<label for="username">Login: advajax.js, BigInt.js, Barrett.js i RSA.js
</label> oraz utworzenie zmiennych JavaScript
<input type="text" name="username" id="username" /><br/> enc_exp i modulus, przechowujących
<label for="password">Password:
wartość wykładnika publicznego (szy-
</label>
<input type="password" name="password" id="password" /><br/> frującego) i modułu.
<label for="name">e-mail:
</label> Szyfrowanie
<input type="text" name="email" id="email" /><br/> Spójrzmy teraz na Listing 2, na którym
<label for="name">Name:
przedstawiamy napisany w języku Ja-
</label>
<input type="text" name="name" id="name" /><br/> vaScript obiekt updateObjects() (pa-
<label for="surname">Surname: miętajmy, że w JavaScript nie ma klas,
</label> tylko pojedyncze obiekty, definiowane
<input type="text" name="surname" id="surname" /><br/> analogicznie, jak funkcje). Będzie on
<input type="submit" value="Register" id="submitBtn" />
wywoływany przy wystąpieniu zdarze-
</form>
nia onload zdefiniowanego w znacz-
<div style="margin-top: 10px" id="response"></div> niku <body>, czyli po każdym załado-
waniu strony WWW umieszczonej po-
</body> między <body> a </body>. Wewnątrz
</html>
updateObjects() tworzymy parę kluczy
RSA, czyli obiekt RSAKeyPair, na pod-

34 www.phpsolmag.org PHP Solutions Nr 6/2006


RSA w PHP Bezpieczeństwo

stawie publicznego wykładnika i modu- szyfrowanie przesyłanych danych, czyli Deszyfrowanie i tworzenie konta
łu. Prywatny wykładnik nie jest nam po- wartości wszystkich pól formularza (na- Przejdźmy do omówienia zawartości
trzebny, więc wpisujemy 0. Następnie zwy użytkownika, hasła, adresu ema- wspomnianego już pliku register2.php
wdrażamy obsługę formularza za po- il, imienia i nazwiska) za pomocą al- (Listing 3). Otrzymuje on przekazane za
mocą obiektu advAJAX. Metoda assign gorytmu RSA. Funkcja onSuccess wy- pomocą advAJAX dane z formularza,
pobiera (jako parametr) nazwę formula- świetla wewnątrz znacznika <div> (o które musimy teraz odszyfrować. Posłu-
rza oraz obiekt zawierający metody je- id=”response”) dane zwrócone przez żymy się do tego zapisanym wcześniej
go obsługi. Funkcja OnInitialization plik register2.php, do którego wysłali- w sesji jako rsa obiektem $key_pair,
jest wykonywana jeszcze przed wysła- śmy wprowadzone w formularzu war- który nazwiemy $rsa_keys i zdeseria-
niem formularza. To w niej następuje tości. lizujemy (unserialize()). Następnie
wydobędziemy z tego obiektu klucz
Listing 2. Plik register.js – szyfrowanie i wysyłanie danych prywatny, który także będzie obiektem
o nazwie $priv. Potem utworzymy nowy
function updateObjects() { obiekt $rsa_obj klasy Crypt_RSA i wy-
var key; wołujemy jego metodę DecryptBinary()
key = new RSAKeyPair(enc_exp,0,modulus);
odpowiedzialną za deszyfrowanie
function $(id) { return document.getElementById(id); }
advAJAX.assign($("registerForm"), { danych (każdego z przesłanych pól for-
onInitialization : function(obj) { mularza z osobna). Metoda ta przyjmuje
// szyfrowanie danych z formularza dwa parametry: zaszyfrowany tekst oraz
obj.parameters["username"]=encryptedString( utworzony przez nas wcześniej klucz
key,obj.parameters["username"]);
prywatny $priv (obiekt klasy Crypt_RSA_
obj.parameters["password"]=encryptedString(
key,obj.parameters["password"]); KeyPair). Ponieważ dane pochodzące
obj.parameters["email"]=encryptedString(key,obj.parameters["email"]); z formularza zostały po zaszyfrowaniu
obj.parameters["name"]=encryptedString(key,obj.parameters["name"]); przekonwertowane na system szesnast-
obj.parameters["surname"]=encryptedString(key,obj.parameters["surname"]); kowy, więc musimy je przywrócić do
},
formy binarnej przy użyciu funkcji he-
onSuccess : function(obj) {
$("response").innerHTML=obj.responseText; x2bin(), którą napiszemy sami (Listing 4),
}, gdyż nie została ona zaimplementowana
onError : function() { w PHP.
alert("Can't connect to server."); Mając odszyfrowane dane użyt-
}
kownika, możemy przystąpić do jego
});
} rejestracji w serwisie. Robimy to np.
przy pomocy klasy user, której imple-
Listing 3. Plik register2.php – deszyfrowanie i zapisanie danych nowego usera mentację pominęliśmy, gdyż nie jest
ona istotna dla ukazania technik kryp-
<?php
session_start();
tograficznych.
require_once 'Crypt/RSA.php';
// odczytanie przechowywanego obiektu (kluczy) Kryptografia hybrydowa
$rsa_keys = unserialize($_SESSION['rsa']); Jako kryptografia hybrydowa rozumiane
$priv = $rsa_keys->getPrivateKey();
jest jednoczesne użycie kryptografii sy-
$rsa_obj = new Crypt_RSA;
// deszyfrowanie
metrycznej i asymetrycznej. Dotychczas
$username=$rsa_obj->decryptBinary(hex2bin($_POST['username']),$priv); szyfrowaliśmy dane tylko w jednym kie-
$password=$rsa_obj->decryptBinary(hex2bin($_POST['password']),$priv); runku: od użytkownika do serwera. Aby
$email = $rsa_obj->decryptBinary(hex2bin($_POST['email']),$priv); zrealizować nasz projekt, będziemy jed-
$name = $rsa_obj->decryptBinary(hex2bin($_POST['name']),$priv);
nak potrzebowali szyfrowania dwukierun-
$surname = $rsa_obj->decryptBinary(hex2bin($_POST['surname']),$priv);
// tworzenie nowego konta
kowego, gdyż w jedną stronę będziemy
$user = new user; wysyłali nasz login i hasło oraz dodawali
$result = $user->register($username,$password,$email,$name,$surname); nowe notatki, a w drugą – wyświetlali ist-
if ($result) { echo "Your account are successfully created."; niejące notatki. Zabezpieczenie danych
}else { echo "error: your account can't created!"; }
przy tych czynnościach za pomoca sa-
?>
mego RSA byłoby trudne, wykorzystamy
Listing 4. Funkcja hex2bin więc dodatkowo symetryczny algorytm
blowfish.
function hex2bin($hex) {
$str="";
Przygotowania
for($i=0;$i<strlen($hex);$i=$i+2) {
$str.=chr(hexdec(substr($hex,$i,2))); Będziemy więc potrzebowali implemen-
} return $str; tacji algorytmu blowfish. W PHP posłu-
} żymy się do tego klasą Crypt_Blowfish
z repozytorium PEAR. Po stronie prze-

PHP Solutions Nr 6/2006 www.phpsolmag.org 35


Bezpieczeństwo RSA w PHP

glądarki internetowej, obsługą blowfisha


Listing 5. Formularz logowania zajmie się plik encrypt.js, który możemy
pobrać ze strony www.farfarfar.com lub
// ...
<script type="text/javascript" src="encrypt.js"></script> bezpośrednio z http://limak.blg.pl/crypto/
//... encrypt.js. Cała nasza aplikacja, podob-
<body onload="updateObjects()"> nie jak to było w przypadku rejestracji,
<b>MyNotes - Your own on-line notices</b><br/> będzie się składała z trzech plików:
<div id="html">
index.php, index.js i index2.php, których
<form method="post" action="index2.php" id="loginForm">
<label for="username">Login:</label> zadanie jest analogiczne do plików z po-
<input type="text" name="username" id="username" /><br/> przedniego przykładu.
<label for="password">Password:</label>
<input type="password" name="password" id="password" /><br/> Logowanie
<input type="hidden" name="blowfish" id="blowfish" value="" />
Spójrzmy na Listing 5. Przedstawia-
<input type="submit" value="Login" id="submitBtn" />
</div> my na nim fragment pliku index.php
– w miejscu, w którym różni się on
Listing 6. index.js – obsługa AJAX od register.php. Dołączamy w nim też
skrypt encrypt.js w języku JavaScript,
function updateObjects() {
który odpowiada za szyfrowanie i de-
// tworzenie klucza publicznego szyfrowanie algorytmem blowfish. Tym
var key; razem zagnieździmy nasz formularz
key = new RSAKeyPair(enc_exp,0,modulus); wewnątrz znacznika <div>, któremu
// tworzenie klucza symetrycznego
nadamy id html: później będziemy dy-
var blowfishKey = "";
var chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
namicznie zastępowali ten fragment
var i; kodu inną treścią.
for (i=0; i<25; i++) {
blowfishKey += chars.charAt(Math.floor(Math.random()*62)) Obsługa AJAX
}
Różnice między plikiem index.js a regi-
document.forms["loginForm"].elements["blowfish"].value=blowfishKey;
function $(id) { return document.getElementById(id); }
ster.js są znacznie większe, niż pomię-
advAJAX.assign($("loginForm"), { dzy index.php a register.php. Na Listin-
onInitialization : function(obj) { gu 6 prezentujemy obsługę praktycznie
// szyfrowanie danych z formularza (RSA)) całej szyfrowanej wymiany pomiędzy
obj.parameters["username"] = encryptedString(key, obj.parameters[
plikami index.php i index2.php. Tworzy-
"username"]);
obj.parameters["password"] = encryptedString(key, obj.parameters[
my w nim zarówno klucz asymetrycz-
"password"]); ny dla algorytmu RSA, jak i losowy ciąg
obj.parameters["blowfish"] = encryptedString(key, blowfishKey); znaków będący kluczem symetrycznym
}, blowfish.
onSuccess : function(obj) {
Następnie korzystamy ze znanego
// deszyfrowanie blowfish
$("html").innerHTML = secureDecrypt(obj.responseText, blowfishKey);
już nam obiektu advAJAX, który wystę-
} puje trzykrotnie. Pierwszy raz odwołu-
}); je się do formularza loginForm (advA-
advAJAX.assign($("addNoteForm"), { JAX.assign) i za pomocą RSA szyfru-
onInitialization : function(obj) {
je login, hasło oraz dodatkowo wyge-
// szyfrowanie blowfish
obj.parameters["title"] = secureEncrypt(obj.parameters["title"],
nerowany klucz blowfish, a następnie
blowfishKey); przekazuje je do pliku login2.php. Od
obj.parameters["note"] = secureEncrypt(obj.parameters["note"], tej pory, po otrzymaniu klucza blow-
blowfishKey); fish przez serwer, wymiana szyfrowa-
},
nych danych odbywa się przy pomo-
onSuccess : function(obj) {
$("html").innerHTML = obj.responseText;
cy algorytmu symetrycznego. RSA było
} potrzebne jedynie do przesłania syme-
}); trycznego klucza.
} Gdy teraz będziemy chcieli wyświe-
function goto(page, pageid=0) {
tlić wszystkie nasze notatki, wywoła-
advAJAX.post({
url : "index2.php?page="+page+"&pageid="+pageid,
my funkcję goto() z parametrem note,
onSuccess : function(obj) { aby w odpowiedzi z pliku index2.php
$("html").innerHTML = secureDecrypt(obj.responseText, blowfishKey); otrzymać zaszyfrowany fragment ko-
} du odpowiedzialny za ich wyświetle-
});
nie. To, w jaki sposób plik index2.php
}
szyfruje dane, widzimy na Listingu 7.
Aby odpowiednio zaszyfrować i odszy-

36 www.phpsolmag.org PHP Solutions Nr 6/2006


RSA w PHP Bezpieczeństwo

frować dane, po stronie przeglądarki


Listing 7. Zawartość pliku index2.php używamy w skrypcie JavaScript funk-
cji secureEncrypt() i secureDecrypt(),
<?php
natomiast w PHP wykorzystujemy PE-
session_start(); AR-ową klasę Crypt_Blowfish, która
require_once 'Crypt/RSA.php'; jest bardzo intuicyjna i prosta w uży-
require_once 'Crypt/Blowfish.php'; ciu. Z listingów możemy wywniosko-
wać, że sam proces przesyłania nowej
echo "<a href=\"javascript:goto('notes')\">my notes</a> | ";
echo "<a href=\"javascript:goto('addnote')\">add note</a> | "; notatki w formie zaszyfrowanej (obiekt
echo "<a href=\"javascript:goto('logout')\">logout</a><br/><br/>"; advAJAX z parametrem odwołującym
się do formularza AddNoteForm) oraz
if(!isset($_GET['page'])) { // logowanie jej deszyfrowanie z poziomu PHP jest
$rsa_keys = unserialize($_SESSION['rsa']);
praktycznie identyczny, jak w przypad-
$priv = $rsa_keys->getPrivateKey();
$rsa_obj = new Crypt_RSA; ku pobierania istniejącej notatki z ser-
$username = $rsa_obj->decryptBinary(hex2bin($_POST['username']),$priv); wera, tyle że kolejność wykonywania
$password = $rsa_obj->decryptBinary(hex2bin($_POST['password']),$priv); czynności jest odwrotna.
$blowfishKey = $rsa_obj->decryptBinary(hex2bin($_POST['blowfish']),$priv);

$_SESSION['username'] = $username;
Podsumowanie
$_SESSION['password'] = $password; W przedstawionej aplikacji pokazali-
$_SESSION['blowfishKey'] = $blowfishKey; śmy, w jaki sposób można skorzystać
z dwóch metod kryptograficznych (asy-
$user = new user($username, $password); metrycznej i symetrycznej) oraz poda-
$notes = $user->getNotes();
liśmy przykładowe zastosowanie tych
$html="";
technik. Potęga RSA w połączeniu z
foreach ($notes as $note) { prostotą języka PHP jak też z całkiem
$html.="<a href=\"javascript:goto('note','".$note['id']."')\">"; niezłą funkcjonalnością JavaScrip-
$html.=$note['title']."</a><br/>"; tu umożliwia tworzenie naprawdę bez-
}
piecznych aplikacji, które będą odpor-
$blowfish = new Crypt_Blowfish($blowfishKey); ne na podsłuch i przechwytywanie da-
echo(base64_encode($blowfish->Encrypt($html))); nych.
} Zachęcamy do korzystania z krypto-
else if ($_GET['page']=="addnote") { // wyświetlanie formularza grafii we własnych projektach – zwłasz-
$html = <<<heredochtml
cza tam, gdzie poufność przesyłanych i
<form method="post" action="index2.php=addnote2" id="addNoteForm">
<label for="title">title:</label> gromadzonych danych jest szczególnie
<input type="text" name="title" id="title" /><br/> istotna, poczynając od aplikacji do gro-
<textarea name="note" id="note"></textarea><br/> madzenia prywatnych notatek i syste-
<input type="submit" value="add" id="submitBtn" /> mów przekazywania wiadomości osobi-
</form>
stych, poprzez narzędzia używane we-
heredochtml;
$blowfish = new Crypt_Blowfish($_SESSION['blowfishKey']); wnątrz firmy (np. do zarządzania pro-
echo(base64_encode($blowfish->Encrypt($html))); jektem, danymi księgowymi czy biz-
} else if ($_GET['page']=="addnote2") { // dodawanie notatki nesplanem), po dostępne dla tysię-
$user = new user($_SESSION['username'], $_SESSION['password']); cy użytkowników jednocześnie syste-
my e-commerce, takie jak sklepy inter-
// ustawienie klucza blowfish
$blowfish = new Crypt_Blowfish($_SESSION['blowfishKey']); netowe czy pasaże aukcyjne. Na pohy-
//deszyfrowanie blowfish bel intruzom! n
$title = $blowfish->decrypt(base64_decode($_POST['title']));
$noteText = $blowfish->decrypt(base64_decode($_POST['note']));
//dodawanie notki
$user->addNote($title,$noteText);
echo "new note added";
} else if (($_GET['page']=="note")&&(isset($_GET['pageid']))) {
// wyświetlanie notki O autorze
$user = new user($_SESSION['username'], $_SESSION['password']);
Kamil Karczmarczyk jest uczniem Li-
$note = $user->getNoteById($_GET['pageid']); ceum Ogólnokształcącego. Od kilku lat
$html = "<b>".$note['title']."</b><br/>"; hobbystycznie zajmuje się programowa-
$html .= "<p>".$note['text']."</p>"; niem, między innymi w PHP. Interesuje
$blowfish = new Crypt_Blowfish($_SESSION['blowfishKey']); się bezpieczeństwem sieci, kryptografią
echo(base64_encode($blowfish->Encrypt($html))); oraz matematyką.
}
?> Kontakt z autorem:
limak@mmj.pl

PHP Solutions Nr 6/2006 www.phpsolmag.org 37


Projekty

XML_FastCreate
Stopień trudności: lll
Guillaume Lecanu

Technologia XML podbija świat, a PHP opiera


swój sukces na wsparciu dla czołowych
rozwiązań. Zastosowanie XML_FastCreate
pozwala poszerzyć i tak bardzo zaawansowane
możliwości PHP w dziedzinie tworzenia
i manipulacji dokumentami XML...

F
ormat XML (eXtensible Markup nerowanego kodu XML z zasadami okre-
Language) jest coraz częściej uży- ślonymi przez DTD, informowanie użyt-
wany na stronach internetowych, kownika o niezgodności, szybką konwer-
gdzie zawitał przede wszystkim jako sję na XHTML oraz bardzo łatwe tworze-
XHTML – nowy, promowany przez W3C nie rozbudowanych dokumentów, również
standard zapisu stron WWW. Innym po- jeśli chcemy je składać z wielu części.
pularnym zastosowaniem XML-a są for-
maty plików pakietów biurowych, w szcze- Instalacja
gólności stosowane od dawna w OpenOf- Jak już powiedzieliśmy, XFC jest pakietem
fice.org formaty SXW czy SXC oraz nowa- należącym do repozytorium PEAR (PHP
torskie i uznane za standard przez wiele Extension and Application Repository,
firm instytucji ODT (tekst) czy ODG (grafi-
ka). XML jest również wykorzystywany ja-
W SIECI Co należy wiedzieć...
ko format eksportu i importu danych wielu Potrzebna będzie podstawowa znajo-
programów czy też komunikacji pomiędzy mość zagadnień programowania obiekto-
aplikacjami znajdującymi się na różnych wego w PHP5. Przydatna będzie również
l http://pear.php.net/ ogólna wiedza na temat standardu XML.
packages/XML_FastCreate maszynach (klient-serwer), np. w pro-
– klasa XML_FastCreate tokołach typu SOAP czy XML-RPC. Ję- Co obiecujemy...
l http://xmlsoft.org – strona Pokażemy, jak za pomocą XML_Fast-
zyk PHP dysponuje dużymi możliwościa-
główna projektu libxml Create utworzyć prawidłowy kod XML.
l http://pear.php.net/ mi tworzenia i przetwarzania dokumentów Zademonstrujemy też, jak dokonywać
packages/Cache_Lite XML. W artykule pokażemy, jak użycie transformacji znaczników XML-a, wy-
– pakiet PEAR::Cache_Lite
PEAR-owego pakietu XML_FastCreate muszać sprawdzanie DTD, wykrywać
l http://lya.fr/pear/XML_
FastCreate/tests/ – przykła- błędy składni czy tworzyć dokumenty
(XFC) poszerza tę funkcjonalność, pozwa-
dy użycia XML_FastCreate w XHTML-u.
lając m.in. na wymuszanie zgodności ge-

38 www.phpsolmag.org PHP Solutions Nr 6/2006


XML_FastCreate Projekty

http://pear.php.net). Aby go zainstalować, XFC: pierwsze kroki jącą z innych tagów typowych dla HTML-a
wystarczy w linii poleceń systemu ope- Aby skorzystać z XML_FastCreate, do- i XHTML-a (<html>,<head> i <body>).
racyjnego użyć narzędzia pear. Jeżeli łączamy plik XML/FastCreate.php (zob. Patrząc na kod z Listingu 2 zauważymy,
takowego nie posiadamy, jego instalacja Listing 1) i tworzymy obiekt $x, będący że wykorzystane tam metody (oprócz
jest bardzo prosta i została szczegółowo instancją klasy XML_FastCreate (uwaga: toXML()) obiektu $x mają takie same
opisana na stronach PEAR-a. używamy w tym celu należącej do kla- nazwy, jak użyte przez nas tagi XHTML-
Mając narzędzie pear jesteśmy gotowi sy XML_FastCreate metody statycznej a. Wstawienie każdej z tych metod do
do instalacji XFC. W tym celu wystarczy factory()). Zainicjowanie $x wymaga po- skryptu powoduje automatyczne utworze-
wpisać pear install XML_FastCreate. dania dwóch parametrów: pierwszy z nich nie znacznika o takiej samej nazwie. Jest
Pamiętajmy, że instalacja niektórych pa- określa format wygenerowanego XML-a. to wielką zaletą XML_FastCreate, która
kietów opcjonalnych XML_FastCreate, My używamy formatu Text, co oznacza, ułatwia tworzenie nawet rozbudowanych
które są w wersji beta lub alpha, może że po użyciu metody toXML() (którą omó- dokumentów.
wymagać użycia dodatkowego parametru wimy później) uzyskamy XML w formie Zwróćmy też uwagę na zagnieżdżenie
narzędzia pear. Przykładowo, dla wersji tekstu (zserializowanej). Drugi parametr metod reprezentujących znaczniki: zaczy-
beta będzie to: stanowi tablicę asocjacyjną opcji, którą namy od metody:
również omówimy później.
pear -d preferred_state=beta Wspomnijmy, że jedną z najistotniej- $x->html()
install nazwa_pakietu szych spośród tych opcji jest właściwy na-
główek doctype, który zostanie następnie której parametrem jest:
Jeżeli ta składnia nie działa (co ma umieszczony w pierwszej linii pliku XML
miejsce w przypadku starszych wersji i będzie określał DTD, na którym oparto $x->head()
narzędzia pear), to musimy sprawdzić na dokument. Nagłówek ten możemy zdefi-
stronie pakietu, w jakiej fazie znajduje się niować ręcznie, ale XML_FastCreate daje która z kolei zawiera wywołania head() i
jego najnowsza wersja (stable, alpha lub nam do wyboru kilka gotowych definicji body(). W ten sposób tworzymy strukturę
beta) i uwzględnić tę fazę, jeśli jest różna odpowiadających najczęściej używanym znaczników w XML_FastCreate, która po
od stable, w sposób następujący: doctype. My użyjemy: użyciu metody toXML() zostanie przekształ-
cona na gotowy dokument XML (zob. Listing
pear install nazwa_pakietu-faza XML_FASTCREATE_DOCTYPE_ 3). UWAGA: toXML() jedynie wypisuje XML
XHTML_1_0_STRICT na ekran; aby zapisać dokument w zmien-
np.: nej, trzeba użyć metody getXML(), np.:
co w rezultacie daje nagłówek:
pear install XML_Tree-beta $xml_out=$x->getXML();
XHTML 1.0 Strict.
Wśród pakietów opcjonalnych współ- W tym przykładzie nie określiliśmy atrybutów
pracujących z XFC można wymienić np. Stwórzmy teraz najprostszy dokument znaczników. Aby je dodać do określonego
XML_Tree, XML_DTD, XML_Beautifier XML, który będzie stroną XHTML-ową znacznika, wystarczy umieścić je w tablicy
czy XML_HTMLSax. Ich użycie jest zale- zawierającą napis Hello World! w akapicie asocjacyjnej przekazanej jako pierwszy ar-
cane w celu pełnego wykorzystania możli- (<p>..</p>) oraz tekst XML_FastCreate ja- gument metody reprezentującej ten znacz-
wości XML_FastCreate. ko tytuł (tag <title>..</title>) i korzysta- nik. Przykładowo, aby utworzyć link (tag <a
href>...</a>) prowadzący do strony głównej

Listing 1. Tworzenie instancji XML_FastCreate repozytorium PEAR, wpisujemy:

<?php $x->a(array(‘href’=>’http://
require_once 'XML/FastCreate.php'; pear.php.net’),’P.E.A.R.’)
$x=&XML_FastCreate::factory('Text',array(
'doctype' =>
XML_FASTCREATE_DOCTYPE_XHTML_1_0_STRICT Efektem będzie:
)
); <a_href=”http://pear.php.net”>
?> P.E.A.R.</a>

Listing 2. Hello World! w XML-u przy użyciu XML_FastCreate

$x->html(
$x->head(
$x->title('XML_FastCreate')), $x->body($x->p('Hello World !'))
);

$x->toXML(); Rysunek 1. Przykład błędu polegające-


go na niezgodności XML-a z jego DTD

PHP Solutions Nr 6/2006 www.phpsolmag.org 39


Projekty XML_FastCreate

Już na podstawie tych przykładów może- Jak już wiemy, użycie argumentu Bez ograniczeń możemy łączyć blo-
my się przekonać, jak łatwe i szybkie jest Text podczas tworzenia instancji klasy ki wygenerowane z użyciem parametru
tworzenie XML-a przy użyciu XML_Fast- XML_FastCreate powoduje, że skutkiem Text podczas tworzenia instancji doku-
Create. użycia metody toXML() będzie wygenero- mentu. Nie będzie również problemu, je-
wanie XML-a w formie tekstowej (zseriali- żeli format jest inny (np. XML_Tree), o ile
Wymuszanie zowanej). Gdybyśmy zamiast Text podali wszystkie bloki są tego samego formatu.
i weryfikacja XML_Tree, utworzony zostałby obiekt klasy Spójrzmy na Listing 6: łączymy na nim
zgodności z DTD XML_Tree, która jest dostępna jako osobny dwa bloki typu Text, wygenerowane przy
Tworząc lub modyfikując dowolny doku- pakiet PEAR-owy. użyciu utworzonej wcześniej instancji $x
ment XML powinniśmy się upewnić, czy Co więcej, możemy złożyć dokument klasy XML_FastCreate (nie musimy po-
będzie on zgodny ze standardem DTD, XML z kilku mniejszych bloków, co się dawać żadnych parametrów oprócz ty-
do czego często używamy dodatkowych często przydaje, np. gdy generujemy jego pu XML-a).
narzędzi. Użycie XML_FastCreate pozwa- elementy korzystając z pętli czy instrukcji
la nam zaoszczędzić czas dzięki funkcji warunkowych. Jest to wręcz zalecane, Transformacje XML
automatycznego wyświetlania błędów gdyż umożliwia zachowanie przejrzysto- XFC posiada opcję szybkiej transforma-
składni XML-a. ści – zarówno kodu aplikacji generującej cji, której zadaniem jest zamiana wy-
Aby wymusić sprawdzanie DTD, mu- XML, jak i samego XML-a. branych znaczników na inne. Przykła-
simy mieć odpowiedni plik zawierający de-
finicje DTD (niektóre z nich, np. XHTML, Listing 3. Rezultat XHTML przykładu z Listingu 2
znajdują się w katalogu dtd zainstalowane-
go XML_FastCreate) i uruchomić jego ła- <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
dowanie podczas tworzenia instancji klasy <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/
xhtml1/DTD/xhtml1-strict.dtd">
XML_FastCreate. To drugie wykonamy do-
<html><head><title>XML_FastCreate</title></head>
dając opcję dtd do wspomnianej wcześniej <body><p>Hello World !</p></body></html>
tablicy asocjacyjnej (Listing 4). My załadu-
jemy plik xhtml_1_0_strict.dtd, będący defi- Listing 4. Tworzymy instancję XML_FastCreate sprawdzającą zgodność XML
nicją standardu XHTML 1.0 w wersji strict. z jego DTD
Jeżeli w składni XML-a wystąpi nie- $x =& XML_FastCreate::factory('Text',
zgodność z DTD, komunikat o błędzie array(
zostanie wyświetlony podczas wywołania 'doctype' =>
wspomnianej już metody toXML(). Po- XML_FASTCREATE_DOCTYPE_XHTML_1_0_STRICT,
'dtd' => 'xhtml_1_0_strict.dtd'
zwala to np. na wyświetlenie go na końcu
)
strony WWW. );
Na Listingu 5 podajemy przykład skryp-
tu generującego stronę XHTML-ową, w któ- Listing 5. Sprawdzamy zgodność XML z jego DTD
rej celowo wstawiliśmy błąd polegający na
<?php
pominięciu atrybutu alt w znaczniku <img
require_once 'XML/FastCreate.php';
src> (co było dopuszczalne w HTML-u, ale $x =& XML_FastCreate::factory('Text',
jest zabronione w XHTML-u). Po utworze- array(
niu strony, konwertujemy ją do postaci do- 'doctype' => XML_FASTCREATE_DOCTYPE_XHTML_1_0_STRICT,
kumentu XML jako zmienną $err. Następ- 'dtd' => 'xhtml_1_0_strict.dtd'
)
nie korzystając z metody statycznej PEAR:
);
:isError() sprawdzamy, czy zmienna ta $x->html(
zawiera komunikat o błędzie: jeżeli tak, to $x->head($x->title('XML_FastCreate')),
go wypisujemy przy pomocy metody $err- $x->body(
>getMessage(). Przykład wykonania tego $x->p($x->img(array('src' => 'soleil.png'))
)
skryptu przedstawiamy na Rysunku 1.
)
);
Manipulacja if (PEAR::isError($err = $x->toXML())) {
dokumentami XML echo nl2br(htmlSpecialChars($err->getMessage()));
Przejdźmy teraz do omówienia rozmaitych }
?>
metod generowania i manipulacji doku-
mentami XML. Listing 6. Przykład konkatenacji (łączenia) dwóch tagów

$hello = $x->p('Hello');
$world = $x->p('World');
$body = $x->body($hello,$world,);
Rysunek 2. Entytki umożliwiające zako- $x->html($x->head($x->title('XML_FastCreate')),$body);
dowanie znaków specjalnych w doku-
mentach XML

40 www.phpsolmag.org PHP Solutions Nr 6/2006


Projekty XML_FastCreate

dowo, możemy w ten sposób uprościć XML_FastCreate w tablicy asocjacyjnej na cały zestaw tagów (<h1><span>...</
sobie składnię XML-a czy XHTML-a, wpisujemy opcję translate, która z ko- span></h1>).
oznaczając tagi po swojemu, w spo- lei otwiera tablicę znaczników do tłuma- Gdybyśmy nie korzystali z XFC w ce-
sób dostosowany do naszych prefe- czenia.Mamy tam tagi opisujące notkę lu ułatwienia konwersji znaczników, aby
rencji czy potrzeb programu, który z informacyjną (<news>, <desc>, <title> osiągnąć ten sam efekt co na Listingu 7
tych danych korzysta. Transformacja z i <date>), które będą zamienione na musielibyśmy ręcznie wstawić wartości do
XFC przyda się również przy konwersji oryginalne znaczniki XML-a. Zwróć- odpowiednich tagów XML-a, jak na Listin-
z XML-a na HTML-a. Spójrzmy na Li- my szczególną uwagę na znacznik gu 8. Wracając do Listingu 7 zauważmy,
sting 7: przy tworzeniu instancji klasy <title>: zostanie on przekonwertowany że po zdefiniowaniu tablicy transformacji
tworzymy kod XML metodą tradycyjną.
Listing 7. Przykład zastosowania opcji ‘translate’ Tam również wystąpi znacznik <title>,
tyle że nie jest on tytułem naszej wiado-
<?php mości, lecz standardowym tagiem <title>
require_once 'XML/FastCreate.php'; oznaczającym tytuł strony WWW i znanym
$x =& XML_FastCreate::factory('Text',
z HTML-a oraz XHTML-a. Aby uniknąć je-
array(
'doctype' => XML_FASTCREATE_DOCTYPE_XHTML_1_0_STRICT, go konwersji (która nastąpiłaby zgodnie
'dtd' => 'xhtml_1_0_strict.dtd', z regułami przetwarzania znaczników
'translate' => array( naszych wiadomości), poprzedzamy jego
'news' => array('div'), nazwę podkreśleniem (_title). Dzięki
'desc' => array('p'),
temu generowany będzie standardowy
'title' => array('<h1 class="title"><span>', '</span></h1>'),
'date' => array('<span class="date">', '</span>') znacznik <title>..</title>.
)
) Opcje i metody XFC
); XFC zawiera kilka dodatkowych, przydat-
$x->html(
nych metod:
$x->head($x->_title('XML_FastCreate')),
$x->body($x->news(
$x->title('News'), Ÿ comment() – pozwala na dodawanie
$x->date('10-12-2005'), komentarzy w kodzie XML; ogranicz-
$x->desc('blah blah blah') nikami komentarza są oczywiście tagi
)
<!-- i -->. Przykładowo, następujące
)
); użycie tej metody:
if (PEAR::isError($err = $x->toXML())) { Ÿ $x->comment($x->p(‘Hello World
echo nl2br(htmlSpecialChars($err->getMessage())); !’))
} Ÿ spowoduje wygenerowanie kodu:
?>
Ÿ <!-- Hello World! -->.

Listing 8. Wariant cytowany w Listing 7 bez opcji ‘translate’ Ÿ cdata() –otacza wybraną zawartość
znacznikami CDATA. Jest to niezbędne,
$x->div( kiedy musimy wprowadzić zawar-
$x->h1(array('class' => 'title'),$x->span('News')),
tość, która niekoniecznie jest zgodna
$x->span(array('class' => 'date'), '10-12-2005'),
$x->p('blah blah blah')
z DTD, np. kod typu JavaScript czy
); jakiś rodzaj importu stylów w ramach
strony XHTML. Bez CDATA zwyczajnie
Listing 9. Przykład zastosowania metody cdata() nie da się umieścić takiej zawartości
w dokumencie XML. Przykład użycia
$x->style(array(
'type' => 'text/css', 'media' => 'all'), cdata() przedstawiamy na Listingu 9.
$x->cdata("@import url('example.css');") Ÿ quote() – metody tej używamy, aby
); przekonwertować wybrane znaki na
entytki (ang. entities). Jest to koniecz-
Listing 10. Tworzymy instancję XML_FastCreate sprawdzającą zgodność XML
ne, gdyż umieszczenie pewnych zna-
z jego DTD poprzez program zewnętrzny
ków (np. &) w dokumentach XML jest
$x =& XML_FastCreate::factory('Text', niemożliwe i wygeneruje błąd. Metody
array( quote() możemy używać w sposób
'doctype' =>
zautomatyzowany, jeżeli zadeklaruje-
XML_FASTCREATE_DOCTYPE_XHTML_1_0_STRICT,
my ją podczas definiowania obiektu
'dtd' => 'xhtml_1_0_strict.dtd',
'file' => '/tmp/XFC.xml', klasy XML_FastCreate jako true.
'exec' => 'xmllint --valid --noout /tmp/XFC.xml 2>&1', W takiej sytuacji, jeśli nie chcemy
) używać tej opcji wobec określonych
);
części dokumentu, możemy wywołać
opcję noquote() (Rysunek 2). UWA-

42 www.phpsolmag.org PHP Solutions Nr 6/2006


XML_FastCreate Projekty

GA: jeżeli tworzymy dokument znaczniki o nazwach pokrywających padku edytora vim wystarczy umieścić
XHTML, wyszukiwarka Microsoft In- się z nazwami tych metod. Aby unik- w ~/.vimrc linię:
ternet Explorer (wersja 6 i niższe) nie nąć tego problemu i zmusić XFC do
rozpoznaje encji &apos;. Aby uniknąć traktowania metody jako odnoszącej map ,fc :!HTML2XFC.php<CR>
jej konwersji, musimy umieścić opcję się do znacznika, przed nazwą tagu
apos w false. wstawiamy znak podkreślenia, np. wybrać do konwersji kod HTML i urucho-
_quote(). mić makro wpisując ,fc.
Należy też pamiętać o tym, że:
Wygoda Podsumowanie
Ÿ Kod XML jest generowany bez for- użytkowania XFC W tym artykule pokazaliśmy najciekaw-
matowania. Oznacza to, że wszystkie Aby uprościć tworzenie projektu korzy- sze spośród podstawowych zastosowań
znaczniki są wklejane i zagnieżdżane stającego z XML_FastCreate, zalecane XML_FastCreate. Jak widzimy, użycie tej
bez przechodzenia do nowej linii czy jest, aby inicjowanie instancji tej klasy biblioteki znacznie upraszcza tworzenie,
stosowania tabulacji. Jeżeli chcemy odbywało się w pliku, który będziemy do- manipulację i konwersję dokumentów
uczynić kod XML bardziej czytelnym, łączać do każdej ze stron. Inną opcją jest XML. Nie bez znaczenia jest również to,
dodajmy opcję indent do true. Takie generowanie tego obiektu wewnątrz funk- że należy ona do repozytorium PEAR,
formatowanie może jednak sprawić, cji, która będzie wywoływana na końcu do którego trafiają wyłącznie sprawdzone
że dokumenty XML będą niezgodne każdej strony. i działające projekty. Wraz z dodatkami ty-
z DTD. Pamiętajmy też, że generowanie pu XML_Tree, XML_FastCreate powinna
Ÿ Niektóre nowe DTD, jak na przykład XML-a zajmuje czas, przez co korzystanie zająć miejsce wśród niezbędnych narzę-
XHTML 1.1, bazujące na kilku kar- z XML_FastCreate będzie wolniejsze, niż dzi każdego programisty, który korzysta
totekach, jeszcze nie są tolerowane. używanie gotowych, zapisanych na dysku z XML-a. n
Mimo to, za pomocą opcji file może- (czy w bazie danych) dokumentów XML.
my wydać XFC polecenie utworzenia Aby przyspieszyć dostęp do nich (tylko
pliku tymczasowego i wykonania pro- w przypadku plików), możemy użyć narzę-
gramu zewnętrznego za pomocą opcji dzia keszującego, np. PEAR::Cache_Lite. O autorze
exec, przeznaczonej do takiej analizy.
Program xmllint doskonale sobie radzi Konwersja Guillaume Lecanu jest autorem pakie-
tu programów XML FastCreate, rozwija-
z takimi problemami, a przykład jego dokumentów HTML nego od przeszło 12 lat. Jego pasja pro-
użycia w ramach naszej aplikacji pre- Konwersję dokumentu HTML na XML uła- gramowania zaczęła się od asemblera,
zentujemy na Listingu 10. twi załączony w pakiecie XML_FastCreate którego nauczył go na domowym kom-
puterze brat, twórca programów typu de-
Ÿ XFC ma kilka własnych, wbudowa- skrypt HTML2XFC.php. Za jego pomo-
mo. Niedawno Lecanu stworzył własną
nych metod, co przy specyficznym cą możemy przekształcić cały plik (skrypt firmę Noovea, która prezentuje pełnię je-
sposobie traktowania znaczników (me- wywołujemy wtedy w linii poleceń) albo go możliwości.
toda ma taką samą nazwę, jak znacz- wybrany fragment – uruchamiamy wte-
Kontakt z autorem:
nik, do którego się odnosi) powoduje dy HTML2XFC.php jako makro w wybra-
guillaume@lya.fr
problemy, jeżeli chcemy zdefiniować nym edytorze programistycznym. W przy-

R E K L A M A
Dla zaawansowanych

Rozwiązywanie problemów
przekrojowych z użyciem IoC
Stopień trudności: lll
Piotr Szarwas

W każdej aplikacji podzielonej na warstwy


występują elementy, których nie można przypi-
sać do żadnej z warstw i które stanowią twardy
orzech do zgryzienia nawet dla dużego zespo-
łu programistów. Istnieje jednak w miarę prosty
sposób poradzenia sobie z nimi: użycie konte-
nera IoC...

K
ażdą aplikację można podzielić nicznego MVC. Pojawienie się warstwy
na logiczne warstwy. Najczę- dostępu do danych wynika z zastoso-
ściej występują trzy warstwy: wania wzorca DAO, który uniezależnia
warstwa danych, logiki biznesowej i logikę biznesową od warstwy danych.
prezentacji. Wszystkie spośród nich Dzięki temu można łatwo wymieniać
powinny być nałożone na siebie tak, źródło danych. Schemat tego podziału
aby wyższa wiedziała tylko o istnie- pokazujemy na Rysunku 2.
niu jednej warstwy niższej; wszystko, Rozbicie systemu na warstwy zapew-
co znajduje się poniżej, powinno być nia szereg korzyści:
dla niej niedostępne. Z drugiej strony, Każdej z warstw można wymie-
warstwa niższa nie wymaga dla swoje- nić implementację, nie zmieniając jed-
go działania istnienia żadnej z warstw nocześnie implementacji pozostałych
wyższych. warstw. Jak już wspomnieliśmy, moż-
Podział ten ilustrujemy na Rysun- na na przykład wymienić warstwę DAO
ku 1. Można jeszcze bardziej dopraco-
wać ten schemat mówiąc, że aplikacja
powinna posiadać aż pięć warstw: war- Co należy wiedzieć...
Należy znać zasady programowania
stwę danych, dostępu do danych, logi-
obiektowego w PHP5 z wykorzystaniem
ki biznesowej/usług, kontrolerów i pre- wzorców projektowych.
zentacji. Ten drugi podział wynika bez-
Co obiecujemy...
pośrednio ze stosowania wzorców pro- Pokażemy, jak rozwiązać niektóre spo-
gramistycznych i architektonicznych. śród problemów przekrojowych (ang.
Przykładowo, kontrolery i prezentacja crosscutting concerns).
stanowią element wzorca architekto-

44 www.phpsolmag.org PHP Solutions Nr 6/2006


Crosscutting concerns Dla zaawansowanych

Niestety, praktyka bywa czasem


bardziej skomplikowana i nie wszyst-
kie zagadnienia programistyczne mo-
������������������� �������������������
żemy umieścić na jednej wybranej
warstwie. Te, których nie da się w ten
sposób potraktować, nazywamy pro-
������������������������� blemami przekrojowymi (ang. Cros- �������������������

scutting concerns). Najpopularniejsze


z nich to:
�������������� �������������������������
l
logowanie błędów i debugowanie,
l
bezpieczeństwo i uwierzytelnianie,
Rysunek 1. Podział aplikacji na trzy transakcje. �������������������������
logiczne warstwy. Warstwa wyższa wie
tylko o istnieniu warstwy znajdującej się Problem
bezpośrednio pod nią. Żadna z warstw logowania błędów
niższych nie wie nic o wyższych Rozważmy problem logowania błę-
��������������

(dostępu do danych) tak, aby obsługi- dów. Praktycznie każda aplikacja za-
wała inne źródło danych, nie zmienia- wiera elementy kodu odpowiadające
jąc jednocześnie logiki biznesowej. za logowanie i debugowanie. Czy jed- Rysunek 2. Podział aplikacji na pięć
Każda warstwa może być traktowa- nak możemy je przydzielić do którejś logicznych warstw. Warstwa wyższa wie
na jako spójna całość. Pozwala to m.in. warstwy? Niestety nie. Ich kod jest roz- tylko o istnieniu warstwy znajdującej się
bezpośrednio pod nią. Żadna z warstw
na tworzenie każdej warstwy przez proszony po całej aplikacji i występu-
niższych nie wie nic o wyższych
osobny zespół programistów, który nie je w każdej warstwie. Pamiętajmy, że
musi znać implementacji pozostałych z punktu widzenia logicznego podziału stycznie maleje. W przypadku logowa-
warstw. na warstwy, logowanie błędów nie jest nia problem nie jest tak wielki, ale już
Jeśli zostanie dobrze zaprojektowa- ani częścią logiki biznesowej, ani innej w sytuacji, gdy w aplikacji zaszyjemy
na, każda warwstwa(warstwa) może być warstwy. Bezpośrednie zakodowanie reguły bezpieczeństwa, czy też zdefi-
wykorzystywana wielokrotnie, w różnych logiki logowania w każdej z warstw po- niujemy miejsca, w których rozpoczy-
projektach lub miejscach tego samego woduje, że niezależność, przenaszal- nają się i kończą transakcje, cały kod
projektu. ność i elastyczność tej warstwy dra- przestaje w zasadzie być przenaszal-
ny i staje się mocno związany z regu-
Listing 1. Implementacja klasy MailSender służącej do wysyłania maili. Kod klasy łami biznesowymi danej aplikacji. Ry-
pozbawiony jest reguł problemów przekrojowych: logowania i bezpieczeństwa. sunek 3 pokazuje schematycznie nasz
Reguły te będą implementowane przez odpowiednie dekoratory problem.
Skoro problemy przekrojowe mogę
interface MailSender {
sprawić takie trudności, czy da się je
public function send($to, $from, $subject, $message);
} wydzielić do osobnych modułów, któ-
class MailSenderImpl implements MailSender { re nie będą w ogóle powiązane z żad-
public function send($to, $from, $subject, $message) { ną z warstw? Okazuje się, że tak. Roz-
return mail($to,$subject,$message,'From: '.$from); wiązania są dwa. Pierwsze, którym się
}
zajmiemy w tym artykule, będzie pole-
}
gało na wykorzystaniu wzorca Deko-
Listing 2. Przykładowa klasa dekorująca dowolną klasę implementującą interface rator i kontenera IoC, który zbudowa-
MailSender liśmy wspólnie w poprzednim artykule.
Drugi sposób opiera się na zastosowa-
class MailSenderDecorator implements MailSender {
private $mailSender;
niu programowania aspektowego (ang.
public function __construct(MailSender $mailSender ){ aspect-oriented programming - AOP).
$this->mailSender = $mailSender; Ten drugi sposób byłby dużo lepszym
} podejściem; niestety, obecnie dla PHP
public function send($to, $from, $subject, $message) {
nie istnieje żaden framework AOP, któ-
// Kod dekoratora
$result = $this->send($to,$subject,$message,'From: '.$from);
ry w wystarczającym stopniu imple-
// Kod dekoratora mentowałby wszystkie założenia pro-
return $result; gramowania aspektowego. Tak jak po-
} przednio, cały kod zaprezentowany w
}
artykule zostanie umieszczony na stro-
nie http://flexi.sf.net/.

PHP Solutions Nr 6/2006 www.phpsolmag.org 45


Dla zaawansowanych Crosscutting concerns

Wzorzec Dekorator
Zgodnie z definicją GoF (ang. Gang
of Four – termin, który określa czwór-
�������������������
kę programistów odpowiedzialnych za
zapoczątkowanie idei wzorców pro-
jektowych), zadaniem wzorca Deko-
rator jest wzbogacanie funkcjonalno- ������������������� ����������������

ści obiektów wybranych klas w sposób


dynamiczny, bez konieczności modyfi-
kowania oryginalnego kodu. Ponieważ �������������������������
ta definicja może być mało przejrzy-
sta, posłużymy się przykładem. Zbu-
dujemy prostą klasę MailSenderImpl, ������������������������� ���������������������
której zadaniem będzie wysyłanie ma-
ili (Listing 1).
Wiemy, że klasa ta będzie na ty-
��������������
le uniwersalna, że będziemy chcieli
wykorzystać ją w kilku innych, już ist-
niejących projektach. Niestety, każ-
Rysunek 3. Problemy przekrojowe znajdują się zawsze z boku każdego podziału na
dy z tych projektów ma inne wyma-
warstwy, a ich kod jest rozproszony pomiędzy tymi ostatnimi
gania biznesowe związane z regułami
logowania i bezpieczeństwa. Dlatego istnieje potrzeba wysłania maila, wy- Stosując wzorzec Dekorator udało się
nie wyposażymy klasy MailSenderIm- korzystamy do tego odpowiedni deko- nam osiągnąć dwie rzeczy. Po pierw-
pl w logikę związaną z tymi aspekta- rator, przykładowo: sze, uniwersalna klasa MailSender pozo-
mi, lecz zaimplementujemy tę ostatnią stała niezmieniona – cała logika związa-
w dekoratorach (Listing 2). Za każdym new MailSenderDecorator( na z wysyłaniem maili jest w jednym miej-
razem, gdy w którymś z projektów za- new MailSenderImpl()) scu. Po drugie, kod charakterystyczny dla
każdej z aplikacji znajduje się w osobnych
Listing 3. Implementacja nowej wersji kontenera IoC obsługującej dekorowanie klasach.
obiektów Wykorzystując wzorzec Dekorator
warto również pamiętać, że jest on bar-
class IoCContainerWithDecoratorSupport extends
dzo użyteczny, gdy kod źródłowy klas nie
DefaultIoCContainter {
jest dostępny: jego użycie stanowi wte-
private $decoratorsSupport = array();
public function create($className) { dy jedyną możliwość zmiany zachowa-
$classObj = parent::create($className); nia obiektu.

if (!$classObj instanceof IoCDecoratorSupport&&!$classObj


Wzorzec Dekorator
instanceof IoCDecorator){
foreach( $this->getDecoratorsSupport() as $decoratorSupport ) {
w połączeniu
if ( $decoratorSupport->match($className,$classObj) ) { z kontenerem IoC
$decoratorObj = parent::create( $decoratorSupport-> Wróćmy teraz do przedstawionego w po-
getDecoratorName() ); przednim artykule kontenera IoC. Jak za-
$decoratorObj->setObject($classObj);
pewne pamiętamy, kontener IoC to zwy-
$classObj = $decoratorObj;
czajnie konfigurowalna fabryka obiektów
}
} potrafiąca powołać do życia całe drzewa
} tych ostatnich. Pokażemy teraz, jak moż-
return $classObj; na wykorzystać kontener IoC tak, aby po-
}
za tworzeniem obiektów potrafił je także
dekorować.
public function setDecoratorsSupport( array $decoratorsSupport ) {
foreach($decoratorsSupport as $decorator) { Wyobraźmy sobie następujący
if ( !$decorator instanceof IoCDecoratorSupport ) { problem: klient, który zlecił nam pro-
throw new Exception('Class '.get_class($decorator). jekt, zażyczył sobie, aby każda ope-
' does not implement IoCDecoratorSupport interface');
racja modyfikacji danych (kto, kiedy i
}
co zmieniał) była logowana. Typowy
$this->decoratorsSupport[] = $decorator;
} programista zapewne rozwiązałby ten
} problem dokonując modyfikacji każdej
metody zmieniającej dane. To podej-
private function getDecoratorsSupport(){ return $this->decoratorsSupport; }
ście wydaje się najbardziej oczywiste,
}
ale niestety jest najgorszym z możli-
wych. Zmodyfikowane zostałyby bo-

46 www.phpsolmag.org PHP Solutions Nr 6/2006


Crosscutting concerns Dla zaawansowanych

wiem wszystkie kluczowe metody apli- by przestać go współdzielić. Jak więc Zabierzmy się więc do pracy.
kacji, co oznacza potrzebę przetesto- widać, podejście, które wydawało się Pierwszym krokiem będzie modyfika-
wania całego projektu od nowa. Dodat- najbardziej oczywiste, spowodowałoby cja kontenera IoC tak, aby na podsta-
kowym problemem byłaby konieczność całą lawinę problemów. wie zadanego wzorca mógł połączyć
przekazania do każdej z warstw aplika- My na szczęście będziemy mądrzejsi dekorowany obiekt z dekoratorem. W
cji informacji o tym, kto aktualnie wyko- i do rozwiązania problemu wykorzystamy tym celu rozszerzymy klasę DefaultI-
nuje daną metodę. Ponadto, jeżeli mo- kontener IoC. Nasze założenie jest nastę- oCContainer i zmodyfikujemy jej meto-
dyfikowany kod był wykorzystywany w pujące: nie możemy dokonać żadnej mo- dę create(). W nowej wersji tej klasy,
innych projektach, programista musiał- dyfikacji kodu zapisującego i modyfikują- jej metoda create() najpierw wywołuje
create() klasy nadrzędnej. Następnie

Listing 4. Implementacja klasy dopasowującej dekoratory do klas na podstawie sprawdza, czy powołany obiekt nie im-
nazw klas plementuje jednego z interfejsów klas
pomocniczych dekoratorów, aby unik-
class ClassNameIoCDecoratorSupport implements IoCDecoratorSupport {
nąć dekoracji tych ostatnich. Następ-
private $decoratorName;
nie kontener sprawdza przy pomocy
private $classNameToDecorator = array();
public function __construct($decoratorName,array $classNameToDecorator){ klas pomocniczych, czy nazwa świeżo
$this->decoratorName = $decoratorName; powołanej klasy pasuje do wzorca któ-
$this->classNameToDecorator = $classNameToDecorator; regoś z dekoratorów. Jeżeli tak, to kla-
}
sa pomocnicza zwraca nazwę deko-
public function match($className,$classObject) {
ratora. Każda klasa pomocnicza mu-
return (in_array($className,$this->classNameToDecorator))?true:false;
} si implementować interfejs IoCDecora-
cego$this->decoratorName;
public function getDecoratorName() { return dane. Dlatego skonstruujemy
} kon- torSupport, który z kolei składa się z
} tener, który na podstawie odpowiednie- dwóch metod:
go wzorca składającego się z nazwy kla-
Listing 5. Implementacja klasy dopasowującej dekoratory do klas na podstawie
sy oraz implementowanego przez nią in- l match() – zwraca true, jeżeli wzorzec
dowolnego wyrażenia regularnego
terfejsu będzie umiał udekorować tworzo- dekoratora pasuje do wzorca klasy,
class RegExpIoCDecoratorSupport implements ny obiekt.
IoCDecoratorSupport { l getDecoratorName() – zwraca nazwę
private $decoratorName; dekoratora z pliku konfiguracyjnego
private $pattern;
IoC.
public function __construct($decoratorName,$pattern){
$this->decoratorName = $decoratorName;
$this->pattern = $pattern; Klasy pomocnicze dostarczane są do
} kontenera poprzez metodę setDecora-
public function match($className,$classObject) { tors(). Następnie kontener IoC powo-
return (preg_match($this->pattern,get_class($classObject)))?true:false;
łuje do życia tenże dekorator i umiesz-
}
public function getDecoratorName() { return $this->decoratorName; } cza w nim dekorowany obiekt. Dekora-
} tor musi implementować interfejs IoC-
Decorator, który posiada jedną meto-
Listing 6. Implementacja klasy dopasowującej dekoratory do klas na podstawie dę – setObject(). Zauważmy, że w ten
interfejsu
sposób możemy każdą z klas udekoro-
class InterfaceIoCDecoratorSupport wać wieloma dekoratorami. Warto jesz-
implements IoCDecoratorSupport { cze zapamiętać, że zgodnie z imple-
private $decoratorName; mentacją naszego kontenera, dekora-
private $interfaceName;
tor nie może być singletonem. To zna-
public function __construct(
$decoratorName,$interfaceName){
czy: dla każdej dekorowanej klasy musi
$this->decoratorName= być powoływany do życia nowy deko-
$decoratorName; rator. Na Listingu 3 przedstawiamy kod
$this->interfaceName= nowej wersji kontenera, a na Listingach
$interfaceName;
4, 5 i 6 – kilka przykładowych klas po-
}
public function match($className,
mocniczych implementujących interfejs
$classObject) { IoCDecoratorSupport. Pierwsza klasa
return ($classObject instanceof odnajduje klasy według pełnej nazwy,
$this->interfaceName)? druga wg wyrażenia regularnego, a
true:false;
trzecia wg konkretnego interfejsu, któ-
}
public function
ry implementuje dekorowana klasa.
getDecoratorName(){ Wróćmy teraz do naszego przykładu.
return $this->decoratorName; Załóżmy dla uproszczenia, że aplikacja,
} w której trzeba dokonać zmian, ma trzy
}
klasy modyfikujące dane: UserDAO, Order-
DAO oraz ItemDAO. Wszystkie one imple-

PHP Solutions Nr 6/2006 www.phpsolmag.org 47


Dla zaawansowanych Crosscutting concerns

mentują jeden wspólny interfejs DAO za- dzie. Na Listingu 7 prezentujemy przykła- tuje dwa interfejsy: IoCDecorator i DAO (Li-
wierający metody setConnection(), find- dową konfigurację kontenera bez dekora- sting 8). Oczywiście, jej docelowa imple-
ById(), save(), update(), delete(). Me- torów. Jak widzimy, składa się ona z czte- mentacja powinna zawierać bardziej wy-
toda setConnection() ustawia połącze- rech wpisów, z których jeden dotyczy po- rafinowany kod logujący. Teraz wykorzy-
nie do źródła danych, findById() zwraca łączenia z bazą danych, a pozostałe trzy stując klasę z Listingu 6 modyfikujemy
obiekt na podstawie jego id, save() za- zawierają ustawienia klas DAO. konfigurację kontenera. Nową konfigura-
pisuje obiekt, update() go zmienia, a de- Zmodyfikujemy teraz przykład tak, cję pokazujemy na Listingu 9. Zauważmy,
lete() kasuje. Dane są modyfikowane je- aby spełniał on wymagania biznesowe że dodaliśmy tylko dwa wpisy i zgodnie
dynie przez metody save(), update() i de- naszego klienta. Nic prostszego: zada- z założeniami, kod klas UserDAO, Order-
lete(). Szczegóły implementacji klas *DAO nie rozpoczynamy od utworzenia klasy DAO oraz ItemDAO nie został zmieniony. W
nie mają znaczenia w naszym przykła- DAOLoggerDecorator. Klasa ta implemen- przykładzie pominęliśmy problem przeka-

Listing 7. Plik konfiguracji kontenera IoC dla opisanego w Listing 8. Klasa DAOLoggerDecorator jest dekoratorem
tekście przykładu bez wsparcia dekoratorów klas UserDAO, OrderDAO i ItemDAO

$currentDir = dirname(__FILE__); class DAOLoggerDecorator implements IoCDecorator, DAO {


$frameworkPath = realpath( $currentDir.'/../../flexi' );
ini_set( 'include_path', ini_get('include_path'). private $dao;
PATH_SEPARATOR.$frameworkPath.'/' );
public function findById($id){
require_once 'ioc/IoCContainerWithDecoratorSupport. // Kod dekoratora
class.php'; $result = $this->dao->findById($id);
require_once 'ioc/MappingBuilderFromArray. // Kod dekoratora
class.php'; ...
$iocMap = array( return $result;
"connection" => array( }
"className" => "Connection",
"file" => $currentDir."/Connection.class.php", public function save($object){
"singleton" => true, // Kod dekoratora
"properties" => array(), $result = $this->dao->save($object);
"constructorParams" => array() // Kod dekoratora
), return $result;
"userDAO" => array( }
"className" => "UserDAO",
"file" => $currentDir."/UserDAO.class.php", public function update($object){
"singleton" => true, // Kod dekoratora
"properties"=>array( $result = $this->dao->update($object);
"connection"=>"&connection"), // Kod dekoratora
"constructorParams" => array() ...
), return $result;
"orderDAO" => array( }
"className" => "OrderDAO",
"file" => $currentDir."/OrderDAO.class.php", public function delete($object){
"singleton" => true, // Kod dekoratora
"properties" => array( $result = $this->dao->delete($object);
"connection"=>"&connection"), // Kod dekoratora
"constructorParams" => array() ...
), return $result;
"itemDAO" => array( }
"className" => "ItemDAO",
"file" => $currentDir."/ItemDAO.class.php", public function setConnection($connection){
"singleton" => true, // Kod dekoratora
"properties" => array( $result = $this->dao->
"connection"=>"&connection"), setConnection($connection);
"constructorParams" => array() // Kod dekoratora
), return $result;
); }
$mappingBuilder = new
MappingBuilderFromArray($iocMap); public function setObject($object){
$iocContainer=new IoCContainerWithDecoratorSupport( if ( !$object instanceof DAO ){
$mappingBuilder->getApplicationMap()); throw new Exception('Class '.get_class($object).
$userDAO = $iocContainer->create("userDAO"); ' must implement DAO interface');
$orderDAO = $iocContainer->create("orderDAO"); }
$itemDAO = $iocContainer->create("itemDAO"); $this->dao = $object;
var_dump($userDAO); }
var_dump($orderDAO);
var_dump($itemDAO); }

48 www.phpsolmag.org PHP Solutions Nr 6/2006


Crosscutting concerns Dla zaawansowanych

zywania informacji o tym, kto modyfikuje


Listing 9. Nowa wersja pliku konfiguracyjnego kontenera dane. Gdyby kod (zupełnie inny niż w na-
$currentDir = dirname(__FILE__);
szym przykładzie) nie miał tak jasno zde-
$frameworkPath = realpath( $currentDir.'/../../flexi' ); finiowanego interfejsu, jak nasze klasy
DAO, do tworzenia dekoratorów mogliby-
ini_set( 'include_path', ini_get('include_path').PATH_SEPARATOR. śmy wykorzystać metodę _ _ call(), któ-
$frameworkPath.'/' );
rej działanie zostało opisane w dokumen-
require_once 'ioc/IoCContainerWithDecoratorSupport.class.php';
tacji PHP.
require_once 'ioc/MappingBuilderFromArray.class.php'; W podobny sposób, jak reguły
$iocMap = array( związane z logowaniem, moglibyśmy
dodać do aplikacji kod odnoszący się
"connection" => array(
do reguł bezpieczeństwa i praw dostę-
"className" => "Connection",
"file" => $currentDir."/Connection.class.php",
pu użytkowników. Wyobraźmy sobie,
"singleton" => true, że klient definiuje nowy wymóg: chce,
"properties" => array(), aby modyfikacji danych mogli dokony-
"constructorParams" => array() wać tylko administratorzy aplikacji. Mo-
),
żemy tu zastosować podobne podej-
"userDAO" => array(
"className" => "UserDAO",
ście, jak przy logowaniu. Utworzymy
"file" => $currentDir."/UserDAO.class.php", więc klasę DAOSecutiryDecorator, któ-
"singleton" => true, ra przy wywoływaniu każdej metody
"properties" => array("connection"=>"&connection"), save(), update() czy delete() będzie
"constructorParams" => array()
sprawdzała, czy wykonujący ją użyt-
),
"orderDAO" => array(
kownik ma prawa do jej użycia: jeżeli
"className" => "OrderDAO", nie, aplikacja zgłosi wyjątek.
"file" => $currentDir."/OrderDAO.class.php",
"singleton" => true, Podsumowanie
"properties" => array("connection"=>"&connection"),
Stosowanie kontenera IoC i wzorca De-
"constructorParams" => array()
),
korator nie jest remedium na wszel-
"itemDAO" => array( kie problemy przekrojowe. Aby wpro-
"className" => "ItemDAO", wadzenie do kodu rzeczywistej aplikacji
"file" => $currentDir."/ItemDAO.class.php", aspektów logowania i bezpieczeństwa
"singleton" => true,
było możliwe od samego początku, apli-
"properties" => array("connection"=>"&connection"),
"constructorParams" => array()
kacja ta musi mieć jasny podział na war-
), stwy. Należy również ją tworzyć zgodnie
"interfaceIoCDecoratorSupport" => array( z dobrymi praktykami programowania
"className" => "InterfaceIoCDecoratorSupport", obiektowego, w szczególności kładąc
"file" => $frameworkPath."/ioc/decorators/InterfaceIoCDecoratorSupport.
nacisk na interfejsy i kompozycję obiek-
class.php",
"singleton" => true,
tów. Warto też pamiętać, że reguły bez-
"properties" => array(), pieczeństwa i logowania nie są jedynymi
"constructorParams" => array("daoDecorator","DAO") problemami przekrojowymi: zaliczają się
), do nich również transakcje, profilowanie,
"daoDecorator" => array(
keszowanie (caching) czy walidacja. Na
"className" => "DAOLoggerDecorator",
"file" => $currentDir."/DAOLoggerDecorator.class.php",
szczęście i te problemy można rozwią-
"singleton" => false, zać przy pomocy kontenera IoC i odpo-
"properties" => array(), wiednich dekoratorów. n
"constructorParams" => array()
),
);

$mappingBuilder = new MappingBuilderFromArray( $iocMap ); O autorze


$iocContainer = new IoCContainerWithDecoratorSupport(
$mappingBuilder->getApplicationMap() ); Piotr Szarwas ma wieloletnie doświad-
$iocContainer->setDecoratorsSupport( array( czenie w programowaniu i tworzeniu apli-
$iocContainer->create("interfaceIoCDecoratorSupport"))); kacji WWW (PHP, Java). Jest konsultan-
$userDAO = $iocContainer->create("userDAO"); tem w jednej z największych polskich firm
$orderDAO = $iocContainer->create("orderDAO"); IT, a także doktorantem na Wydziale Fizy-
$itemDAO = $iocContainer->create("itemDAO"); ki Politechniki Warszawskiej. Od dawna
var_dump($userDAO); pisze artykuły dla PHP Solutions.
var_dump($orderDAO);
var_dump($itemDAO); Kontakt z autorem:
piotr.szarwas@gmail.com

PHP Solutions Nr 6/2006 www.phpsolmag.org 49


Dla zaawansowanych

Przyjazne URL-e w PHP,


czyli zaprzęgamy
mod_rewrite do pracy
Stopień trudności: lll
Michał Gacki

Wszyscy lubimy przejrzyste i proste adresy


stron WWW. Niestety, logika aplikacji PHP
bywa dość skomplikowana – parę argumentów
przekazywanych w URL-u wystarcza, aby
skutecznie utrudnić życie użytkowników
naszej witryny i obniżyć jej atrakcyjność
dla wyszukiwarek. Problem ten rozwiążemy
używając modułu Mod_Rewrite: dzięki niemu
możemy zamienić nawet największą plątaninę
linków i parametrów na czytelne i przyjazne
adresy WWW.

M
od_Rewrite to moduł Apache'a, Mod_Rewrite. Ponieważ jednak coraz
który jest domyślnie zainstalowa- więcej użytkowników oczekuje możliwo-
ny na serwerze (choć nie zawsze ści korzystania z niego, jest on powoli
włączony w konfiguracji). Jego funkcją jest wprowadzany jako standard. Na początek
przepisywanie (ang. rewriting) URL-i, czy- musimy się dowiedzieć, czy nasz serwer
li możliwość prezentowania plików i katalo- udostępnia ten moduł – możemy zapytać
gów umieszczonych na witrynie przy użyciu o to jego administratora albo sprawdzić to
innych nazw i ścieżek niż w rzeczywistości. na własną rękę. Aby wykonać to drugie,
W SIECI Pozwala nam to m.in. na dynamiczne prze- przeprowadzimy prosty test: utworzymy
pisywanie linków, przekazywanie do skryp- plik test.php, w którym umieścimy nastę-
tów PHP dodatkowych zmiennych, które nie pujący kod, po czym wykonamy go na
l http://httpd.apache.org zostały podane w zewnętrznym URL-u (me- serwerze:
– strona główna Apache'a todą GET) ani przekazane metodą POST
l http://httpd.apache.org/docs/
czy blokowanie zewnętrznych linków (zwa-
1.3/mod/mod_rewrite.html Co powinieneś
– oficjalna dokumentacja nych też HotLinkami) do wybranych treści.
Mod_Rewrite
Działanie Mod_Rewrite opiera się na sto- wiedzieć...
l http://phpnuke.org – strona Potrzebna będzie średniozaawansowana
domowa projektu PHP-Nuke sowaniu reguł opisujących sposób przepisy- wiedza na temat konfiguracji i korzysta-
http://www.postnuke.com/
wania URL-i, które umieszczamy domyślnie
l

– oficjalna strona CMS-a Po-


nia z serwera Apache.
stNuke w pliku .htaccess. Co obiecujemy...
l http://perldoc.perl.org/ Pokażemy, jak stosując Mod_Rewrite
perlre.html – oficjalne strony
Perla o wyrażeniach regular- Czy możemy korzystać m.in. osiągnąć przejrzyste linki i za-
nych z Mod_Rewrite? bezpieczyć dostęp do plików. Przybli-
żymy również podstawy wyrażeń
l http://www.regular-expres-
sions.info/ – witryna o wyra-
Niestety, obecnie większość serwe- regularnych.
żeniach regularnych rów hostingowych nie wspiera modułu

50 www.phpsolmag.org PHP Solutions Nr 6/2006


mod_rewrite Dla zaawansowanych

przejść do bardziej zaawansowanych przy-


kładów jego użycia. Poprzednio sprawdza-
liśmy, czy zmienna przekazana do skryp-
tu przy użyciu Mod_Rewrite istnieje: te-
raz sprawdzimy również jej wartość (true
lub false). W tym celu stworzymy plik in-
dex.php, którego kod przedstawiamy na Li-
Rysunek 1. Moduły serwera Apache w środowisku PHP stingu 3. Pamiętajmy o register_globals!
<?php utworzymy plik o nazwie np. test.htaccess Potrzebny nam też będzie plik .htaccess o
phpinfo(INFO_MODULES); i dopiero na serwerze zmienimy jego na- następującej zawartości:
?> zwę na .htaccess. Gdy wgramy na serwer
plik .htaccess z komendami Mod_Rewri- RewriteEngine On
W tym skrypcie wywołujemy funkcję php_ te, a sam moduł nie będzie działał, wpisu- RewriteRule ^index\.php$
info(), która podaje parametry dotyczące jąc adres jakiegokolwiek pliku istniejącego index.php?menu=true [L]
środowiska PHP. Użycie parametru INFO_ w tym samym katalogu, co .htaccess, uj-
MODULES oznacza, że wyświetlone mają rzymy informację o błędzie 404 (nie znale- Działanie tego zestawu index.php-.htac-
zostać wyłącznie informacje o zainstalo- ziono pliku).Inne funkcje katalogu nie bę- cess jest następujące: po wpisaniu na-
wanych modułach (zob. Rysunek 1). dą działać lub zobaczymy błąd 500 (In- zwy index.php w przeglądarce WWW (al-
Jeżeli na liście widnieje nazwa mod_ ternal Server Error). Ten drugi zobaczymy bo przejściu do katalogu, w którym on się
rewrite, którą zaznaczyliśmy na Rysunku 1, zwłaszcza wtedy, gdy serwer zinterpretuje znajduje, jeżeli serwer automatycznie roz-
to mamy pewność, że Mod_Rewrite jest za- RewriteRule jako błąd składni .htaccess. poznaje indeksy), Mod_Rewrite przepi-
instalowany na naszym serwerze. Może się Jeżeli nie korzystamy z serwera hostin- sze index.php na index.php?menu=true,
jednak zdarzyć, że funkcja nie zwróci infor- gowego, tylko mamy własny, a przepisywa- co spowoduje przesłanie zmiennej $menu
macji o modułach (zależy to od konfiguracji nie linków nie działa, to musimy zmodyfiko- do parsera PHP. Jeżeli się to uda, ujrzymy
serwera) – aby sprawdzić obecność Mod_ wać główny plik konfiguracyjny Apache'a tekst informujący nas, że zmienna $menu
Rewrite, użyjemy wtedy skryptu przedsta- noszący nazwę httpd.conf. W tym celu istnieje. Wartość zmiennej $menu2 zosta-
wionego na Listingu 1, który również umie- otworzymy go w dowolnym edytorze nie też zmieniona na true (zauważmy,
ścimy na serwerze pod nazwą test.php. Na- tekstu, odszukamy linie LoadModule że na początku skryptu jest ona ustawio-
stępnie utworzymy plik .htaccess, którego rewrite_module modules/mod_rewrite.so, na na false, aby nie można jej było zmie-
zawartość pokazujemy na Listingu 2. Dzia- ClearModuleList oraz AddModule mod_ nić metodą GET) i zobaczymy resztę tek-
łanie tego tandemu jest proste: w pliku .htac- rewrite.c i usuniemy znaki komentarza stu. Ta część jest prosta, przejdźmy więc
cess definiujemy przykładową regułę Mod_ (#), od których się rozpoczynają. Należy do omówienia pliku .htaccess. Po pierw-
Rewrite, która przepisuje wszystkie odwoła- też poszukać w tym pliku dyrektywy sze, aby móc tworzyć reguły Mod_Rew-
nia do pliku test.php jako test.php?tester=1, AccessFileName, która określa domyślną rite, musimy najpierw umieścić instruk-
przez co do skryptu test.php przekazywana nazwę pliku mogącego przechowywać cję RewriteEngine On, która – jak sama
jest zmienna tester ($tester) z przypisaną ustawienia w katalogach. Standardowo jest nazwa wskazuje – włącza przepisywa-
wartością 1, co jest następnie sprawdzane. nią .htaccess, jeśli jednak jest inaczej, to nie. Następnie, korzystając z polecenia
Jeśli nasz skrypt zwróci informację o braku wpisujemy .htaccess. Teraz wystarczy zre- RewriteRule definiujemy poszczególne
Mod_Rewrite, możemy napisać do admini- startować Apache'a i ponownie przeprowa- zasady. Jego składnia jest następująca:
stratora z prośbą o jego instalację (czy choć- dzić testy: przepisywanie powinno działać.
by uruchomienie lokalne). RewriteRule ^adres_źródłowy$ adres
Pamiętajmy jeszcze o jednym: spraw- Mod_Rewrite: przepisany [FLAGI]
dzając obecność zmiennej $tester przy zaczynamy
użyciu isset($tester) zakładamy, że w Jeżeli jesteśmy szczęśliwymi posiadacza- Pierwszy adres, czyli adres źródłowy, który
ustawieniach parsera PHP włączone są mi modułu Mod_Rewrite, możemy teraz zawsze rozpoczynamy znakiem ^, a koń-
register_globals. Tymczasem, na wielu
serwerach są one wyłączane ze względów Listing 1. Sprawdzamy, czy Mod_Rewrite został zainstalowany na serwerze
bezpieczeństwa. Lepiej jest więc użyć kon-
strukcji isset($_GET['tester']) w PHP5 <?php
lub isset($HTTP_GET_VARS['tester']) w if (isset($sprawdzacz)) {
?> Mod_Rewrite jest zainstalowany na tym serwerze <?
PHP4. Kolejna uwaga dotyczy tworzenia
}
pliku .htaccess w edytorze działającym pod else {
systemem Windows w celu późniejsze- ?> Mod_Rewrite nie jest zainstalowany na tym serwerze lub jest błędnie
go przegrania go na serwer (np. za pomo- skonfigurowany <?
cą FTP). Pod tym systemem nie możemy }
?>
utworzyć pliku, którego nazwa zaczyna się
od kropki, co musimy obejść. W tym celu

PHP Solutions Nr 6/2006 www.phpsolmag.org 51


Dla zaawansowanych mod_rewrite

czymy znakiem dolara ($), to oryginalna dając tę flagę do reguły możemy być z plików (a nie z bazy danych), nie musi-
ścieżka pliku, którą chcemy przepisać ja- pewni, że jeżeli ta ostatnia nie zosta- my modyfikować za każdym razem tych
ko adres_przepisany. Pamiętajmy, że jeśli nie wykonana, serwer spróbuje użyć ostatnich - wystarczy raz umieścić odpo-
w którymkolwiek adresie używamy znaków reguły znajdującej się o linijkę niżej, wiedni warunek w skrypcie PHP i wgrać
specjalnych: dolara ($), kropki (.), dasz- Ÿ L – skrót od angielskiego słowa last plik .htaccess na serwer. Po zakończeniu
ka (^), gwiazdki (*), plusa (+), pytajnika (?), (ostatni). Wstawienie tej flagi powo- promocji wystarczy usunąć .htaccess
backslasha (\) czy nawiasów klamrowych, duje zatrzymanie sprawdzania kolej- albo zmienić jego nazwę, np. na promo-
okrągłych lub kwadratowych, to musimy je nych reguł w .htaccess, jeżeli reguła cja.htaccess. Przy takich rozwiązaniach
poprzedzać znakiem backslasha (\). Wyni- ją zawierająca jest poprawna i zosta- należy jednak pamiętać, że jeżeli ktoś wie,
ka to stąd, że obie ścieżki są rozpoznawa- nie użyta. od jakiej zmiennej zależy warunek, może
ne przy użyciu wyrażeń regularnych (ang. Ÿ R - skrót od redirect (po ang. prze- ją dopisać do adresu URL i tym sposobem
regular expressions), w których te zna- kierowanie). Dodanie tej flagi powo- obejrzeć ukrytą część strony!
ki pełnią określone funkcje; poprzedzanie duje przekierowanie adresu na adres Aby sprecyzować zakres działania na-
tych znaków backslashem nazywamy na- przepisany. Gdybyśmy wstawili tę szego pliku .htaccess, możemy pod linijką
tomiast ich eskejpowaniem lub uwalnia- flagę w naszym drugim przykładzie, zawierającą komendę RewriteEngine On
niem (ang. character escaping), które po- w pasku adresowym przeglądarki wstawić polecenie RewriteBase, które
woduje, że każdy znak specjalny znajdują- dopisany zostałby ciąg ?menu=true, określa relatywną ścieżkę do aktualnego
cy się bezpośrednio po tym backslashu jest co w tej sytuacji nie byłoby pożądane. katalogu. Jej składnia jest następująca:
rozpoznawany jako zwykły znak. Stosując flagę R możemy też wska-
Kolejna sprawa to tzw. flagi, które zać typ przekierowania, np. słynne RewriteBase ścieżka
umieszczamy na końcu reguł. Nie użyli- przekierowanie 301 (Redirect 301)
śmy ich w dotychczasowych przykładach, zapiszemy jako R=301. Jeśli chcemy, aby nasze reguły miały
ponieważ przydają się one wtedy, gdy zasięg globalny, nie musimy wstawiać
korzystamy z większej ilości reguł. Najczę- Możemy dodać kilka flag jednocześnie RewriteBase do pliku .htaccess.
ściej używane flagi to: – wystarczy je wymienić po przecinku
między nawiasami kwadratowymi na koń- Migracja domen przy
Ÿ NC – dzięki tej fladze reguła zadziała cu reguły. Przykład: [L,NC,OR]. użyciu Mod_Rewrite
bez względu na wielkość liter, jakimi W naszym przypadku ścieżka Często się zdarza, że zmieniamy dome-
został napisany URL. Gdybyśmy index.php zostanie przepisana na in- nę naszego serwisu i chcemy zachować
w opisanym przykładzie dodali fla- dex.php?menu=true. Rozwiązanie prze- starą. Przykładowo, możemy przenosić
gę NC, moglibyśmy wpisać adres kazywania zmiennych jest przydatne, ją z darmowej na płatną (np. .com), na-
w dowolny sposób, np. INDEX.PHP, gdy chcemy bez modyfikacji plików PHP tomiast likwidacja starej domeny ozna-
inDeX.pHp, IndeX.php, itd., przesłać dodatkowe wartości. Przykłado- czałaby dla nas utratę tysięcy linków do
Ÿ OR – oznacza to samo, co słowo or wo, jeśli chcemy dodać promocję w na- nas zamieszczonych na innych witrynach
w języku angielskim, czyli lub. Do- szym sklepie internetowym, który korzysta oraz pozycji w wyszukiwarkach, które
zindeksowały wiele spośród naszych
Listing 2. Plik .htaccess, którego używamy wraz z test.php, aby sprawdzić podstron ze starym adresem i związane
obecność Mod_Rewrite z nimi słowa kluczowe. Na pierwszy rzut
oka, rozwiązanie tego problemu migracji
Options +FollowSymLinks wydaje się proste – wystarczy podpiąć
<IfModule mod_rewrite.c>
dwie domeny pod to samo konto. Wiążą
RewriteEngine On
RewriteRule ^(test.php)$ test.php?tester=1 [QSA] się z tym jednak kolejne trudności, np.
RewriteRule ^$ test.php?tester=1 [QSA] wyszukiwarka Google widzi takie domeny
</IfModule> jako tzw. double content, czyli dwie witryny
z identyczną treścią. Tu również pomoże
Listing 3. Sprawdzamy, jaką wartość ma zmienna $menu
nam Mod_Rewrite: w katalogu głównym
<? starej domeny umieścimy plik .htaccess
$menu2 = false; o następującej zawartości:
if (isset($menu) && $menu == true) {
?> <br /><b>Zmienna $menu istnieje, choć nie została podana w URL-u</b> <?
RewriteEngine On
$menu2 = true;
RewriteCond %{HTTP_HOST} staradomena.com
}
?> RewriteRule ^(.*)$
<br /> http://www.nowadomena.com/$1 [R=301,L]
<? if ($menu2 == true) { ?>
Dzięki poprzedniej zmiennej wartość zmiennej $menu2 została zmieniona na
Jak widać, w kodzie pojawia się nowe po-
true, dlatego ten tekst jest widoczny.
<?
lecenie RewriteCond. Określa ono zakres
} wykonywania dalszych reguł. W tym przy-
?> padku ustala warunek, że reguły będą in-
terpretowane tylko wtedy, gdy wejście na-

52 www.phpsolmag.org PHP Solutions Nr 6/2006


Dla zaawansowanych mod_rewrite

stąpi z adresu staradomena.com. Składnia Aby się bronić przed tym zjawiskiem, cych: następne reguły (które przekierowują
komendy RewriteCond jest następująca: należy zastosować blokadę HotLinków. wszystkie HotLinki na stronę informującą,
Dotychczas opracowano i wdrożono wiele że jest to zabronione) będą wykonywane
RewriteCond %{WARUNEK} adres tego rodzaju blokad działających na pozio- tylko wtedy, jeżeli sprawdzany przez HTTP_
mie PHP. Jednymi z najskuteczniejszych REFERER adres serwera polecającego nie
Jedyna zdefiniowana przez nas tym razem były te, które sprawdzały serwer pole- jest naszym adresem. Problem mogący
reguła przepisuje wszystkie stare adresy cający (tzn. serwer, z którego nastąpiło się pojawiać przy użyciu przedrostka www
na http://www.nowadomena.pl/[adres]. kliknięcie na link) i porównywały jego adres rozwiązuje zastosowanie wyrażenia regu-
Do rozpoznawania wszystkich znaków z adresem witryny, której właściciel umie- larnego (.+\.)?, dzięki któremu nieważne
służy wyrażenie regularne (.*) – kropka ścił swoje pliki oraz używały przepisywania jest, czy w adresie użyto tego prefiksu, czy
symbolizuje dowolny znak, a * – dowolną linków w celu ukrycia plików do pobrania, nie. Zauważmy, że przed naszym adresem
ilość znaków. zarazem uruchamiając skrypt. Niestety, (strona.com) umieściliśmy znak wykrzykni-
Użyte przez nas przekierowanie przy blokady te miały jedną wadę – jeżeli ktoś ka: w języku wyrażeń regularnych oznacza
użyciu RewriteRule i RewriteCond jest znał prawdziwe ścieżki do plików, skrypt on przeczenie.
znacznie lepsze od tradycyjnego się nie uruchamiał i plik można było spo- Umieszczona w następnej linijce re-
Redirect301 / http://www.nowadomena.com, kojnie pobrać. guła sprawdza po rozszerzeniu pliku, czy
ponieważ to drugie przekierowuje użytkow- Tu również z pomocą przychodzi wolno do niego linkować. Jeżeli rozsze-
nika od razu na nową domenę niezależnie Mod_Rewrite, pozwalając na udoskonale- rzenie to (opisane wyrażeniem regularnym
od tego, czy wpisał adres starej, czy no- nie opisanej metody. Wystarczy stworzyć .*\., czyli dowolny_znak.znaki_po_kropce;
wej. Co więcej, to stosując przytoczoną plik .htaccess, który następnie umiesz- zwróćmy uwagę na eskejpowanie znaku
instrukcję Redirect301 (którą też umiesz- czamy w katalogu przeznaczonym do kropki) należy do grupy zabronionych (u
czamy w pliku .htaccess) można łatwo zabezpieczenia: nas zip, exe, rar, gz, tar.gz i tar), to link
zapętlić serwer. Umieszczenie instrukcji do pliku nie zadziała: użytkownik, który na
Redirect301 przekierowującej ze starego RewriteEngine On niego kliknie, zostanie przekierowany na
adresu na serwerze, pod który podpięte są RewriteCond %{HTTP_REFERER} !^http:// stronę HotLink.html.
dwie domeny (staradomena.com i nowado- (.+\.)?strona\.com [NC] Opisane rozwiązanie jest bardzo przy-
mena.com), grozi tym, że serwer będzie RewriteRule ^.*\.(zip|exe|rar|gz| datne, ale ma jedną wadę: gdy wpiszemy
nas przekierowywał w nieskończoność, tar.gz|tar)$ http://www.strona.com/ adres pliku w przeglądarce (otrzymany
gdyż nie będzie wiedział, która domena HotLink.html [L] np. w mailu czy komunikatorze), blokada
jest stara, a która nowa – obie będą czytały HotLink zadziała i nie będziemy mogli
ten sam plik .htaccess. Dzięki zastosowa- Dzięki poznanej wcześniej komendzie pobrać pliku. A przecież ideą blokowania
niu przedstawionego zestawu składają- RewriteCond i wyrażeniom regularnym HotLinków nie jest utrudnianie życia zwy-
cego się z przekierowania warunkowego możemy w prosty sposób ustalić warunek kłym użytkownikom, tylko właścicielom
RewriteCond i reguły RewriteRule tego wykonywania reguł dla serwerów polecają- serwisów obciążających nasze łącza! Aby
unikniemy: serwer nie będzie miał proble-
mu z ustaleniem, która domena jest nowa,
a która stara, gdyż przekierowanie podziała
Wyrażenia regularne
W regułach RewriteRule stosowaliśmy wyrażenia regularne (ang. regular expressions).
tylko, jeżeli wejdziemy ze starej. Mówiąc najprościej, są to wzorce w postaci łańcuchów symboli, których używamy w ce-
lu zaawansowanego wyszukiwania wybranych fraz w tekście (w przypadku RewriteRule
Skuteczna przeszukiwane są nazwy plików i katalogów). Wyrażenia te odnajdują tekst pasujący do
blokada HotLinków wzorca. Ze względu na elastyczność, wyrażenia regularne stanowią wręcz standard wśród
metod przeszukiwania tekstu. Warto wiedzieć, że dzielą się na składnię uniksową i perlo-
Chyba każdy, kto zamieszcza jakiekolwiek wą (znaną jako PCRE czyli Perl Compatible Regular Expressions i stosowaną nie tylko w
pliki do pobrania na stronie WWW, wie, co Perlu, ale także w PHP, Ruby i wielu innych językach, które korzystają z utworzonej w C
znaczy termin HotLink: oznacza on podpi- biblioteki PCRE). Bez znajomości choćby podstaw wyrażeń regularnych ciężko mówić o
korzystaniu z Mod_Rewrite. Oto kilka znaczników używanych w wyrażeniach regularnych,
nanie się pod wybrane pliki (np. MP3 czy
które ułatwią nam dalszą pracę z tym modułem:
PNG) z innego serwisu. W praktyce wy-
gląda to tak, że webmaster kupuje serwer, Ÿ kropka (.) – dowolny znak,
na którym umieszcza pliki, które z kolei są Ÿ tekst1|tekst2 – alternatywa: tekst1 lub tekst2,
Ÿ daszek (^) – rozpoczyna daną strefę znaków,
przeznaczone do pobrania z jego strony,
Ÿ dolar ($) – kończy daną strefę znaków,
a osoba prowadząca inny serwis po prostu Ÿ pytajnik (?) – zero lub jeden znak poprzedzający wyrażenie,
podpina te same linki u siebie na stronie, Ÿ gwiazdka (*) – zero lub więcej znaków poprzedzających wyrażenie,
czego następstwem jest wzrost transferu Ÿ plus (+) – jeden lub więcej znaków poprzedzających wyrażenie,
Ÿ (tekst _ w _ nawiasach) – grupuje tekst tekst _ w _ nawiasach.
na koncie webmastera, który wykupił ser-
wer. Powoduje to zwiększenie obciążenia Aby podstawić w adresie docelowym znalezione nazwy, które pasują do wyrażenia uży-
łączy prowadzących do tego serwera; co tego w regule wobec adresu źródłowego, używamy oznaczającego rezultat wyszukiwa-
więcej, niektóre firmy hostingowe ustalają nia dolara ($), bezpośrednio po którym stawiamy numer wyrażenia (licząc od pierwsze-
go pozwalającego na wstawienie tekstu), czyli np. $1, $2 i $3. Numery rezultatów szuka-
górne limity miesięczne transferu z konta, nia są przyporządkowane kolejnym wyrażeniom (zawartym w nawiasach), np. wyrażeniu
po przekroczeniu których strona jest za- ^([^-]+)/([^-]+)/([^-]+)$ zostaną przypisane $1, $2 i $3.
wieszana.

54 www.phpsolmag.org PHP Solutions Nr 6/2006


mod_rewrite Dla zaawansowanych

Tabela 1. Przepisywanie adresów zaczynających się od słowa gallery index.php?module=gallery&function=


Adres Adres prawdziwy photo&id=35&PHPSESSID=accfcc
299077b36817dc534c90588253
gallery/wartość2/wartość3 index.php?module=gallery&function=warto-
lub gallery/wartość2/wartość3/ ść2&id=wartość3
Dzięki Mod_Rewrite możemy uczynić ad-
gallery/wartość2 lub gallery/wartość2/ index.php?module=gallery&function=wartość2
res naszej strony bardziej atrakcyjnym dla
gallery lub gallery/ index.php?module=gallery
wyszukiwarek (i tych użytkowników, któ-
Tabela 2. Przepisywanie adresów zaczynających się od słowa news rzy lubią zapisywać bądź zapamiętywać
Adres Adres prawdziwy URL-e) oraz usunąć doklejanie identyfika-
tora sesji do linków.
news/wartość2/wartosć3 index.php?module=news&action=warto-
lub news/wartość2/wartość3/ ść2&id=wartość3 Zacznijmy od tego, że nie ma gotowe-
go kodu, który będzie działał na wszyst-
news/wartość2 lub news/wartość2/ index.php?module=news&action=wartość2
kich stronach; posłużymy się więc praktycz-
news lub news/ index.php?module=news nym przykładem testowym, który trzeba bę-
Tabela 3. Przepisywanie adresów zaczynających się od słowa, którym nie jest ani dzie przystosować do każdej strony. Załóż-
gallery, ani news my zatem, że chcemy zmienić zwykłe linki
Adres Adres prawdziwy index.php?module=wartość1&function=war-
wartość1/wartość2/wartość3 index.php?module=wartość1&option=warto- tość2&id=wartość3 na przepisane adresy
lub wartość1/wartość2/wartość3/ ść2&id=wartość3 w stylu katalogowym (wartość1/wartość2/
wartość1/wartość2 index.php?module=wartość1&option=war- wartość3). Przykładowo, mając galerię zdjęć
lub wartość1/wartość2/ tość2 chcemy, żeby wpisując adres typu http:
wartość1 lub wartość1/ index.php?option=wartość1 //www.mojadomena.com/gallery/photo/21,
użytkownik był kierowany na stronę http:
zaradzić temu problemowi, możemy prze- resy typu gallery-photo-1.html, które tak //www.mojadomena.pl/index.php?module=
robić kod na następujący: naprawdę maskują właściwe URL-e (np. gallery&function=photo&id=21 nawet o tym
index.php?module=gallery&function=pho- nie wiedząc (w przeglądarce będzie cały
RewriteEngine On to&id=351). Jak wiadomo, w dzisiejszych czas widniał przyjazny URL). Nie chcemy i
RewriteCond %{HTTP_REFERER} !^http:// czasach dla sukcesu strony bardzo waż- nie będziemy przy tym tworzyć katalogów o
(.+\.)?strona\.com [NC] na jest pozycja w wyszukiwarkach typu nazwach gallery, photo czy 21. Mod_Rewri-
RewriteRule^ .*\.(zip|exe|rar|gz|tar.gz Google. Link zaczynający się od in- te pozwala zarówno na udawanie nazw pli-
|tar)$ dex.php z ustawieniem wielu zmiennych ków, jak i katalogów – stwórzmy plik .htac-
http://www.strona.com/index.php?page=down nie jest przyjazny ani dla użytkownika, ani cess o następującej zawartości:
load&file=$1 [L] dla wyszukiwarki, ponieważ zawiera zna-
ki, których te ostatnie nie lubią, a miano- RewriteEngine On
Spowoduje on przekierowanie użytkow- wicie pytajnik i ampersand (&). Powoduje RewriteRule ^([^-]+)/([^-]+)/([^-]+)$
nika pod adres: index.php?page=downlo- to, że strony zawierające takie adresy index.php?module=$1&function=$2&id=
ad&file=nazwa_pliku pod którym zamiesz- URL nie są w ogóle indeksowane lub są $3 [L,NC,NS]
czamy skrypt PHP służący do ściągania indeksowane bardzo powoli. Co więcej,
plików (nie podajemy go w artykule, gdyż systemy CMS (ang. Content Manage- W powyższej regule użyliśmy trzech flag,
może być to dowolny skrypt tego rodzaju ment Systems, czyli systemy zarządzania oznaczających m.in. to, że wielkość li-
dostępny w Internecie albo napisany przez treścią) często korzystają z przekazywa- ter w adresach nie będzie miała znacze-
nas). Użytkownik zobaczy komunikat nia identyfikatora sesji w adresach URL nia (flaga NC), a przepisywanie nie zakoń-
zawierający link do pliku i będzie musiał (przesyłania tego ID przez cookies wy- czy się na tej jednej regule (flaga L), po-
go pobrać z naszej strony, więc blokada szukiwarki i przeglądarki często w ogóle nieważ jest to dopiero początek nasze-
HotLinków mu w tym nie przeszkodzi. nie akceptują), przez co adres może wy- go większego pliku .htaccess. Tak więc,
Oczywiście użyty w tym przykładzie adres glądać niezbyt zachęcająco, np.: poprawny będzie zarówno adres http:
jest testowy i należy go przystosować do
własnego serwisu. Opisaną blokadę Hot- Listing 4. Poprawny zestaw reguł do tworzenia przyjaznych URL-i w przykładowej
Linków można oczywiście wykorzystać galerii zdjęć
także w przypadku plików graficznych,
z czego korzystają zazwyczaj firmy udo- RewriteEngine On
RewriteRule ^([^-]+)/([^-]+)/([^-]+)/$ index.php?module=$1&function=$2&id=
stępniające darmowy hosting.
$3 [L,NC,NS]
RewriteRule ^([^-]+)/([^-]+)/$ index.php?module=$1&function=$2 [L,NC,NS]
Przepisywanie RewriteRule ^([^-]+)/$ index.php?module=$1 [L,NC,NS]
adresów URL RewriteRule ^([^-]+)/([^-]+)/([^-]+)$ index.php?module=$1&function=$2&id=
(tzw. przyjazne linki) $3 [L,NC,NS]
RewriteRule ^([^-]+)/([^-]+)$ index.php?module=$1&function=$2 [L,NC,NS]
Mod_Rewrite pozwala także przepisywać
RewriteRule ^([^-]+)$ index.php?module=$1 [L,NC,NS,QSA]
linki. Niejedna osoba zdziwiła się pewnie
widząc na dynamicznej stronie WWW ad-

PHP Solutions Nr 6/2006 www.phpsolmag.org 55


Dla zaawansowanych mod_rewrite

//www.mojadomena.pl/gallery/photo/21, jak wartość1/wartość2/wartość3 bez względu Dodatkowo, jeżeli pierwszym słowem


np. http://www.mojadomena.pl/galLeRy/ na to, jakie zmienne mają zostać przeka- w adresie nie jest ani gallery, ani news, to
phOtO/21. Natomiast wyrażenie regularne zane. Jak widzimy, najwyżej w hierarchii adresy są przepisywane w sposób przed-
([^-]+) pozwala na wstawienie dowolne- reguł znajduje się zestaw zasad dla galerii. stawiony w Tabeli 3.
go słowa. Jest on skonstruowany tak, że działa jedy- W tym przypadku w adresie wyróżnia
Wszystko działa, ale czy na pewno? nie wtedy, gdy pierwszy pseudokatalog (w się zmienna option. Wspomnijmy tu jeszcze
Co będzie, jeśli w adresie wpiszemy tylko tym przypadku wartość1) wskazuje na sło- raz o całej hierarchii reguł. Gdybyśmy przed
gallery/photo/, bez podawania numeru wo gallery. W tym przypadku Mod_Rew- zestawami news i gallery wstawili ostatni ze-
zdjęcia? Ujrzymy wtedy komunikat o braku rite przepisuje nam adresy wg schematu staw z tego pliku, wszystkie adresy byłyby
katalogu, ponieważ przedstawiona reguła z Tabeli 1. Drugi zestaw działa tylko wtedy, mu podporządkowane, czyli przekazywa-
opisuje tylko trzypoziomowe adresy URL. gdy pierwszy pseudokatalog wskazuje na na byłaby zawsze zmienna option jako dru-
Kod, który rozwiąże ten problem, będzie słowo news. W tym przypadku adresy wy- gi parametr – tylko dlatego, że np. adresy
wyglądał jak na Listingu 4. glądają jak w Tabeli 2. gallery/photo/341 i news/read/53 pasują do
W tym przypadku ważna jest hierarchia Jak widać, zamiast zmiennej function tego schematu i byłyby z nim porównywa-
reguł: reguły muszą być ułożone w takiej przekazywana jest zmienna action, ale ne na samym początku. Jednak gdy sche-
kolejności, aby sobie nawzajem nie prze- oczywiście można także zamiast zmien- mat ten jest na końcu pliku, adresy porów-
szkadzały. Przykładowo, gdyby pierwsza nych module i id przekazywać cokolwiek nywane są najpierw z pierwszymi zestawa-
z nich była jednopoziomowa, to po wpisa- innego – wszystko zależy od konstrukcji mi, a później dopiero w przypadku niepowo-
niu gallery/photo/21 adres wskazywałby na strony. dzenia z ostatnim.
index.php?module=gallery/photo/21, z cze-
go wynika, że w pierwszej kolejności powin- Listing 5. Zachowujemy schemat linków przy różnorodnych parametrach
niśmy definiować reguły o największej licz-
bie poziomów. Zwróćmy też uwagę na flagę RewriteEngine On
QSA w ostatniej regule, która by bez niej nie RewriteRule ^gallery/([^-]+)/([^-]+)/$ index.php?module=gallery&function=$1&id=
$2 [L,NC,NS]
działała, gdyżwyrażenie regularne ([^-]+)
RewriteRule ^gallery/([^-]+)/$ index.php?module=gallery&function=$1 [L,NC,NS]
nie może występować samodzielnie, jeżeli RewriteRule ^gallery/$ index.php?module=gallery [L,NC,NS]
nie zdefiniowano RewriteBase. RewriteRule ^gallery/([^-]+)/([^-]+)$ index.php?module=gallery&function=$1&id=
Można wykorzystać nasz zestaw reguł $2 [L,NC,NS]
nieco inaczej, np. niepodanie ID zdjęcia RewriteRule ^gallery/([^-]+)$ index.php?module=gallery&function=$1 [L,NC,NS]
RewriteRule ^gallery$ index.php?module=gallery [L,NC,NS]
może prowadzić do listy wszystkich obraz-
RewriteRule ^news/([^-]+)/([^-]+)/$ index.php?module=news&action=$1&id=
ków w galerii (w galeriach o większej licz- $2 [L,NC,NS]
bie poziomów może kierować np. na listę RewriteRule ^news/([^-]+)/$ index.php?module=news&action=$1 [L,NC,NS]
zdjęć o określonej tematyce) lub przekie- RewriteRule ^news/$ index.php?module=news [L,NC,NS]
rować pod sam adres /gallery. Pozwala to RewriteRule ^news/([^-]+)/([^-]+)$ index.php?module=news&action=$1&id=
$2 [L,NC,NS]
niesamowicie uelastycznić funkcjonalność
RewriteRule ^news/([^-]+)$ index.php?module=news&action=$1 [L,NC,NS]
naszego serwisu. RewriteRule ^news$ index.php?modul=news [L,NC,NS]
Zauważmy też, że przy trzech pozio- RewriteRule ^([^-]+)/([^-]+)/([^-]+)/$ index.php?module=$1&option=$2&id=
mach zdefiniowaliśmy 6 reguł, podczas $3 [L,NC,NS]
gdy wystarczy po jednej zasadzie na RewriteRule ^([^-]+)/([^-]+)/$ index.php?module=$1&option=$2 [L,NC,NS]
RewriteRule ^([^-]+)/$ index.php?module=$1 [L,NC,NS]
każdy z nich. Przyjrzyjmy się dokładnie
RewriteRule ^([^-]+)/([^-]+)/([^-]+)$ index.php?module=$1&option=$2&id=
trzem pierwszym regułom i sprawdźmy, $3 [L,NC,NS]
czym różnią się od kolejnych trzech (nie RewriteRule ^([^-]+)/([^-]+)$ index.php?module=$1&option=$2 [L,NC,NS]
licząc QSA w ostatnim wersie). W pierw- RewriteRule ^([^-]+)$ index.php?module=$1 [L,NC,NS,QSA]
szej trójce na końcu ostatniego wyrażenia
Listing 6. Zestaw reguł umożliwiający stosowanie adresów udających pliki html
występuje znak slash (/), a w drugiej nie
(np. gallery-photo-21.html)
– dzięki temu poprawne będą zarówno
adresy, które się na niego kończą (np. RewriteEngine On
http://www.mojadomena.pl/gallery/photo/ RewriteRule ^gallery-([^-]+)-([^-]+)\.html$ index.php?module=gallery&function=
$1&id=$2 [L,NC,NS]
21/), jak i te, które nie mają takiego za-
RewriteRule ^gallery-([^-]+)\.html$ index.php?module=gallery&function=
kończenia (np. http://www.mojadomena.pl/ $1 [L,NC,NS]
gallery/photo/21). RewriteRule ^gallery\.html$ index.php?module=gallery [L,NC,NS]
Nic nie stoi na przeszkodzie, aby RewriteRule ^news-([^-]+)-([^-]+)\.html$ index.php?module=news&action=$1&id=
zachować taki sam schemat linków (np. $2 [L,NC,NS]
RewriteRule ^news-([^-]+)\.html$ index.php?module=news&action=$1 [L,NC,NS]
wartość1/wartość2/wartość3) przy róż-
RewriteRule ^news\.html$ index.php?module=news [L,NC,NS]
nych parametrach (np. gallery/wartość1/ RewriteRule ^([^-]+)-([^-]+)-([^-]+)\.html$ index.php?modul=$1&opcja=$2&id=
wartość2 kontra news/wartość2/wartość3). $3 [L,NC,NS]
Spójrzmy na Listing 5. Przedstawiona RewriteRule ^([^-]+)-([^-]+)\.html$ index.php?module=$1&option=$2 [L,NC,NS]
na nim zawartość pliku .htaccess pozwala RewriteRule ^([^-]+)\.html$ index.php?module=$1 [L,NC,NS]
na zachowanie wspomnianego schematu

56 www.phpsolmag.org PHP Solutions Nr 6/2006


mod_rewrite Dla zaawansowanych

Tabela 4. Zasady przepisywania adresów galerii na pliki HTML Tytuły podstron


Adres Adres prawdziwy lub artykułów w adresach URL
Choc przyjazne linki nie są kluczem do
gallery-wartość2-wartość3.html index.php?module=gallery&function=warto-
ść2&id=wartość3 wielkiego sukcesu w wyszukiwarkach, mają
duży wpływ na pozycję strony. Aby dodat-
gallery-wartość2.html index.php?module=gallery&function=wartość2
kowo umocnić się np. w Google, najlepiej
gallery.html index.php?module=gallery
jest przygotować adresy, w których widnieją
Tabela 5. Zasady przepisywania adresów newsów na pliki HTML nazwy podstron, tytuły newsów, itp. Jeżeli
ktoś wtedy wpisze w Google hasło Bolek
Adres Adres prawdziwy
i Lolek na wycieczce, a my (np. mając
news-wartość2-wartość3.html index.php?module=news&action=wartość2&i- witrynę poświęconą filmom animowanym)
d=wartość3
będziemy mieli zaindeksowaną podstronę
news-wartość2.html index.php?module=news&action=wartość2 o adresie bolek_i_lolek_na_wycieczce.html
news.html index.php?module=news lub news/bolek_i_lolek_na_wycieczce, to
możemy być pewni, że Google potraktuje
Tabela 6. Zasady przepisywania pozostałych adresów (nie newsów i nie galerii) na pliki HTML
naszą stronę lepiej niż inne. Uzyskanie
Adres Adres prawdziwy takiego adresu wymaga jedynie dodania
wartość1-wartość2-wartość3.html index.php?module=wartość1&option=warto- prostej reguły w pliku .htaccess:
ść2&id=wartość3
wartość1-wartość2.html index.php?module=wartość1&option=wartość2 RewriteRule ^news/(.*)$
wartość1.html index.php?option=wartość1 index.php?module=news&name=$1

Adresy przepisywane wcale jednak gallery/photo/21?PHPSESSID= lub, w przypadku linków z sufiksem .html:
nie muszą wyglądać jak katalogi. Można accfcc299077b36817dc534c90588253
tak wykorzystać Mod_Rewrite, aby adresy RewriteRule ^news-(.*)\.html$
były maskowane w postaci plików HTML, czy też: index.php?module=news&name=$1
co wygląda jeszcze bardiej efektownie.
Ogólnie wyglądałoby to tak, że schemat gallery-photo-21.html?PHPSESSID= Wyrażenie regularne (.*) pozwala na za-
wartość1/wartość2/wartość3 wyglądałby accfcc299077b36817dc534c90588253 pisanie dowolnego ciągu znaków (również
np. następująco: wartość1-wartość2-war- spacji), więc teoretycznie adres mogliby-
tość3.html, np. gallery-photo-21.html. Aby Wyszukiwarka znajduje w nich pytajnik, śmy zapisać jako news/Bolek i Lolek na
uzyskać taki efekt, należy przebudować znak równości oraz bezsensowny (dla wy- wycieczce, aczkolwiek nie jest to zalecane,
poprzedni zestaw reguł w pliku .htaccess, szukiwarki) ciąg znaków przedłużających gdyż przeglądarki i wyszukiwarki różnie
co już nie będzie trudne (Listing 6). URL-a. Problem ten oczywiście występu- interpretują spacje. Powyższy przykład
Jak widzimy, reguł jest teraz mniej, po- je tylko i wyłącznie wtedy, gdy dany serwis może już praktycznie działać, o ile system
nieważ w poprzednim przykładzie występo- korzysta z funkcji obsługi sesji w PHP. Aby CMS na naszej witrynie potrafi odnaleźć
wały dodatkowe zasady, dzięki którym adres wyszukiwarki nie doklejały numeru sesji artykuł po nazwie zawartej w zmiennej, nie
mógł się kończyć na slashu (/), ale nie mu- do linków, należy w pliku .htaccess, za- potrzebując ID. Mało który system CMS ma
siał. Tym razem slashe nie są używane: zo- raz pod RewriteEngine On, dodać nastę- jednak funkcje typu GetIdFromName(nazwa),
stały zastąpione myślnikami, a na końcu do- pujące linijki: a my nie będziemy się teraz zagłębiać
dajemy sufix .html, udający rozszerzenie pli- w ich tajniki.
ku. Jak już wspomnieliśmy, przed znakami Options FollowSymLinks Jest jeszcze jeden problem dotyczący
specjalnymi należy stawiać znak backslash php_flag session.use_trans_sid off powyższego przykładu – mianowicie znaki
(\), dlatego widnieje on przed kropką. Dzię- specjalne oraz polskie znaki diakrytyczne,
ki plikowi .htaccess z Listingu 6, adresy URL Jeżeli zamiast strony zobaczymy błąd 500 akcenty (np. włoskie czy francuskie), nie-
wyglądają jak w Tabelach 4, 5 i 6. W rezulta- (Internal Server Error), to znaczy, że opcja mieckie umlauty, itd. Znaki specjalne naj-
cie wszystko działa tak, jak w przypadku ad- use_trans_sid jest ustawiona globalnie na lepiej zamienić na myślniki, a narodowe
resów w stylu katalogowym. on w konfiguracji serwera i nie możemy jej – na wersje łacińskie. Spacje zastąpimy
lokalnie zmienić. Pozostaje nam wtedy po- znakami podkreślenia (_), zaś wszystkie
Pozbywanie się prosić administratora, aby ustawił tę opcję litery uczynimy małymi.
identyfikatora sesji lokalnie na off. Dzięki napisanej w PHP funkcji
Jak już wspomnieliśmy, nawet najlepiej replace_titles(), którą przedstawiamy na
zbudowane adresy URL nic nie dadzą Praktyczne Listingu 7, możemy zamienić zdanie Mały
(w sensie pozycjonowania stron w wy- zastosowania jeż je sobie jabłko, które jest zepsute na
szukiwarkach), jeżeli do linków dokleja- Mod_Rewrite maly_jez_je_sobie_jablko,_ktore_jest_ze-
ny będzie identyfikator sesji. W rezulta- w systemach CMS psute.
cie nasze adresy, zamiast wyglądać jak W praktyce, Mod_Rewrite ma wiele zasto- Działanie tej funkcji opisaliśmy w ko-
gallery/photo/21 czy gallery-photo-21.html, sowań w systemach typu CMS. Wymieni- mentarzach. Oczywiście, tablicę ze znaka-
będą wyglądały jak: my i opiszemy najpopularniejsze z nich. mi można powiększyć lub dostosować do

PHP Solutions Nr 6/2006 www.phpsolmag.org 57


Dla zaawansowanych mod_rewrite

innych języków (my jako przykładu użyli- pomnijmy, że parametr $1 nie musi zostać Wspomniane systemy portalowe
śmy polskich znaków diakrytycznych). Za- użyty. Nie będziemy od nowa podawali ta- w katalogu modules/News/ mają plik
stosowanie tej funkcji w systemie CMS jest bel z przykładami przepisywania adresów, funcs.php. Odpowiada on za generowa-
proste. Załóżmy, że za wyświetlanie new- bo można to łatwo wywnioskować same- nie linków w systemie newsów. Otwie-
sów odpowiada plik news.php. Po pierw- mu. Oczywiście, użyliśmy przykładów te- ramy go i wstawiamy w nim opisa-
sze, należy umieścić tę funkcję najlepiej stowych – chcąc je wdrożyć w działających ną już funkcję zamiany tytułów (repla-
zaraz na początku tego skryptu. Po drugie, systemach CMS należy mieć jakąś wiedzę ce_titles()) z Listingu 7, zaraz po ko-
wystarczy odszukać szablon dla linku (za- na temat PHP. mentarzu autora. Następnie odnajduje-
kładamy, że CMS nie korzysta z systemu my w funcs.php komentarz:
szablonów w celu ustalenia wzorca dla Zastosowanie przyjaznych
linków, tylko ze zwykłych zmiennych PHP), linków w popularnych // Allowed to read full article?
który wygląda mniej więcej tak: systemach CMS
Pokażemy teraz, jak wdrożyć przyjazne i wstawiamy pod nim:
<a href=”index.php?module=news&amp; URL-e w popularnych CMS-ach opartych
id=<?=$id?>”><?=$title?></a> na platformie Nuke (PostNuke, MDPro, $nazwa_newsa =
PHP-Nuke, enVolution, itp.). Są one kom- replace_titles($info[title]);
Ten szablon należy zamienić na umożli- patybilne z modułem generowania layoutu
wiający tworzenie przyjaznych URL-i przy o nazwie AutoTheme (MDPro i PHP-Nuke Zmienna $nazwa_newsa będzie wywoływa-
użyciu naszej funkcji, której dla adresów mają go nawet w standardowej instalacji). ła funkcję replace_titles() i stosowała ją
udających katalogi użyjemy w następujący Jest on obecnie najczęściej używanym wobec tytułu newsa podanego w zmiennej
sposób: systemem szablonów dla tych CMS-ów. $info[title]. Poniżej wstawionego przez
Moduł ten umożliwia już włączenie przyja- nas kodu widnieje linijka:
<a href=”news/<?=$id?>/<?= znych linków w ustawieniach. Niestety, są
replace_titles($title)?>”><?= to tylko linki w stylu plikowym – dobrze, że if (pnSecAuthAction(0, $component,
$title?></a> możemy chociaż wybrać sufix (domyślnie $instance, ACCESS_READ)) {
do wyboru mamy .html, .htm i .phtml). Po
Natomiast w przypadku tych, które udają uruchomieniu zamiany linków na przyja- Pod tym kodem powinna być umieszczo-
pliki, szablon będzie wyglądał następująco: zne, wystarczy przekopiować do katalogu na definicja zmiennej $fullarticle. Za-
głównego jeden z gotowych plików .htac- mieniamy całą linijkę z tą definicją na:
<a href=”news-<?=replace_titles( cess, które są dostarczane razem z mo-
$title)?>-id<?=$id?>/”><?=$title?></a> dułem. Nazwy tych plików zaczynają się $fullarticle = $news_name.'-id'.
od sufiksu, więc jeżeli wybierzymy .phtml, $info[sid].'.html';
Nasz schemat przy adresach katalogo- kopiujemy plik phtml.htaccess do głównego
wych będzie więc wyglądał tak: news/ katalogu strony i zmieniamy jego nazwę na Dzięki temu link do newsa będzie teraz
ID/nazwa_newsa, a przy plikowych: .htaccess. Po tym zabiegu domyślne sche- wyglądał następująco: nazwa_newsa-
news-nazwa_newsa-idID.html. Teraz, aby maty przyjaznych linków zaczną działać. id31.html, gdzie liczba 31 jest przykłado-
ukończyć nasze dzieło, dodajemy do pliku Naszym celem jest jednak opisanie, jak te wym ID newsa. Oczywiście sufix .html na-
.htaccess następującą regułę (dla sche- schematy przerobić i ustawić tak, aby w lin- leży zamienić na wybrany wcześniej przez
matu katalogowego): kach do newsów były nazwy tych ostatnich. nas. Pozostało jedynie dodanie reguły

RewriteRule ^news/([^-]+)/(.*)$
Listing 7. Funkcja replace_titles(), która zmienia wielkość liter i usuwa znaki
index.php?modul=news&id=$1 [L,NC,NS] narodowe

Przekazuje ona tylko ID do zmiennej id <?php


function replace_titles($text){
jako słowo. Drugi parametr może być
// tablice
dowolnym ciągiem znaków i nie musi być $search =array(' ', '/', '\'', '&', '%', 'ć', 'ś', 'ą', 'ż', 'ó', 'ł', 'ś',
przekazywany, ponieważ do rozpoznawa- 'ż', 'ń', 'ę',);
nia newsa wystarczy samo ID. $replace=array('_', '-', '-', 'and', 'procent', 'c', 's','a', 'z', 'o', 'l',
Ta sama reguła dla schematu plikowe- 's', 'z', 'n', 'e',);

go będzie wyglądała następująco:


// Zamiana wszystkich liter w stringu na małe
$text = strtolower($text);
RewriteRule ^news-(.*)-([^-]+)\. // Zamiana znaków w stringu na znaki html
html$ index.php?modul=news& $text = html_entity_decode($text);
id=$2 [L,NC,NS] // Zamiana znaków z tablic
$text = str_replace($search, $replace, $text);
// Zwracanie tekstu oczyszczonego ze znakow specjalnych i narodowych
Również ta reguła przekazuje tylko ID return $text;
do zmiennej id, lecz tym razem jest ono }
ustawione jako drugi parametr, w związku ?>
z czym jest przekazywane jako $2. Przy-

58 www.phpsolmag.org PHP Solutions Nr 6/2006


mod_rewrite Dla zaawansowanych

w naszym pliku .htaccess, która powinna Jak widzimy, w miejscu, w którym można Na koniec, aby wszystko działało po-
być ustawiona jako pierwsza: wstawić wartość zmiennej w URL-u, znaj- prawnie, należy zmodyfikować re-
duje się wyrażenie ([\w\d\.\:\_\/]+), guły w pliku .htaccess tak, aby by-
RewriteRule ^(.*)-id([0-9]+)\.html$ którego rezultat po przetworzeniu przez ły one zgodne z pozycjami pliku auto-
modules.php?op=modload&file= funkcję preg_replace() jest oznaczony urls.ext.php. Miejmy nadzieję, że nie bę-
article&sid=$2 [L,NC,NS] pod nazwą $1, którą musimy umieścić dzie z tym już problemów. Efekt zasto-
w tablicy $replace. Odpowiedni wpis sowania przyjaznych linków w systemie
Dzięki temu wszystko będzie już działać. w tablicy $replace będzie więc wyglądał CMS można zobaczyć chociażby na ofi-
Zauważmy, że w tym przypadku dla ID następująco: cjalnej stronie CMS-a PostNuke, http:
użyliśmy wyrażenia regularnego, które ze- //www.postnuke.com/ czy polskiej stro-
zwala na wprowadzenie tylko danej liczby. '”changelang-$1.'.$autourlsext.'”', nie www.underflip.pl.
Można dodatkowo zmodyfikować
domyślny schemat generowania linków Zmienna $autourlsext oznacza sufix. Podsumowanie
w AutoTheme. Standardowo są to angiel- Nie mamy jednak zamiaru opisywać Przedstawiliśmy Ci, jak w praktyce za-
skie odpowiedniki modułów działających szczegółowo modyfikacji CMSów i ich stosować reguły Mod_Rewrite i opisać
z tymi CMS-ami. Aby je zmodyfikować modułów, dlatego też nie będziemy się je za pomocą wyrażeń regularnych. Uży-
w dowolny sposób, należy otworzyć plik w to dalej zagłębiać. Ograniczymy się te przez nas przykłady powinny dać Ci
modules/AutoTheme/extras/nazwa_uży- do pokazania, jak powinny wyglądać no- wyraźne wskazówki, jak postępować.
wanego_systemu_cms/autourls.ext.php. we pozycje. Przykładowo, jeśli chcemy Wszystkie z nich zostały sprawdzone na
W pliku tym widzimydwie tablice dodać pozycję index.php?module=war- kilku serwerach i działają, dlatego jeże-
- $search i $replace oraz funkcję preg_ tość1&file=wartość2, to przed znakiem li którykolwiek z nich nie będzie zwracał
replace(), która zamienia wszystkie ad- zapytania należy wstawić slash, a znak oczekiwanego wyniku, może być to wy-
resy według wyrażeń z tablic. Zatem aby & zastąpić wyrażeniem &(?:amp;)?, które łącznie winą serwera. Wszystko zależy
zmienić schemat linków, należy zmody- jest warunkiem typu lub: zadziała i wtedy, bowiem od konfiguracji Mod_Rewrite na
fikować tablice, które sobie odpowiadają gdy w linku jest znak & i wtedy, gdy znaj- serwerze, która bywa błędna.
na zasadzie, że pierwsza pozycja z tablicy duje się tam &amp; (poprawna wersja). Jednocześnie pamiętajmy, że moduł
$search jest zamieniana na pierwszą Wartość1 i wartość2 należy zastąpić wy- Mod_Rewrite jest na tyle potężny, że
z tablicy $replace, druga na drugą, itd.; rażeniem ([\w\d\.\:\_\/]+), a na końcu nie sposób streścić wszystkie jego moż-
dlatego też obie tablice mają tę samą ilość pozycji umieścić znaki "|. Całość powinna liwości nie tylko w jednym artykule, ale
pozycji. Sposób edycji pokażemy na przy- wyglądać następująco: nawet w całym numerze pisma. Życzymy
kładzie jednej z pozycji tablicy $search: zatem poprawnych reguł, zadowolenia
$prefix.'index.php\?module= użytkowników oraz wysokich pozycji
$prefix . 'index.php\?newlang= ([\wd\.:\_/]+)&(?:amp;)? w wyszukiwarkach dzięki zastosowaniu
([\wd\.:\_/]+)”|', file=([\wd\.:\_/]+)”|' Mod_Rewrite. n

R E K L A M A
Kasa dla
Webmastera

Tajniki freelancingu
Stopień trudności: lll
Krzysztof Trynkiewicz

W poprzednim numerze przedstawiliśmy


zagadnienie freelancingu jako formy zarabiania
przez programistów oraz szukania wykonawców
projektów. Dziś skupimy się na szczegółach
definiowania zleceń i składania ofert
zleceniodawcom. Wskażemy także użyteczne
praktyki stosowane podczas tworzenia
swojego portfolio oraz resume do celów
freelancingowych. Poznamy również opinię
profesjonalnego freelancingowca w wywiadzie
udzielonym specjalnie dla PHP Solutions.

J
uż na początku swojej przygody z freelancingowców. Pamiętajmy jednak, by
freelancingiem możemy dojść do nie kopiować treści, ani wyglądu – port-
wniosku, że mnogość zleceń wca- folio i resume muszą być inne – w końcu
le nie gwarantuje łatwej pracy, zaś po- każdy ma swój styl i własne dokonania.
zyskanie pierwszych klientów jest zada- Warto na wstępie wspomnieć kilka słów o
niem trudnym. W większości przypadków sobie. Niech to jednak nie będzie opis mu-
zleceniodawcy wybierają droższych, lecz zyki, której słuchamy. Istotne dla zlecenio-
bardziej doświadczonych programistów. dawcy jest: gdzie mieszkamy, dla kogo i w
Ich przewaga leży jednak nie tylko w ilo- jakich porach pracujemy, jakie mamy do-
ści ukończonych projektów. Wraz z ro- świadczenie w innych stałych pracach,
snącym doświadczeniem, wypracowali a wreszcie: jakie projekty ukończyliśmy.
oni własne schematy pisania kuszących Ukończone projekty czołowych freelan-
ofert. Przyjrzyjmy się bliżej praktykom, cingowców można liczyć w setkach, jed-
które stosują. nak żaden nie wspomina o więcej niż kilku
w swoim portfolio – powinniśmy wybrać te
Tworzymy najlepsze i najbardziej rozwinięte. Dobrze
W SIECI portfolio i resume jest załączyć fragment własnego skryptu
Pierwszą rzeczą, jaką musimy zrobić, jest lub gotową klasę, opatrzoną komentarza-
stworzenie strony informującej o naszych mi. Możemy wspomnieć nazwy naszych
• http://allfreelance.com dokonaniach. Przy tworzeniu portfolio i re- największych klientów oraz podać moż-
– kompendium linków fre-
elancingowych
sume na potrzeby freelancingowe, nale- liwe sposoby komunikacji – w grę wcho-
• http://guru.com ży zwrócić uwagę w pierwszej kolejności dzi naturalnie e-mail, ale także komuni-
• http://rentacoder.com na konkurencję. Głównym źródłem naszej katory oraz telefon. Nie zapomnijmy po-
• http://getafreelancer.com
wiedzy będą szczegóły kont wiodących chwalić się znanymi nam językami obcymi

62 www.phpsolmag.org PHP Solutions Nr 6/2006


Kasa dla
Tajniki freelancingu
Webmastera

– zleceniodawca w znakomitej większości “ty”. W treści oferty powinniśmy zamie- nić projekt. W tym celu posłużymy się
przypadków chętniej spojrzy na wykonaw- ścić coś przyciągającego – zwykłe “zro- ponownie historią ukończonych projek-
cę, z którym może porozumieć się w oj- bię to za $25” nie wystarczy. Przydatne tów zleceniodawcy. Pamiętajmy, że naj-
czystym języku. Schludną stronę informa- jest tu przedstawienie swojego pomysłu lepiej widziana jest cena najbardziej od-
cyjną jednego z czołowych freelancingow- na wykonanie projektu, naturalnie zgod- powiednia – jeżeli ustalimy za niską kwo-
ców możemy zobaczyć na Rysunku 1. nego z wytycznymi. Jeśli ukończyliśmy tę, zostaniemy wzięci za amatorów. Z ko-
podobne projekty, warto wspomnieć o lei zbyt wysoka cena skreśla nas nawet
Szukamy kilku najistotniejszych oraz wskazać od- przed przeczytaniem treści oferty. Ilość
odpowiedniego projektu nośnik do swojej historii ukończonych złożonych ofert wskaże nam, jak wyglą-
Zanim złożymy swoją pierwszą ofertę prac. Warto w tym miejscu zajrzeć do hi- da nasza konkurencja – jeśli jest ich wie-
(ang. bid), musimy znaleźć projekt ade- storii projektów zleceniodawcy – pozna- le, powinniśmy obniżyć swoją cenę.
kwatny do naszych umiejętności. W przy- my jego wymagania, a w zależności od
padku programistów PHP sprawa wyda- pośrednika freelancingowego, także styl Przekonujemy
je się prosta: interesują nas zlecenia z i treść ofert, które zaakceptował. Nie mo- do siebie zleceniodawcę
tej właśnie kategorii. Warto jednak zwró- żemy krytykować wytycznych zlecenio- Wiele pośredników freelancingowych udo-
cić uwagę także na projekty, które zosta- dawcy – zawsze mu przytakujmy, wska- stępnia różnego typu narzędzia, pomocne
ły zakwalifikowane do kategorii ASP, JSP, zując opcjonalne rozwiązania. Koniecz- w komunikacji ze zleceniodawcą. Powinni-
czy nawet Flash. Często bowiem znaj- nie musimy wspomnieć o przewidywa- śmy więc używać wbudowanych chatów
dziemy tam swoich przyszłych klientów, nym czasie wykonania zlecenia, najlepiej i for dyskusyjnych, celem sprecyzowania
którzy nie mając głębszej wiedzy o języ- krótszym niż ustalony deadline. Jeżeli zlecenia – nie bójmy się spytać o szcze-
kach webowych, błędnie zakwalifikowali składamy ofertę na zlecenie wykonania góły, w końcu zależy nam, by zlecenio-
projekt. Niektórych możemy także przeko- skryptu bądź klasy, warto umieścić odno- dawca dostrzegł nasze zainteresowanie.
nać, że dana strona jest możliwa do stwo- śnik do przykładowych fragmentów wła- W kontaktach tego typu kluczowe jest sto-
rzenia przy użyciu darmowych technik, w snego kodu. Pamiętajmy jednak, by kod sowanie stylu podobnego do zleceniodaw-
naszym przypadku najpewniej połączenia ten był opatrzony odpowiednimi komen- cy oraz bardzo szybkie, precyzyjne odpo-
PHP oraz bazy danych MySQL. Przy skła- tarzami! Również w ofercie poinformuj- wiedzi. Jeśli pośrednik umożliwia złoże-
daniu oferty zaznaczmy, że koszty wyko- my, że nasz kod będzie jasny, czytelny, nie pieniędzy do depozytu zabezpieczają-
nania będą niższe – nie musimy bowiem a co najważniejsze – będzie zawierał ko- cego zleceniodawcę (ang. Seller Guaran-
opłacać licencji za programy, w których mentarze. Dobrym pomysłem jest wspo- tee Deposit), skorzystajmy z niego! Ta-
wykonujemy projekt. mnienie o możliwości stworzenia doku- kiego typu zabezpieczenie finansowe da-
mentacji do projektu, np. przy użyciu na- je nam ogromne szanse na szybkie wybi-
Składamy rzędzia phpDocumentor. Na zakończe- cie się spośród tłumu nowych freelancin-
pierwszą ofertę nie zachęcamy do zadawania pytań i po- gowców, pieniądze zaś zostaną nam i tak
Znaleźliśmy już odpowiedni dla nas pro- zdrawiamy w adekwatnym do opisu zle- zwrócone. Dodatkowo niektórzy pośredni-
jekt, który będziemy w stanie ukończyć. cenia stylu. Pozostało nam tylko wyce- cy umożliwiają zamieszczenie w szczegó-
Przyszedł czas na złożenie oferty. Za-
poznajmy się najpierw dokładnie z wy-
tycznymi projektu oraz wszelkimi za-
łącznikami. Po treści wytycznych pozna-
my stopień zaawansowania zlecenio-
dawcy – odpowiedź napiszemy adekwat-
nym językiem. Nie możemy przecież za-
sypać laika mnóstwem skrótów i obiet-
nicami zgodności z wciąż rosnącą licz-
bą standardów. Projekt należy uważnie
przemyśleć – dobrze jest, jeśli mamy ja-
kąś wizję i ją przedstawimy w treści na-
szej oferty. Wizję może stanowić dodat-
kowa funkcjonalność, czy nawet układ i
kolorystyka wykonywanej strony. Naszą
ofertę zaczynamy, naturalnie, od przywi-
tania. Warto zwrócić już tutaj uwagę, ja-
kim stylem pisze zleceniodawca, czy jest
to poważna firma, czy też jedna osoba.
W tym drugim przypadku możemy poku-
sić się o trochę mniej formalności, tym
samym pisząc stylem internautów – per Rysunek 1. Fragment strony informacyjnej jednego z czołowych freelancingowców

PHP Solutions Nr 6/2006 www.phpsolmag.org 63


Kasa dla Webmastera Tajniki freelancingu

łach naszego konta informacji o ukończo- tkniemy się na ich średnie wyceny pra- Ile czasu dziennie spędzasz na jej wyko-
nych kursach i posiadanych certyfikatach cy za godzinę: wahają się one od $10 nywaniu?
– z pewnością ten od firmy Zend jest wart do $70, zaś podsumowane zarobki wy- SIG: Mógłbym żyć z samego freelan-
wspomnienia. kraczają poza kwotę 100 000 USD. Z cingu, ale obecnie mam także inną pra-
kolei na Rent A Coder znajdziemy in- cę. Dziennie na freelancing przeznaczam
Szukamy wykonawcy formacje na temat samego rozwoju fre- do 12 godzin, chociaż czasem robię sobie
Wiemy już, jak złożyć kuszącą ofertę. Co elancingu: rocznie ilość zakończonych wolne – w zależności od ilości pracy oraz
jednak, jeśli to my mamy projekt, które- zleceń rośnie o 72%, obecnie miesięcz- spraw do załatwienia poza domem.
go wykonanie wymaga pomocy osoby nie zakańczanych jest około 15 000 pro- PS: Jak porównasz freelancing z pra-
trzeciej? Powinniśmy najpierw chwycić jektów, z czego aż 93% to oferty stałych cą na pełny etat?
za kartkę i ołówek. Z ich pomocą stwo- zleceniodawców. Mamy tym samym od- SIG: We freelancingu mam możliwość
rzymy wytyczne projektu oraz zarys gra- powiedź na pytanie, czy jako freelance- wyboru zleceń – mogę zwyczajnie odmó-
ficzny, który następnie możemy zeska- rzy znajdziemy stałych klientów – z pew- wić, dobierając sobie bardziej odpowiedni
nować i dodać do opisu naszego zlece- nością tak. projekt. Dodatkowo, po zakończonym zle-
nia. Istotne jest, by zastanowić się, w ja- ceniu, otrzymuję pozytywne opinie, które
kich technikach powinien być wykonany Opinia profesjonalisty pomagają mi znaleźć kolejnych klientów
nasz projekt. Jeśli jest to strona interne- Freelancing może być sposobem na ży- – zamknięte koło. Z drugiej strony, cza-
towa, pamiętajmy o problemach z dru- cie. Jednym z ludzi, którzy swój sukces sami trudno jest wygrać aukcję i nie ma
kowaniem stron z animacjami Flasha. zawdzięczają tej formie zarabiania pienię- się pewności, że wpływy będą regularne.
Co jednak istotniejsze, sprecyzujmy ja- dzy, jest Sergey I. Grachyov, z którym mie- Warto mieć zabezpieczenie w postaci dru-
kie wersje PHP i MySQL mamy dostęp- liśmy przyjemność rozmawiać. giej pracy.
ne u naszego hosting providera. Różnica PHP Solutions: Opisz siebie w kilku PS: Czy freelancing uważasz za spo-
między PHP 4 a 5 jest kolosalna, warto słowach – jakimi osiągnięciami freelancin- sób na życie, czy też jest to dla Ciebie for-
więc się dobrze zastanowić, a nawet roz- gowymi możesz się pochwalić? Jak zaczę- ma zarobku na rozpoczęcie własnej dzia-
ważyć zakup serwera z obsługą nowszej ła się Twoja przygoda z freelancingiem? łalności?
wersji PHP, by nasz projekt był rozwijal- Sergey I. Grachyov: Mam 34 lata i SIG: Nie planuję rozpoczęcia własnej
ny w przyszłości bez konieczności prze- mieszkam w St. Petersburgu, w Rosji. Swo- działalności – nie lubię kierować innymi
pisywania kodu. Pamiętajmy też, że nie ich pierwszych zleceń szukałem na Rent A ludźmi. Narazie chcę wciąż działać jako
wszystko należy bezwzględnie opierać Coder, gdzie obecnie mam 745 ukończo- freelancer, kierując swoją ofertę głównie
na klasach, a jeśli już ich chcemy, wyma- nych projektów i status Top Coder. Uzyska- na rynek amerykański – większość zlece-
gajmy dokładnego ich opisu. Przy wybo- łem także status Gold Member na GetAFre- niodawców pochodzi właśnie z tego kra-
rze oferty wykonawcy zwracajmy uwagę elancer ze średnią oceną 9,95. Korzystam ju. Oczywiście freelancing może być do-
na skończone zlecenia, a przede wszyst- także z serwisu Scriptlance, na którym mo- brym wyjściem na zarobienie pieniędzy
kim, na fragmenty przykładowych skryp- ja ocena to 10. Projekty wykonuję głównie w koniecznych do otwarcia własnej firmy,
tów. Na ich podstawie stwierdzimy, czy PHP, Visual Basic, C# oraz ASP. głównie dla studentów, nie mających cza-
będziemy w stanie samodzielnie wnosić PS: Czy freelancing jest na tyle opła- su na regularną pracę. Ja jednak już lubię
poprawki do kodu, czy też każda zmiana calny, by wystarczał jako jedyna praca? to, co robię.
będzie wymagała konsultacji (zapewne
odpłatnej). Zadajmy też przykładowe py-
tanie, np. jakimi językami posługuje się
wykonawca. Da nam to wiedzę o szyb-
kości odpowiedzi, a przy okazji mamy
szansę trafić na osobę znająca nasz ję-
zyk ojczysty, co ułatwi komunikację. Ce-
na nie jest tak istotną kwestią – nie nale-
ży oszczędzać na programistach. Najle-
piej określić pewien zakres – zbyt niska
cena może oznaczać słabe wykonanie,
zaś zbyt wygórowana wcale nie gwaran-
tuje lepszego produktu końcowego.

Odpowiadamy
na pytania
Po publikacji pierwszego artykułu o fre-
elancingu (PHP Solutions 5/2006) otrzy-
maliśmy wiele zapytań odnośnie płatno-
ści. Serwis Guru.com wskaże nam w tej
kwestii wiele odpowiedzi. Szukając de-
welopera PHP na szczycie rankingu, na- Rysunek 2. ProfilSergeya I. Graychova na Rent A Coder

64 www.phpsolmag.org PHP Solutions Nr 6/2006


Tajniki freelancingu Kasa dla Webmastera

PS: Jak freelancing wpływa na Twoje Mogę umieszczać trochę wyższe propo- cingowców: arbiter w pięć sekund przy-
życie prywatne? Czy nie uważasz, że zaj- zycje cenowe, ponieważ klienci mają pew- zna rację (i fundusze) zleceniodawcy, któ-
muje zbyt dużo czasu? ność, że jest to koszt zatrudnienia profe- ry zgłosi, że przekroczyliśmy ustalony de-
SIG: Freelancing nie zajmuje mi wię- sjonalisty. Zdecydowanie polecam ukań- adline, zaś otrzymany negatywny komen-
cej czasu, niż każda inna praca. Ma taki czanie kursów i zdobywanie certyfikatów tarz fatalnie wpłynie na naszą reputację.
sam wpływ na moje życie prywatne, jak wszystkim, którzy chcą być liczącymi się Terminy to kluczowa sprawa, należy ich
praca na etat. programistami. bezwzględnie dotrzymywać.
PS: Od kogo i jakiego typu zlecenia PS: Co uważasz za najważniejsze we PS: Czy sądziłeś, że freelancing bę-
najczęściej przyjmujesz? freelancingu: dobre rekomendacje, portfo- dzie trudniejszy lub łatwiejszy, niż się oka-
SIG: Przyjmuję każde zlecenia, któ- lio, korzystne wyceny, szybki kontakt, czy zał?
re potrafię wykonać. Przed zaakceptowa- może coś innego? SIG: Gdy zaczynałem, nie myślałem o
niem zadania, zawsze sprawdzam, jakie SIG: Najważniejsze, to posiadać dużą trudnościach. Z obecnej perspektywy wy-
komentarze o zleceniodawcy pozostawili ilość pozytywnie zakończonych projektów daje się, że jest to praca o stopniu trudno-
poprzedni wykonawcy – negatywne opinie u swojego pośrednika freelancingowego. ści zależnym od zlecenia, ale przynosząca
w zasadzie przekreślają szansę na zatrud- Ukończone projekty, to reputacja. Reputa- wielką satysfakcję. Nikt mnie nie zmusza,
nienie mnie. Preferuję zadania z precyzyj- cja, to nowi klienci. bym wykonywał trudne zadania – sam o
nie określonymi wytycznymi, w których PS: Posiadanie tak wielu klientów z tym decyduję, co jest ogromnym plusem.
dokładnie wiem, co mam zrobić. Zlecenio- pewnością wymaga dobrego rozplanowa- PS: Jesteś zadowolony z formy zarob-
dawca powinien wiedzieć, czego chce – ja nia czasu i zadań. Czy korzystasz z do- ku, którą wybrałeś, czy też wolałbyś ina-
wiem, co mogę zaoferować. Unikamy tym datkowych narzędzi poza tymi, które udo- czej spożytkować swój czas przeznaczo-
samym niedomówień. stępniają pośrednicy? ny na freelancing?
PS: Korzystałeś z wielu serwisów fre- SIG: Do przechowywania i zarządza- SIG: Jestem bardzo zadowolony z te-
elancingowych. Który z nich mógłbyś po- nia moimi pracami używam Visual So- go, co robię. Gdybym mógł zmienić czas,
lecić? urce Safe. Gdy chcę pokazać zlecenio- zostałbym freelancingowcem pięć lat
SIG: Zdecydowanie polecam Rent A dawcy postęp projektu, niekiedy używam wcześniej.
Coder, głównie z racji bezpieczeństwa: własnego systemu. Narzędzia oferowa- PS: Jakie rady udzieliłbyś osobie, która
wpłaty na Escrow są zobowiązujące, zaś ne przez pośredników świetnie sprawdza- planuje rozpocząć pracę jako freelancer?
w przypadku problemów, arbiter zawsze ją się do składowania kopii zapasowych SIG: Przede wszystkim: próbować.
potrafi pogodzić obie strony. Odradzam oraz dowodów wykonania pracy, w przy- Próbować i nie poddawać się. Wraz z wy-
serwisy, które wymagają wniesienia opła- padku sporu. grywaniem kolejnych zleceń, będzie ła-
ty za rejestrację, umieszczenie lub wyszu- PS: Czy musiałeś nabyć dodatkowe twiej o następne. Zalecam także intensyw-
kiwanie zleceń. umiejętności, np. w zakresie marketingu, ną naukę języka angielskiego.
PS: Czy trudno było zdobyć zaufanie czy komunikacji z klientami, by stać się PS: Dziękujemy za rozmowę.
klientów na serwisach freelancingowych? profesjonalnym freelancingowcem?
Jak dużo czasu Ci to zajęło, i co okaza- SIG: Nie, wystarczyło, że dokładnie Podsumowanie
ło się kluczem do sukcesu? Czy obecnie przeczytałem zasady i zalecenia z Rent Niezależnie od tego, czy jesteśmy wyko-
większość Twoich klientów to byli zlece- A Coder i zacząłem je stosować. Reszta nawcą, czy zleceniodawcą, naszym głów-
niodawcy? (np. zdolności komunikatywne) przyszła nym źródłem informacji są bardziej doświad-
SIG: Swoje pierwsze zlecenie wy- sama z siebie. czeni użytkownicy pośredników freelancin-
grałem dopiero po tygodniu pełnym od- PS: Brak nadzoru szefa z pewno- gowych. Warto zwrócić uwagę na podobne
powiedzi “Niestety, zleceniodawca wy- ścią nie motywuje. Jak poradziłeś so- do naszych oferty i na nich się wzorować.
brał inną ofertę”. Wynagrodzenie za to bie ze zmuszaniem siebie do codzien- Ważne jest, by wybrać jeden serwis pośred-
zadanie wynosiło zaledwie $10. Do- nej pracy? niczący i na nim zdobywać renomę oraz
świadczenie nauczyło mnie, że jako po- SIG: Nie lubię, gdy ktoś obserwu- stałych klientów. Jak widzimy na przykładzie
czątkujący freelancer, musisz próbować je moje ręce podczas pracy. Motywację Sergeya, do sukcesu niedaleko! n
pozyskiwać zlecenia, umieszczając ko- do pracy zyskałem, gdy wmówiłem sobie,
rzystne propozycje cenowe. Na dzień że jest to taka sama praca, jak na pełen
dzisiejszy, większość moich klientów to etat. Teraz już nie zmuszam się do nicze-
nowi zleceniodawcy, chociaż często tra- go – lubię to, co robię. O autorze
fiają mi się propozycje wykonania pro- PS: Jak radzisz sobie ze stresem spo-
Krzysztof Trynkiewicz od wielu lat zaj-
jektów dla byłych klientów. Sam dobie- wodowanym obowiązkowym dotrzymywa- muje się tworzeniem witryn w technolo-
ram swoje zlecenia – a wybieram tylko niem terminów, wiedząc, że kolejne zlece- gii PHP oraz Flash. Ściśle współpracuje
te pewne i jasno określone. nie może nie nadejść prędko? z magazynem PHP Solutions, wykonu-
je także zlecenia freelancingowe. Obec-
PS: Posiadasz również kilka certyfi- SIG: Jestem rozgrzany przez cały rok,
nie rozwija kilka równoległych projektów
katów – jak one wpłynęły na ilość klien- jak po opalaniu, a mieszkam przecież w autorskich dostępnych na witrynie http://
tów? Czy uważasz, że warto było je zdo- St. Petersburgu. Dotrzymywanie terminów eldoras.com
bywać? nie sprawia mi problemu, zaś w przypad-
Kontakt z autorem:
SIG: Zdobycie certyfikatów wydatnie ku braku zleceń – wciąż mam drugą pra-
chris.trynkiewicz@gmail.com
wpłynęło na moją karierę freelancingową. cę. Przestrzegam jednak nowych freelan-

PHP Solutions Nr 6/2006 www.phpsolmag.org 65


Technika

PHPUnit2 w praktyce
Stopień trudności: lll
Marcin Staniszczak

Odnalezienie błędu w nawet najprostrzej


aplikacji składającej się z kilku klas zdarza się
nawet tym, którzy mają wbudowany do edytora
debuger PHP. Na szczęście od pewnego czasu
istnieje framework o nazwie PHPUnit2.

P
rzypomnij sobie jak często Zanim przejdziesz do dalszej czę-
po napisaniu nawet wydawa- ści artykułu, musisz zainstalować u sie-
ło by się prostej klasy, odnaj- bie PHPUni2 – zapoznaj się w tym celu z
dywałeś w niej błąd (i to często w naj- ramką Instalacja PHPUnit2.
mniej spodziewanym miejscu)? Ile ner-
W SIECI
wów i czasu kosztowało Cię odnalezie- Krótki wstęp
nie błędnego fragmentu? Nawet jeśli Pisanie testów jednostkowych nie jest
W Internecie znajduje się spo- jesteś szczęśliwcem używającym edy- niczym innym jak stworzeniem klas ma-
ro materiałów na temat testów tora z wbudowanym debugerem PHP, jących na celu kompletne przetestowa-
jednostkowych. Oto kilka wy-
branych, interesujących odno-
zadanie odnalezienia błędu w aplikacji nie klas wchodzących w skład aplikacji.
śników: składającej się z kilkudziesięciu – kil- Przyjęło się, że na każdą klasę aplika-
kuset klas, często jest zadaniem bar- cji powinna przypadać jedna lub więcej
l http://www.phpunit.de/
– strona i wiki projektu dzo trudnym. klas testów. Zdarza się, że klasa testu-
PHPUnit; W świecie Java-y od dawna stosowa- jąca posiada więcej kodu od klasy testo-
l http://www.phpunit.de/
pocket_guide/index.en.php ne są tak zwane testy jednostkowe (ang.
– podręcznik autora frame-
worka PHPUnit;
unit tests), przeprowadzane z wykorzy- Co musisz wiedzieć
staniem wyspecjalizowanych framewor- W celu zrozumienia materiału zawarte-
l http://www.onlamp.com/
pub/a/php/2005/12/08/ ków testowych – najpopularniejszym z go w tym artykule, wymagana jest przy-
phpunit.html – krótki tutorial nich jest JUnit. Na szczęście od pewnego najmniej podstawowa znajomość PHP5
autora frameworka PHPU- oraz programowania obiektowego. Do-
nit2; czasu istnieje kilka podobnych framewor- datkowo niezbędne są chęci – tworzenie
l http://simpletest.sourceforge ków dla PHP. My zajmiemy się tu frame- testów jednostkowych początkowo może
.net/ – konkurencyjny w sto-
workiem o nazwie PHPUnit2. Jest on roz- wydawać się zbędnym traceniem cenne-
sunku do PHPUnit2 frame-
work do testów jednostko- wijany w ramach PEAR przez Sebastiana go czasu, jednak wraz ze wzrostem pro-
wych klas PHP. jektu, szybko docenia się ich siłę.
Bergmanna.

66 www.phpsolmag.org PHP Solutions Nr 6/2006


PHPUnit2 Technika

wanej. Dzieje się tak, gdyż klasa testu


powinna przeprowadzić możliwie kom- Instalacja PEAR
pletny test. Tak więc pisząc testy, mu- Jeśli korzystasz z systemu Windows, prawdopodobnie musisz zainstalować u siebie pa-
simy także zwracać uwagę na sytuacje kiet PEAR. Zanim jednak to zrobisz, musimy najpierw odpowiednio skonfigurować PHP.
nietypowe – takie jak błędne dane, dane W pliku php.ini ustaw zmienną include_path, tak aby wskazywała na katalog php/PEAR. U
mnie zmienna ta wygląda następująco:
z granic zakresów etc.
Często poruszanym problemem jest include_path=".;e:\PHP-5.1.4\PEAR"
pytanie kiedy zacząć pisać testy? Jedy-
Pamiętaj o kropce na początku! Informuje ona PHP, że plików includowanych w skryptach
ną słuszną odpowiedzią jest - jak najwcze-
PHP ma szukać także w bieżącym katalogu. Bez tego większość Twoich skryptów prze-
śniej. Najlepiej jeszcze zanim zaczniemy stanie działać.
kodować samą klasę naszej aplikacji. Za- Następnie uruchom skrypt go-pear.bat, znajdujący się w katalogu Twojego PHP. Wy-
stanawiasz się teraz zapewne jak masz konuj wszystko zgodnie z poleceniami wyświetlanymi Ci przez skrypt instalacyny.
Gdy skrypt zakończy swoje działanie, kliknij dwukrotnie na pliku pear_env.reg. Spowo-
napisać test czegoś co w rzeczywistości
duje to dodanie niezbędnych wpisów do rejestru systemowego Windows. Teraz możesz
jeszcze nie istnieje. Przystępując do pi- na wszelki wypadek zrestartować system i przystąpić do instalacji PHPUnit2.
sania testu, będziesz zmuszony dokład-
nie przemyśleć i zaprojektować funkcjo- Instalacja PHPUni2
nalność tworzonej klasy. Dodatkowo gdy Framework testowy PHPUnit2 jest rozwijany w ramach PEAR więc jego instalacja jest
będziesz miał już gotowe testy, będziesz zdumiewająco prosta. W celu zainstalowania go w oknie konsoli wydaj polecenie:
mógł się nimi posługiwać w trakcie kodo-
pear install –alldeps phpunit2
wania klasy aplikacji, w celu sprawdzania
jej poszczególnych, stopniowo dopisywa- Polecenie to spowoduje zainstalowanie PHPUnit2 oraz w razie konieczności innych zależ-
nych fragmentów. W ten sposób wszyst- nych pakietów (jak na przykład Log i Benchmark).
Pamiętaj że aby działało u Ciebie polecenie pear, musisz mieć odpowiednio ustawio-
kie ewentualne problemy wykryjesz i roz-
ną ścieżkę w zmiennej systemowej PATH, oraz w pliku php.ini, zmienną include_path.
wiążesz już na etapie kodowania.
Testy jednostkowe pełnią jeszcze jed-
Instalacja Xdebug
ną ważną rolę – są one doskonałą doku- Mimo że Xdebug nie jest wymagany do wykorzystania większości oferowanych
mentacją zbudowanej klasy. Wystarczy przez PHPUnit2 możliwości, jednak niektóre z bardziej zaawansowanych z nich wy-
się z nimi zapoznać, aby dowiedzieć się magają tego rozszerzenia do swojego działania. Warto więc pokusić się od razu o
instalacje Xdebug na swoim komputerze. Aby zainstalować Xdebug, musisz pobrać
w jaki sposób korzystać z danej klasy i jak
rozszerzenie ze strony http://www.xdebug.org – pamiętaj przy tym aby pobrać je w
ona działa. wersji dla twojego PHP (w moim przypadku będzie to wersja dla PHP 5.1.2+ - +
oznacza „i wyższe”).
Projekt naszej aplikacji Gdy pobrałeś już odpowiednią bibliotekę, przegraj ją do katalogu w którym znajdują
się inne Twoje rozszerzenia PHP (najczęściej jest to katalog php/ext). Kolejnym krokiem
Na potrzeby tego artykułu stworzone zo-
jest dodanie następującego wpisu do pliku php.ini:
staną dwie klasy, zarządzające użytkow-
nikami. Za ich pomocą będziemy mo- zend_extension_ts=”c:\php-5.1.4\ext\php_xdebug-5.1.2-2.0.0beta6.dll”
gli dodawać i usuwać użytkowników, oraz
Pamiętaj że ścieżka tu podana musi odpowiadać Twojej instalacji PHP, podobnie na-
sprawdzać czy użytkownik podał popraw- zwę rozszerzenia musi odpowiadać nazwie biblioteki którą pobrałeś ze strony Xde-
ny login i hasło – innymi słowy, czy może bug. Zrestartuj teraz swój serwer HTTP. Jeśli wszystko zrobiłeś dobrze, rozszerzenie
się on zalogować. Xdebug powinno być już dostępne dla Twoich skryptów, a co za tym idzie, także dla
Jedna z klas będzie odpowiedzialna PHPUnit2.
Na koniec jeszcze jedna uwaga – powyższy opis instalacji Xdebug dotycz systemu
za sprawdzanie uprawnień użytkowników. Windows. Instalacja w systemie Linux jest bardziej złożona i możesz się z nią zapoznać
Będzie ona nosiła nazwę Authorization na stronie http://www.xdebug.org/install.php. Dokładniej, zainstalowanie Xdebug na syste-
a jej główną metodą będzie check, przyj- mach unixowych, wymaga jego ręcznej kompilacji.
mująca dwa parametry – login oraz hasło
użytkownika. Dodatkowo metoda check
będzie blokowała konto na jakiś czas je-
śli nastąpią trzy nieudane próby logowania
na nie pod rząd.
Druga klasa – User – będzie posiadała
metody add, remove oraz login.

Tworzymy testy
Tyle informacji na początek wystarcza.
Zacznijmy pisać testy dla pierwszej kla-
sy. Każda klasa testu musi dziedziczyć
po klasie PHPUnit2_Framework_Test-
Case. Klasę testującą klasę Authoriza- Rysunek 1. Nieudane uruchomienie testu

PHP Solutions Nr 6/2006 www.phpsolmag.org 67


Technika PHPUnit2

tion, nazwiemy AuthorizationTest (dalej dowałem specjalną klasę odpowiedzial- toda ta nawiązuje połączenie jedynie
będziemy się trzymali tego sposobu na- ną jedynie za nawiązywanie połączenia raz, przy pierwszym jej wywołaniu. Każ-
zewnictwa): z bazą – zobacz Listing 3. Klasa ta po- de następne wywołanie powoduje jedy-
siada prywatny konstruktor, tak aby nie nie zwrócenie obiektu PDO.
<?php dało się utworzyć jej instancji. Dodatko- Klasa ta zabezpiecza nas przed ko-
class AuthorizationTest wo posiada publiczną statyczną meto- niecznością wielokrotnego nawiązy-
extends PHPUnit2_Framework_TestCase { dę, zwracającą połączenie z bazą. Me- wania połączenia w każdej z klas te-
}
?>

W klasie tej umieścimy nasze testy.


Każdy test to pojedyncza (publiczna)
metoda o nazwie rozpoczynającej się
wyrazem test. Na Listingu 1 znajduje
się metoda testująca poprawność dzia-
łanie metody auth po dodaniu prawidło-
wego loginu oraz hasła. Oczekiwaną
wartością jest Authorization::OK. Je-
śli autoryzacja przebiegła poprawnie,
sprawdzamy także podstawowe dane
Rysunek 2. Klasa Authorization nie przeszła żadnego z naszych testów
o użytkowniku, takie jak jego imię i na-
zwisko. Listing 1. Nasza pierwsza metoda testująca – sprawdza ona zachowanie metody
Do sprawdzania poprawności da- auth klasy Authorization na poprawne dane
nych używa się metod z serii assert.
My w metodzie z Listingu 1 użyliśmy public function testLoginOK() {
$intResult = null;
metod assertTrue oraz assertEquals.
$blnResult = ($intResult = $this->objAuthorization->check('test',
Pierwsza z nich sprawdza czy zmienna '123456')) === Authorization::OK;
podana w pierwszym parametrze ma $this->assertTrue($blnResult, 'Test zwrócił '.$this->result($intResult));
wartość true – jeśli nie, test kończy się $this->assertEquals('Jan', $this->objAuthorization->getName());
błędem. Druga metoda porównuje war- $this->assertEquals('Kowalski', $this->objAuthorization->getSurname());
}
tości z dwóch pierwszych parametrów i
gdy nie są one takie same kończy test Listing 2. Metoda inicjalizująca klasy testowej AuthorizationTest- setUp
błędem. Dokładny opis wszystkich me-
tod assert znajduje się w Tabeli 1. Jak public function setUp()
widzisz zbiór metod testujących jest
{
naprawdę potężny. $this->objAuthorization = new Authorization(DBConnection::
W metodzie tej korzystamy ze getDBConnection());
zmiennej objAuthorization, której }
jednak jeszcze nigdzie nie stworzyli-
Listing 3. Klasa nawiązująca połączenie z bazą danych
śmy, która jednak będzie wykorzysty-
wana jeszcze wiele razy – przecho- <?php
wuje ona obiekt klasy Authorization. class DBConnection {
PHPUnit2 umożliwia stworzenie meto- private static $objDBConnection = null;
dy inicjalizującej - setUp - oraz sprzą-
private function __construct() {
tającej – tearDown. W naszym przypad-
}
ku metoda setUp wygląda tak jak na Li-
stingu 2. public static function getDBConnection() {
Żadne z naszych testów nie wyko- if (self::$objDBConnection === null) {
self::$objDBConnection = new PDO(
rzystują metody tearDown. Przydatna
'mysql:dbname=auth;host=127.0.0.1',
może się ona okazać, wówczas gdy te-
'root',
sty wykorzystują jakieś zewnętrzne za- ''
soby, które należy zamknąć bądź usu- );
nąć (np. pliki, połączenia ze zdalnymi }
serwerami etc.).
return self::$objDBConnection;
Jak widzisz nasza klasa
Authorization przyjmuje w konstrukto- }
rze połączenie z bazą danych – dokład- }
niej obiekt PDO. Jednak w celu ułatwie- ?>
nia tworzenia pozostałych testów, zbu-

68 www.phpsolmag.org PHP Solutions Nr 6/2006


Technika PHPUnit2

stów. Istnieją inne sposoby na rozwią-


Listing 4. Klasa testująca klasę Authorization – plik AuthorizationTest.php zanie tego problemu, ten jednak wy-
daje mi się najprostszy i wystarczająco
<?php
require_once('../classes/Authorization.class.php'); skuteczny.
require_once('DBConnection.php'); Cała klasa testująca klasę
class AuthorizationTest extends PHPUnit2_Framework_TestCase { Authorization znajduje się na Listin-
protected $objAuthorization = null; gu 4. Na samym początku dołączamy
public function setUp() {
plik z naszą nie istniejącą jeszcze kla-
$this->objAuthorization = new Authorization(DBConnection::
getDBConnection()); są Authorization. W tym miejscu war-
} to wspomnieć o organizacji plików apli-
//---------------------------------------------- kacji i testów. Najsłuszniejszym wybo-
public function testLoginOK() { rem wydaje się umieszczenie wszyst-
$intResult = null;
kich testów w osobnym katalogu, na-
$blnResult = ($intResult = $this->objAuthorization->check('test',
'123456')) === Authorization::OK; zwanym np. tests, poza katalogiem te-
$this->assertTrue($blnResult, 'Test zwrócił wartość '.$this- stowanych klas. Dzięki temu unikniemy
>result($intResult)); problemu mieszania się rzeczywistych
$this->assertEquals('Jan', $this->objAuthorization->getName()); klas aplikacji z klasami testów. Ponadto
$this->assertEquals('Kowalski', $this->objAuthorization->getSurname());
ułatwi to umieszczanie aplikacji na ser-
}
public function testLoginUnauthorized() { werze z pominięciem plików testów jed-
$intResult = null; nostkowych.
$blnResult = ($intResult = $this->objAuthorization->check('test2', Spróbujmy teraz uruchomić nasz test.
'123456')) === Authorization::ERROR_UNAUTHORIZED; Z poziomu konsoli wydaj polecenie:
$this->assertTrue($blnResult, 'Test zwrócił '.$this->result($intResult));
}
public function testLogin3times() { phpunit AuthorizationTest
for ($intI = 0; $intI<5; $intI++) {
$this->objAuthorization->check('test', md5(rand())); Oczywiście test nie ma prawa się powieść
} gdyż nie istnieje jeszcze klasa Authoriza-
$intResult = null;
tion. Na konsoli wyświetlona zostanie in-
$blnResult = ($intResult = $this->objAuthorization->check('test',
'123456')) === Authorization::ERROR_3TIMES; formacja o błędzie zbliżona do tej z ry-
$this->assertTrue($blnResult, "Dla testu z poprawnym hasłem zwrócona sunku 1.
wartość to ".$this->result($intResult)); Zbierzmy się więc za budowę naszej
$this->assertNotNull($this->objAuthorization->getLockFor(), 'Niepoprwany klasy. Listing 5 przedstawia jej szablon
czas blokady 1');
– wszystkie niezbędne metody, jednak nie
$blnResult = ($intResult = $this->objAuthorization->check('test',
md5(rand()))) === Authorization::ERROR_3TIMES; wypełnione jeszcze kodem.
$this->assertTrue($blnResult, "Dla testu z losowym hasłem zwrócona wartość Z taką namiastką klasy, nasz test uru-
to ".$this->result($intResult)); chomi się już poprawnie, jednak nasza
$this->assertNotNull($this->objAuthorization->getLockFor(), 'Niepoprwany klasa nie przejdzie żadnego z 4 testów
czas blokady 2');
– zobacz Rysunek 2.
}
public function testLoginUnknownIP() { Spójrz teraz na Listing 4 na metodę
$intResult = null; testLoginUnauthorized. Oczekuje ona
$blnResult = ($intResult = $this->objAuthorization->check('testIP', że metoda check klasy Authorization
'123456')) === Authorization::ERROR_IP; zwróci wartość Authorization::ERROR_
$this->assertTrue($blnResult, "Test zwrócił wartość ".$this-
UNAUTHORIZED. Dodajmy więc do metody
>result($intResult));
} check następującą linię:
private function result($intValue) {
switch ($intValue) { return self::ERROR_UNAUTHORIZED;
case Authorization::OK:
return 'OK'; break;
Uruchom test ponownie. Widać (Rysu-
case Authorization::ERROR_IP:
return 'ERROR_IP'; break; nek 3) że klasa przeszła drugi test. Test
case Authorization::ERROR_UNAUTHORIZED: ten jednak sam, w oderwaniu od pozo-
return 'ERROR_UNAUTHORIZED'; break; stałych nie ma większego sensu. Nasza
case Authorization::ERROR_3TIMES: praca polega teraz na takim zaprogra-
return 'ERROR_3TIMES'; break;
mowaniu wszystkich metod klasy Au-
default:
return '??'; thorization, aby przeszła ona wszystkie
} cztery testy.
} Zanim jednak zabierzemy się za pro-
} gramowanie, musimy utworzyć bazę da-
?>
nych oraz tabelę która będzie przecho-
wywała informacje o użytkownikach. Po-

70 www.phpsolmag.org PHP Solutions Nr 6/2006


PHPUnit2 Technika

lecenie SQL tworzące odpowiednią ta- wy login i hasło, a może tylko login. który poda prawidłowy login i hasło po-
belę w bazie MySQL przedstawia Li- Dodajemy do naszej metody kilka li- winien być akceptowanych przez meto-
sting 5. nii – zobacz Listing 6. Pobieramy tu dę check. Musimy jeszcze dodać bloka-
Gdy istnieje już Tabela, musisz dodać użytkowników. Z taką metodą (oraz ze dę w przypadku zdefiniowanego adresu
do niej odpowiednie dane, które są wyma- zmodyfikowanymi metodami getName i IP spod którego użytkownik może się
gane przez nasz test. Przedstawia to Li- getSurname, tak aby zwracały one od- logować (kolumna ip w tabeli users).
sting 6. powiedni imię i nazwisko użytkownika), Nasza metoda wzbogaciła się o kolej-
Pierwszym etapem będzie spraw- nasza klasa przechodzi już dwa pierw- ny warunek – Listing 7. Dzięki temu kla-
dzenie czy użytkownik podał prawdzi- sze testy. Jednak nie każdy użytkownik sa poprawnie przechodzi większość te-
stów. Do rozwiązania został tylko jeden
Listing 5. Szablon klasy Authorization problem – blokowanie konta użytkow-
nika w przypadku trzykrotnego poda-
<?php nia nieprawidłowego hasła. Po krótkim
class Authorization { przemyśleniu i zapisaniu swoich myśli
w PHP, powstała klasa jak z Listingu 8.
const OK = 0;
const ERROR_UNAUTHORIZED = 1; Wydaje się że wszystko powinno działa
const ERROR_3TIMES = 2; poprawnie. W przypadku niepoprawne-
const ERROR_IP = 3; go hasła sprawdzamy ile było nieuda-
private $objDBConnection; nych prób logowania na dane konto pod
rząd. Jeśli zbyt wiele blokujemy konto.
public function __construct(PDO $objDBConnection) {
$this->objDBConnection = $objDBConnection; Jeśli konto jest zablokowane, spraw-
} dzamy czy możemy je już odblokować.
Jeśli nie możemy ustawiamy zmienną
public function check($strLogin, $strPassword) { informującą jak długo konto pozosta-
}
nie jeszcze zablokowane i zwracamy
public function getID() { błąd. Wszystko wydaje się być popraw-
} ne. Nawet w przypadku logowania, czy-
ścimy wpisy w bazie o nieudanych pró-
public function getName() { bach logowania, tak aby były one zli-
}
czane od początku. A więc uruchom-
public function getSurname() { my test. I co się stało? Okazuje się że
} klasa nadal nie przechodzi trzeciego te-
stu – metoda check w momencie poda-
public function getLastLogin() { nia prawidłowego hasła po trzech nie-
}
udanych próbach logowania na kon-
public function getLockFor() { ta zwraca wartość Authorization::OK.,
} czyli pozwala się zalogować na konto
które powinno być zablokowane. Po-
} wód? Spełnione zostały pierwsze dwa
?>
warunki – istnieje użytkownik o zada-
Listing 6. Polecenie SQL tworzące odpowiednią tabelę w bazie MySQL nym loginie, oraz podanym haśle. Me-
toda nie sprawdza więc już czy konto
CREATE TABLE users ( nie jest przypadkiem zablokowane. Zo-
iduser INT NOT NULL auto_increment PRIMARY KEY,
bacz Rysunek 4.
name VARCHAR(50) NOT NULL,
surname VARCHAR(80) NOT NULL,
Miejsce błędu zostało odnalezione
login VARCHAR(50) NOT NULL UNIQUE, bardzo szybko dzięki zastosowaniu te-
password CHAR(32) NOT NULL, stów. Na szczęście naprawienie błędu
ip BIGINT DEFAULT NULL, jest bardzo proste. Wystarczy zmodyfiko-
lastlogin INT NOT NULL DEFAULT 0,
wać warunek:
lasttry INT NOT NULL DEFAULT 0,
trycount INT NOT NULL DEFAULT 0,
); if (strcmp($arrUser['password'],
md5($strPassword)) === 0) {
Listing 7. Dodajemy dane do tabeli
metody check, tak aby sprawdzał on tak-
INSERT INTO users (iduser, name, surname, login, password, ip, lastlogin,
lasttry, trycount) VALUES (101,'Jan','Kowalski','test', że liczbę nieudanych prób logowania
'e10adc3949ba59abbe56e057f20f883e',NULL,0,0,0), (102,'Jan', oraz czy ewentualnie zablokowane kon-
'Kowalski','testIP','e10adc3949ba59abbe56e057f20f883e', to można już odblokować (czyli czy upły-
-943208505,0,0,0); nął już czas na jaki konto zostało zablo-
kowane)

PHP Solutions Nr 6/2006 www.phpsolmag.org 71


Technika PHPUnit2

if ((strcmp($arrUser['password'],
md5($strPassword)) === 0) &&
(($arrUser['trycount']
<$this->intLockAfter)
|| ($arrUser['lasttry']+$this->
intLockTime<time()))) {

Dzięki tej zmianie, nasza klasa przechodzi


zaprojektowane wcześniej testy.
Najważniejsza klasa naszego proste-
go pakietu jest już gotowa i według na-
szych testów działa poprawnie. Jednak
Rysunek 3. Klasa Authorization przeszła drugi test warto by zastanowić się czy nie da się jej
przetestować dokładniej. Pomyśl nad tym
Listing 8. Wstępna postać metody check klasy Authorization i jeśli masz jakiś pomysł zbuduj dodatko-
we testy.
public function check($strLogin, $strPassword) { Czas zabrać się za budowę te-
$strQuery = 'SELECT * FROM users WHERE login = :login'; stów klasy User. Klasa ta powinna za-
$objStament = $this->objDBConnection->prepare($strQuery);
wierać trzy metody: dodającą użytkow-
$objStament->bindParam(':login', $strLogin, PDO::PARAM_STR, 50);
if ($objStament->execute()===false) { nika do bazy – add, usuwającą użyt-
return false; kownika z bazy – remove, sprawdzają-
} cą poprawność podanego loginu i hasła
$arrUser = $objStament->fetchAll(); – login. Metodę add proponuję zbudo-
if (count($arrUser) === 1) {
wać jako statyczną, dzięki temu będzie
$arrUser = $arrUser[0];
if (strcmp($arrUser['password'], md5($strPassword)) === 0) { można dodać użytkownika bez tworze-
$this->strName = $arrUser['name']; nia instancji danej klasy. Na Listingu 9
$this->strSurname = $arrUser['surname']; znajduje się proponowany zbiór testów
return self::OK; naszej klasy. Teraz sam spróbuj na ich
} else {
podstawie zbudować w pełni działającą
return self::ERROR_UNAUTHORIZED;
} klasą, tj. taką, która przejdzie wszystkie
} testy z Listingu 9.
return self::ERROR_UNAUTHORIZED;
} Łączenie
Listing 9. Metoda check przechodząc 3 z 4 testów
testów w pakiety
W rzeczywistości każda aplikacja skła-
public function check($strLogin, $strPassword) { da się z wielu klas. Często jest ich na-
$strQuery = 'SELECT * FROM users WHERE login = :login'; wet kilkaset. Niezbyt wygodne wyda-
$objStament = $this->objDBConnection->prepare($strQuery);
je się być uruchamianie testu każdej z
$objStament->bindParam(':login', $strLogin, PDO::PARAM_STR, 50);
if ($objStament->execute()===false) {
nich osobno, po wprowadzeniu zmian
return false; w aplikacji – można co prawda napi-
} sać sobie skrypt (np. w bash na syste-
$arrUser = $objStament->fetchAll(); mach unix-owych lub plik .bat na Win-
if (count($arrUser) === 1) {
dowsach) który będzie uruchamiał po
$arrUser = $arrUser[0];
if (strcmp($arrUser['password'], md5($strPassword)) === 0) {
kolei wszystkie testy, jednak analizo-
if ($arrUser['ip']!==null) { wanie wyników będzie wówczas utrud-
if (!isset($_SERVER['REMOTE_ADDR']) || strcmp(long2ip($arrUser[ nione. Najlepiej by było gdyby PHPU-
'ip']), $_SERVER['REMOTE_ADDR']) !== 0) { nit2 dostarczał możliwości połączenia
$this->updateTry($arrUser['iduser'], $arrUser['trycount']+1);
wszystkich testów jednostkowych w je-
return self::ERROR_IP;
}
den pakiet, traktowanych jak test całej
} aplikacji. Na szczęście twórcy PHPU-
$this->strName = $arrUser['name']; nit2 przewidzieli taką możliwość. Spró-
$this->strSurname = $arrUser['surname']; bujmy więc połączyć oba testy naszych
return self::OK;
klas w jeden pakiet. Musimy w tym ce-
} else {
return self::ERROR_UNAUTHORIZED;
lu zbudować sobie klasę – nazwijmy ją
} UserTestSuite. Klasa ta musi posiadać
} metodę suite – to ona zostanie wywo-
return self::ERROR_UNAUTHORIZED; łana przez PHPUnit2 i to w niej musi-
}
my ustalić jakie testy i w jakiej kolejno-
ści chcemy przeprowadzić. Na Listin-

72 www.phpsolmag.org PHP Solutions Nr 6/2006


PHPUnit2 Technika

Listing 10. Niemal poprawna klasa Authorization – wydawało się że klasa powinna działać poprawnie, tymczasem okazuje się,
że nie przechodzi ona trzeciego testu

<?php >intLockAfter && ($arrUser['lasttry'


class Authorization { ]+$this->intLockTime)<time()) {
const OK = 0; $this->updateTry($arrUser['iduser']);
const ERROR_UNAUTHORIZED = 1; } else {
const ERROR_3TIMES = 2; $this->updateTry($arrUser['iduser'],
const ERROR_IP = 3; $arrUser['trycount']+1);
private $objDBConnection; }
private $intLockAfter = 3; return self::ERROR_UNAUTHORIZED;
private $intLockTime = 180; }
private $strName = null; }
private $strSurname = null; }
private $intLastLogin = null;
private $intLockFor = null; return self::ERROR_UNAUTHORIZED;
}
public function __construct(PDO $objDBConnection) {
$this->objDBConnection = $objDBConnection; public function getID() {
} return $this->intIDUser;
}
public function check($strLogin, $strPassword) {
$strQuery = 'SELECT * FROM users WHERE login public function getName() {
= :login'; return $this->strName;
$objStament = $this->objDBConnection-> }
prepare($strQuery);
$objStament->bindParam(':login', $strLogin, public function getSurname() {
PDO::PARAM_STR, 50); return $this->strSurname;
}
if ($objStament->execute()===false) {
return false; public function getLastLogin() {
} return $this->intLastLogin;
}
$arrUser = $objStament->fetchAll();
public function getLockFor() {
if (count($arrUser) === 1) { return $this->intLockFor;
$arrUser = $arrUser[0]; }

if (strcmp($arrUser['password'], private function updateLogin($intIDUser) {


md5($strPassword)) === 0) { $strQuery = 'UPDATE users SET lastlogin=:lastlogin,
trycount=0, lasttry=0 WHERE iduser=:iduser';
if ($arrUser['ip']!==null) { $objStament = $this->objDBConnection->
prepare($strQuery);
if (!isset($_SERVER['REMOTE_ADDR']) || $objStament->bindParam(':lastlogin', time(),
strcmp(long2ip($arrUser['ip']), PDO::PARAM_INT);
$_SERVER['REMOTE_ADDR']) !== 0) { $objStament->bindParam(':iduser', $intIDUser,
$this->updateTry($arrUser['iduser'], PDO::PARAM_INT);
$arrUser['trycount']+1); $objStament->execute();
return self::ERROR_IP; $objStament->fetchAll();
} }
}
private function updateTry($intIDUser, $intCount=1) {
$this->updateLogin($arrUser['iduser']); $strQuery = 'UPDATE users SET trycount=:trycount,
$this->intIDUser = intval($arrUser['iduser']); lasttry=:lasttry WHERE iduser=:iduser';
$this->strName = $arrUser['name']; $objStament = $this->objDBConnection->
$this->strSurname = $arrUser['surname']; prepare($strQuery);
$this->intLastLogin = intval( $objStament->bindParam(':lasttry', time(),
$arrUser['lastlogin']); PDO::PARAM_INT);
return self::OK; $objStament->bindParam(':trycount', $intCount,
} else { PDO::PARAM_INT);
if ($arrUser['trycount']>=$this->intLockAfter $objStament->bindParam(':iduser', $intIDUser,
&& ($arrUser['lasttry']+ PDO::PARAM_INT);
$this->intLockTime)>time()) { $objStament->execute();
$this->intLockFor = intval(((($arrUser[ $objStament->fetchAll();
'lasttry']+$this->intLockTime)- }
time())/60)+1); }
return self::ERROR_3TIMES; ?>
} else {
if ($arrUser['trycount']>=$this-

PHP Solutions Nr 6/2006 www.phpsolmag.org 73


Technika PHPUnit2

Listing 10. Niemal poprawna klasa Authorization – wydawało się że klasa powinna działać poprawnie, tymczasem okazuje się,
że nie przechodzi ona trzeciego testu

<?php $this->intLockTime)<time()) {
class Authorization { $this->updateTry($arrUser['iduser']);
const OK = 0; } else {
const ERROR_UNAUTHORIZED = 1; $this->updateTry($arrUser['iduser'],
const ERROR_3TIMES = 2; $arrUser['trycount']+1);
const ERROR_IP = 3; }
private $objDBConnection; return self::ERROR_UNAUTHORIZED;
private $intLockAfter = 3; }
private $intLockTime = 180; }
private $strName = null; }
private $strSurname = null;
private $intLastLogin = null; return self::ERROR_UNAUTHORIZED;
private $intLockFor = null; }

public function __construct(PDO $objDBConnection) { public function getID() {


$this->objDBConnection = $objDBConnection; return $this->intIDUser;
} }

public function check($strLogin, $strPassword) { public function getName() {


$strQuery = 'SELECT * FROM users WHERE login = return $this->strName;
:login'; }
$objStament = $this->objDBConnection->
prepare($strQuery); public function getSurname() {
$objStament->bindParam(':login', $strLogin, return $this->strSurname;
PDO::PARAM_STR, 50); }

if ($objStament->execute()===false) { public function getLastLogin() {


return false; return $this->intLastLogin;
} }

$arrUser = $objStament->fetchAll(); public function getLockFor() {


if (count($arrUser) === 1) { return $this->intLockFor;
$arrUser = $arrUser[0]; }

if (strcmp($arrUser['password'], private function updateLogin($intIDUser) {


md5($strPassword)) === 0) { $strQuery = 'UPDATE users SET lastlogin=:lastlogin,
trycount=0, lasttry=0 WHERE iduser=
if ($arrUser['ip']!==null) { :iduser';
$objStament = $this->objDBConnection->
if (!isset($_SERVER['REMOTE_ADDR']) || prepare($strQuery);
strcmp(long2ip($arrUser['ip']), $objStament->bindParam(':lastlogin', time(),
$_SERVER['REMOTE_ADDR']) !== 0) { PDO::PARAM_INT);
$this->updateTry($arrUser['iduser'], $objStament->bindParam(':iduser', $intIDUser,
$arrUser['trycount']+1); PDO::PARAM_INT);
return self::ERROR_IP; $objStament->execute();
} $objStament->fetchAll();
} }

$this->updateLogin($arrUser['iduser']); private function updateTry($intIDUser, $intCount=1) {


$this->intIDUser = intval($arrUser['iduser']); $strQuery = 'UPDATE users SET trycount=:trycount,
$this->strName = $arrUser['name']; lasttry=:lasttry WHERE iduser=:iduser';
$this->strSurname = $arrUser['surname']; $objStament = $this->objDBConnection->
$this->intLastLogin = intval( prepare($strQuery);
$arrUser['lastlogin']); $objStament->bindParam(':lasttry', time(),
return self::OK; PDO::PARAM_INT);
} else { $objStament->bindParam(':trycount', $intCount,
if ($arrUser['trycount']>=$this->intLockAfter PDO::PARAM_INT);
&& ($arrUser['lasttry']+$this-> $objStament->bindParam(':iduser', $intIDUser,
intLockTime)>time()) { PDO::PARAM_INT);
$this->intLockFor = intval(((($arrUser[ $objStament->execute();
'lasttry']+$this->intLockTime)- $objStament->fetchAll();
time())/60)+1); }
return self::ERROR_3TIMES;
} else { }
if ($arrUser['trycount']>=$this-> ?>
intLockAfter && ($arrUser['lasttry']+

74 www.phpsolmag.org PHP Solutions Nr 6/2006


Technika PHPUnit2

Listing 11. Klasa testująca klasę User


<?php $this->assertTrue($blnResult);
$strQuery = 'SELECT * FROM users WHERE login =
require('../classes/User.class.php'); :login';
require('DBConnection.php'); $objStament = $this->objDBConnection->
prepare($strQuery);
class UserTest extends PHPUnit2_Framework_TestCase { $strLogin = 'testIP';
protected $objUser = null; $objStament->bindParam(':login', $strLogin,
protected $objDBConnection = null; PDO::PARAM_STR);
$objStament->execute();
public function setUp() { $arrResult = $objStament->fetch(PDO::FETCH_ASSOC);
$this->objDBConnection = DBConnection:: $arrParams['password'] = md5($arrParams['password']);
getDBConnection(); $arrParams['ip'] = ip2long($arrParams['ip']);
$this->objUser = new User($this->objDBConnection); $intArrCount = count(array_diff($arrParams,
} $arrResult));
$this->assertTrue($intArrCount==0,
public function testAddUser1() { 'Dodano niepoprawne dane');
$arrParams = array( }
'name' => 'Jan',
'surname' => 'Kowalski', public function testRemoveUser1() {
'login' => 'test', $this->objUser->remove('test', '123456');
'password' => '123456', $strQuery = 'SELECT * FROM users WHERE login =
); :login';
$objStament = $this->objDBConnection->
$blnResult = $this->objUser->add($arrParams); prepare($strQuery);
$this->assertTrue($blnResult); $strLogin = 'test';
$strQuery = 'SELECT * FROM users WHERE login = $objStament->bindParam(':login', $strLogin,
:login'; PDO::PARAM_STR);
$objStament = $this->objDBConnection-> $objStament->execute();
prepare($strQuery); $arrResult = $objStament->fetchAll();
$strLogin = 'test'; $this->assertTrue(count($arrResult)==0,
$objStament->bindParam(':login', $strLogin, 'Nie usuni─Öto usera');
PDO::PARAM_STR); }
$objStament->execute();
$arrResult = $objStament->fetch(PDO::FETCH_ASSOC); public function testRemoveUser2() {
$arrParams['password'] = md5($arrParams['password']); $this->objUser->remove('testIP', '123456');
$intArrCount = count(array_diff($arrParams, $strQuery = 'SELECT * FROM users WHERE login =
$arrResult)); :login';
$this->assertTrue($intArrCount==0, $objStament = $this->objDBConnection->
'Dodano niepoprawne dane'); prepare($strQuery);
} $strLogin = 'testIP';
$objStament->bindParam(':login', $strLogin,
public function testAddUser2() { PDO::PARAM_STR);
$arrParams = array( $objStament->execute();
'name' => 'Jan', $arrResult = $objStament->fetchAll();
'surname' => 'Kowalski', $this->assertTrue(count($arrResult)==0,
'login' => 'testIP', 'Nie usunięto usera testIP');
'password' => '123456', }
'ip' => '199.199. 199.199'
); }

$blnResult = $this->objUser->add($arrParams); ?>

gu 10 znajduje się pełna klasa nasze- Plik zawierający klasę z pakietem mu- Testy
go pakietu. si posiadać nazwę klasy i rozszerzenie niekompletne,
Test buduje się tworząc instancję .php. anulowanie testu
klasy PHPUnit2_Framework_TestSuite. Uruchomienie pakietu testów przebie- Jeśli niektóre z testów nie są jeszcze ukoń-
Następnie korzystając ze stworzonego ga analogicznie do uruchomienia pojedyn- czone, można poinformować o tym frame-
obiektu możemy dodawać kolejne testy czego testu: work, zwracając w metodzie testu wyjątek
do naszego pakietu. Służy do tego me- PHPUnit2_Framework_IncompleteTestError.
toda addTest(). Jako parametr tej me- phpunit UsertTestSuite W jego konstruktorze można opcjonalnie
tody podajemy obiekt testu który doda- podać dodatkową informację:
jemy (konstruktor klasy przyjmuje na- Na Rysunku 5 znajduje się przykła-
zwę metody z testem który chcemy uru- dowy wynik działania pakietu z Listin- throw new PHPUnit2_Framework_
chomić). gu 10. IncompleteTestError('

76 www.phpsolmag.org PHP Solutions Nr 6/2006


PHPUnit2 Technika

phpunit --testdoc-html doc.html


UserTestSuite
phpunit --testdoc-text doc.txt
UserTestSuite

Jeśli natomiast mamy już gotową kla-


sę dla której chcemy przygotować te-
sty, wystarczy wydać następujące pole-
cenie w katalogu w którym znajduje się
nasza klasa:

phpunit --skeleton NazwaKlasy


Rysunek 4. Wynik testu przeprowadzonego na klasie z Listingu 8 jednoznacznie
wskazuje miejsce wystąpienia błędu
Spowoduje to wygenerowanie szkieletu
klasy testowej o nazwie NazwaKlasyTest.
PHPUnit2 wygeneruje zbiór metod, które
powinieneś wypełnić kodem, tak aby prze-
testowały każdą z metod testowanej klasy.
Wygenerowane metody możesz swobod-
nie modyfikować – od zmiany ich nazwy aż
po całkowite usunięcie.

Podsumowanie
Mam nadzieję, że artykuł ten przyczynił
się do zainteresowania Cię testami jed-
nostkowymi. Mimo iż wiele osób na po-
Rysunek 5. Wynik działania pakietu testów z Listingu 10 – plik UserTestSuit.php
czątku podchodzi do nich bardzo scep-
Test ten nie został jeszcze $this->markTestSkipped tycznie i nie chce „tracić czasu” na ich
ukończony'); ('Wymagane rozszerzenie XYZ jest budowę, to jednak po kilku próbach sa-
niedostępnę!'); mi oni stwierdzają że to jest to. Aplika-
Jeśłi chcesz skorzystać z klasy wyjątku cje internetowe bardzo często szybko
PHPUnit2_Framework_IncompleteTestError, Inne możliwości się rozwijają.
musisz dołączyć do klasy testu następu- PHPUnit2 Dodawanie nowych elementów po-
jący skrypt, wchodzący w skład frame- To co zostało zaprezentowane w tym woduje wzrost prawdopodobieństwa
worka PHPUnit2 - PHPUnit2/Framework/ artykule to jedynie wycinek możliwości pojawienia się w nowych fragmentach
IncompleteTestError.php. PHPUnit2. Framework ten pozwala na błędów, które mogą się okazać trudne
przykład sprawdzić nasze testy pod kon- do odnalezienia, a co gorsze mogą po-
require_once(‘PHPUnit2/Framework/ tem pokrycia testami testowanej klasy. wodować błędne działanie innych, sta-
IncompleteTestError.php’); Wystarczy wówczas nieco zmodyfiko- rych, części aplikacji. W takich wypad-
wać wywołanie phpunit: kach testy jednostkowe bardzo często
Częściej spotykaną sytuacją jest zależ- pomagają w szybkim wyłapaniu błę-
ność wykonania testu od zainstalowa- phpunit --coverage-html coverage.html dów, które w innym wypadku mogły by
nia jakiegoś rozszerzenia do PHP, czy UserTestSuite się ujawnić po pewnym czasie dopiero
obecności w systemie dodatkowych bi- u klienta. n
bliotek, wymaganych do działania da- Dzięki temu PHPUnit2 wygeneruje podsu-
nej funkcjonalności projektowanej kla- mowanie na temat przetestowanych frag-
sy, lub od wcześniejszego, poprawne- mentów naszej klasy, w postaci strony in- O autorze
go wykonania się innych testów. Jeśli ternetowej. Jednak aby strona ta była dla
funkcjonowanie pewnych elementów nas czytelna, musimy przygotować so- Marcin Staniszczak jest studentem
pierwszego roku informatyki studiów
naszej klasy jest zależne od czynników bie plik CSS, formatujący ją w odpowied- uzupełniających magisterskich na
zewnętrznych, oraz gdy ich obecność ni sposób. WSHE w Łodzi. W PHP programuje
możemy zbadać z poziomu testów, mo- PHPUnit2 pozwala zapisać wy- od wielu lat. Jest autorem kilkunastu
żemy w razie konieczności poinformo- nik pokrycia kodu także w pliku txt, lub publikacji o tematyce PHP i Interne-
towej. Jest autorem framework MVC
wać PHPUnit2 że chcemy dany test po- w serializowanej tablicy, którą z łatwo- dla PHP5 (web.framework) oraz sys-
minąć (niestety ta metoda działa jedy- ścią można obrabiać z poziomu skryp- temu szablonów dla PHP5 (web.tem-
nie w wersji 3.0.0, która nie jest jesz- tów PHP. plate).
cze stabilna i nie jest w związku z tym PHPUnit2 pozwala także na łatwe wy-
Kontakt z autorem:
standardowo pobierana w czasie insta- generowanie prostej dokumentacji testu w marcin@staniszczak.pl
lacji z PEAR): formacie HTML lub txt:

PHP Solutions Nr 6/2006 www.phpsolmag.org 77


Felieton

Paskudne technologie
Michał Małecki

Światową gospodarką rządzi biznes. To skądinąd banalne stwierdzenie do


dużej części ludzi zaangażowanych w jej rozwój dociera tak trochę nie do
końca. Fakty z tego wynikające są przyjmowane z trudem, zwłaszcza gdy
są nie po czyjejś myśli.

K
toś, kto siedział przez dłuższy czas Czytelność to przykład, i to jeszcze a rozwija się, bo ma kto w niego inwestować.
w biznesie związanym z oprogramo- bardzo praktyczny. Gorzej z kryteriami ty- Tak przecież stało się z komputerem IBM
waniem, zauważył już na pewno, ja- pu „piękno”, „estetyka”, czy „elegancja”. Co PC, który u swojego zarania (XT/AT) był naj-
ka jest różnica między informatyką, a inżynie- prawda ostatnio jak spytałem o to, czy mo- gorszym szmelcem wśród sprzętu kompute-
rią oprogramowania. Inżynieria, nie istniałaby że to być ścisłe kryterium, zostałem odesła- rowego, ale dzięki otwartości pokonał znacz-
bez nauki. Ale dopiero inżynieria oprogramo- ny do jakichś stron na Wikipedii (np. Mathe- nie lepsze od siebie komputery Amiga i Ata-
wania jest w stanie uzasadnić, czy ten rozwój matical beauty), które ze śmiertelną powagą ri ST, o których dziś chyba mało kto pamięta,
informatyki rozwojowi cywilizacji, czy też jest twierdzą, że matematycy czerpią estetycz- a i na „ogryzka” bym nie stawiał. Wcale nie
jedynie twórczym wysysaniem sobie z pal- ną przyjemność ze swojej pracy. Co prawda jest powiedziane, że rozwój tego „gorszego”
ca. W końcu środowiska naukowe potrze- już samo porównanie piękna liczb do piękna w efekcie kosztował więcej, niż kosztowałby
bują funduszy do swojego działania, a jeśli 9-tej symfonii Beethovena powinno sprowa- rozwój tego „lepszego”. Jak przemysł nie do-
nowe technologie opracowane w tych śro- dzić na ziemię tych, którym się wydaje, że te rósł do określonych rozwiązań, to widocznie
dowiskach nie dają się wykorzystać do ro- kryteria mają coś wspólnego z jakąkolwiek przemysłowi jest z tym dobrze. Tak jak to było
bienia pieniędzy, to nie ma mowy o pienią- inżynierią, ale to wielu nie przeszkadza na- z maszyną parową, która została wynalezio-
dzach na badania. To z kolei, potrafi w wie- rzekać na wszystkie współcześnie używa- na już w starożytnym Egipcie, ale praktyczne
lu wypadkach prowadzić do zaniechania ba- ne w inżynierii języki programowania, że są zastosowanie znalazła dopiero w XIX wieku,
dań nad tym, co potencjalnie mogłoby ca- paskudne. Oto na przykład czytam, jakim bo wcześniej nie było na nią zapotrzebowa-
łemu „biznesowi” przynieść korzyści, ale ja- to wspaniale eleganckim językiem jest Pro- nia. Dlatego też w technologiach stosowa-
ki jest „potencjał” tych korzyści nikt nie potra- log. Popatrzyłem na podpowiedziane przy- nych w przemyśle gorąco polecam dale-
fi ocenić. A przedsiębiorcy najczęściej ryzyku- kłady aplikacji GUI w Prologu i stwierdziłem, ko posunięty konformizm i trzymanie się z
ją wystarczająco dużo, żeby w kwestii tech- że... są straszliwie nieczytelne. Gdzie im do daleka od wszelkich ideologii. Nie oznacza
nologii pozwolić sobie na taktykę „lepszy wró- takiego np. Tk! I co, mam znów powtarzać to oczywiście, że nie należy być twórczym
bel w garści”. to banalne i oklepane stwierdzenie "kwe- i nie proponować dobrych pomysłów. Ale
W każdym zawodzie istnieje kwestia stia gustu"? Przemysłu to i tak nie interesu- trzeba pamiętać, że „każdy chce robić to,
„własnych upodobań” co do narzędzi pracy je, bo w przemyśle używa się takich techno- co chce, ale chce, żeby za to, że on robi
i informatycy nie są wyjątkiem. Prowadzi to logii, jakie przynoszą zysk, a nie takich, jakie to, co chce, płacili mu inni, którzy nie chcą,
często do dyskusji, w których rozdział mię- są „eleganckie”. A robienie interesów to nie żeby on robił to, co chce, ale chce, żeby
dzy własnym gustem a argumentami natury jest czysta i piękna sprawa, tylko kombino- robił to, czego oni chcą”. n
technicznej jest często bardzo płynny.Każdy wanie i robienie wszystkich dookoła w bam-
wie, że takie kryterium istnieje, każdy intuicyj- buko, dokładnie tak, jak to robi Microsoft,
nie wyczuwa, że jakoś-tam to jest określone, czy Sun. I wygrywa ten, komu się ta sztuka
ale na pytanie, czy można zrobić jakiś wery- uda, a nie ten, kto stworzy lepszą, czy bar- O autorze
fikator kodu, który sprawdzałby czytelność dziej elegancką technologię.
Autor interesuje się psychologią, progra-
programu, każdy programista odpowie jed- To oczywiście powoduje, że często „pa-
mowaniem, muzyką, publicystyką i ję-
noznacznie przecząco. Oczywiście, mamy skudne technologie zwyciężają” i biorą gó- zykoznawstwem, a w wolnych chwilach
wiele narzędzi do weryfikacji kodu, różne lin- rę nad tymi, które są „estetyczne” i „eleganc- pracuje etatowo jako programista.
ty, klocworki itp. bajery, ale jeśli ktoś sądzi, że kie”, a gorsze technologicznie narzędzia bio-
Kontakt z autorem:
istnieje jakiś związek pomiędzy np. złożono- rą górę nad lepszymi. Oczywiście to gorsze
ethouris@gmail.com
ścią cyklomatyczną, a czytelnością kodu... za jakiś czas staje się lepsze, bo się rozwija,

78 www.phpsolmag.org PHP Solutions Nr 6/2006


d e d s i t e s > > >
Recommen
Jeśli prowadzisz ciekawą stronę internetową
i chcesz abyśmy przedstawili ją w ramach akcji Recommended sites,
skontaktuj się z nami pod adresem redakcja@phpsolmag.org

Serwis poświęcony zagadnieniom związanym z


komputerami klasy PC, choć nie tylko. Prezen-
Dziennik Internautów – czyli gazeta dla użyt- towane są tam informacje związane z Open So-
kowników Internetu, to bogaty i dobrze zapla- urce i światem Internetu.
nowany serwis prezentujący informacje pogru- pcmaniak.pl
Serwis prezentujący prace kobiet, zajmujących powane w działy.
się zawodowo bądź dla własnej przyjemności www.di.com.pl
grafiką, webdesignem, rysunkiem, fotografią,
malarstwem, rzeźbą, itp.
womaninaction.net

Oficjalny serwis magazynu PHP Solutions.


Można stąd pobrać przykładowe artykuły, li-
Obszerny i bogaty w informacje serwis poświę- stingi z artykułów, skorzystać z forum, bądź za-
cony Sieci. Zawiera informacje dotyczące bez- poznać się z aktualnościami.
pieczeństwa, programowania, sieci bezprze- www.phpsolmag.org
Serwis poświęcony aktualnej, piątej wersji ję- wodowych i inne.
zyka PHP. Jego zwięzła i skromna forma nie www.webhat.pl
rozprasza i pozwala szybko odnaleźć szukane
informacje, które zawarto w serwisie.
www.php5.pl

Jeden z ważniejszych polskich portali dla we-


bmasterów i programistów PHP. Istnieje już cał-
Oficjalny serwis czytelników magazynu Softwa- kiem długo, co potwierdza zawartość zarówno
re Developer’s Journal. Często aktualizowany, serwisu jak i forum.
posiada wszystkie informacje przydatne czytel- webcity.pl
Oficjalny serwis magazynu Hakin9. Można nikom magazynu, ale nie tylko.
stąd pobrać darmowe artykuły, zapoznać się z www.software20.org
zawartością aktualnych i archiwalnych nume-
rów lub skorzystać z bogatego forum.
www.haking.pl

ites
Recommended s
www.buyitpress.com

Zaprenumeruj swoje ulubione magazyny


i zamów archiwalne numery!

Już teraz w kilka minut możesz zaprenumerować swoje ulubione pismo.


Gwarantujemy:
- preferencyjne ceny
- bezpieczną płatność on-line
- szybką realizację Twojego zamówienia
Bezpieczna prenumerata on-line wszystkich tytułów Wydawnictwa Software!
zamówienie prenumeraty

Prosimy wypełnić czytelnie i przesłać faksem na numer: (22) 887 10 11 lub listownie na adres: Software-Wydawnictwo Sp. z o.o.,
Bokserska 1, 02-682 Warszawa, e-mail: pren@software.com.pl. Przyjmujemy też zamówienia telefoniczne: (22) 887 14 44

Imię i nazwisko............................................................................................ ID kontrahenta..........................................................................................

Nazwa firmy................................................................................................. Numer NIP firmy.......................................................................................

Dokładny adres....................................................................................................................................................................................................................

Telefon (wraz z numerem kierunkowym)................................................... Faks (wraz z numerem kierunkowym) ....................................................

E-mail (niezbędny do wysłania faktury)............................................................................................................................................................................


automatyczne przedłużenie prenumeraty

Tytuł
Ilość Od numeru Opłata
Ilość
zamawianych pisma lub w zł
numerów
prenumerat miesiąca z VAT
Software Developer’s Journal (1 płyta CD)
– dawniej Software 2.0 12 250/1801
Miesięcznik profesjonalnych programistów
SDJ Extra (od 1 do 4 płyt CD lub DVD)
– dawniej Software 2.0 Extra! 6 150/1352
Numery tematyczne dla programistów

Linux+DVD (2 płyty DVD)


Miesięcznik o systemie Linux
12 199/1791

Linux+Extra! (od 1 do 7 płyt CD lub DVD)


Numery specjalne z najpopularniejszymi dystrybucjami Linuksa
8 232/1982

PHP Solutions (1 płyta CD)


Dwumiesięcznik o zastosowaniach języka PHP
6 135

Hakin9, jak się obronić (1 płyta CD)


Dwumiesięcznik o bezpieczeństwie i hakingu
12 1991/219

.psd (1 płyta CD + film instruktażowy)


Dwumiesięcznik użytkowników programu Adobe Photoshop
6 140

Aurox Linux (4 płyty CD + 1 płyta DVD)


Magazyn z najpopularniejszym polskim Linuksem
4 1193

Suma

Jeżeli chcesz zapłacić kartą kredytową, wejdź na


stronę naszego sklepu internetowego:
1
Cena prenumeraty rocznej dla osób prywatnych
2
Cena prenumeraty rocznej dla osób prenumerujących już Software Developer’s Journal lub Linux+
3
Cena prenumeraty dwuletniej Aurox Linux
www.buyitpress.com
W następnym numerze
PHP Solutions 1/2007 (18)

ROZWIĄZANIA
Wielojęzyczne
portale z wykorzystaniem eZ publish
Przedstawimy krok po kroku jak postawić 3-języczny
portal newsowy z wykorzystaniem eZ publish

ROZWIĄZANIA
XAJAX – łatwy AJAX dla programistów PHP
Pokażemy jak zrealizować formularz logowania, któ-
ry notyfikuje bez przeładowania o błędnym loginie
i/lub haśle. Wskażemy też gdzie, po kliknięciu zakład-
ki ładowania jest nowa strona. Oraz: dodaj/usuń do/z
ulubionych–realizujemy za pomocą AJAX.

NARZĘDZIA
Flashowe interfejsy WWW,
czyli PHP i Action Script w akcji
Pokażemy, jak wykorzystać najnowsze możliwości
Flasha do budowania zaawansowanych GUI dla apli-
kacji PHP. Przedstawimy zalety programowania zda-
rzeniowego i frameworka AsWing.

Ponadto planujemy: W sprzed


aży
od 15 gru
■ Ebay w PHP Solutions, dnia!
■ Nowinki i rozwiązania dla płatności
internetowych

■ Kasa z Twojej strony,

■ Testy konsumenckie firm hostingowych.

You might also like