O autorach ...................................................................................................................................11 O korektorze merytorycznym .......................................................................................................13 Przedmowa ..................................................................................................................................15 Wprowadzenie do PHP ................................................................................................................17 Rozdział 1.
Obiektowość ..........................................................................................................21 Klasy ....................................................................................................................................................... 21 Dziedziczenie i przeciążanie ............................................................................................................... 23 „Magiczne” funkcje ............................................................................................................................. 27 Metody __get i __set .................................................................................................................... 27 Metoda __isset ............................................................................................................................... 28 Metoda __call ................................................................................................................................ 28 Metoda __toString() ..................................................................................................................... 29 Kopiowanie, klonowanie oraz porównywanie obiektów ............................................................... 29 Interfejsy, iteratory i klasy abstrakcyjne ........................................................................................... 31 Kontekst klasy i elementy statyczne .................................................................................................. 35 Podsumowanie ..................................................................................................................................... 36
Mobilne PHP ..........................................................................................................47 Różnorodność urządzeń ..................................................................................................................... 47 Rozpoznanie urządzenia ..................................................................................................................... 48 Aplikacja kliencka ......................................................................................................................... 48 Wbudowane funkcje PHP ........................................................................................................... 48 Rozpoznawanie możliwości urządzenia ........................................................................................... 51 WURFL .......................................................................................................................................... 51 Tera-WURFL ................................................................................................................................. 57
PHP. ZAAWANSOWANE PROGRAMOWANIE
Narzędzia renderujące ........................................................................................................................ 60 WALL ............................................................................................................................................. 60 Reagujący CSS ............................................................................................................................... 62 Emulatory i SDK .................................................................................................................................. 62 Tworzenie dla systemu Android ................................................................................................ 62 Adobe Flash Builder dla PHP ..................................................................................................... 62 Kody QR ................................................................................................................................................ 63 Podsumowanie ..................................................................................................................................... 64
Rozdział 4.
Media społecznościowe .........................................................................................65 OAuth .................................................................................................................................................... 65 Twitter ................................................................................................................................................... 66 API publicznego wyszukiwania .................................................................................................. 66 Prywatne REST API ..................................................................................................................... 67 Wykorzystanie mechanizmu OAuth w celu powiązania strony z systemem logowania ........77 Dodatkowe metody API i przykłady jego wykorzystania ....................................................... 80 Facebook ............................................................................................................................................... 83 Dodanie linku wylogowania z Facebooka ................................................................................. 88 Żądanie dodatkowych uprawnień .............................................................................................. 89 Graph API ...................................................................................................................................... 89 Podsumowanie ..................................................................................................................................... 91
Tworzenie formularzy i zarządzanie nimi ............................................................107 Walidacja danych ............................................................................................................................... 107 Wczytywanie plików i obrazów ....................................................................................................... 113 Konwersja obrazów i miniatury ...................................................................................................... 114 Wyrażenia regularne ......................................................................................................................... 115 Integracja języków ............................................................................................................................. 118 Podsumowanie ................................................................................................................................... 119
Rozdział 7.
Integracja z bazami danych. Część I ....................................................................121 Wprowadzenie do MongoDB .......................................................................................................... 122 Zapytania w MongoDB .............................................................................................................. 126 Modyfikowanie dokumentów w MongoDB ........................................................................... 130 Agregacje w MongoDB .............................................................................................................. 132 Podsumowanie MongoDB ........................................................................................................ 134
6
SPIS TREŚCI
Wprowadzenie do CouchDB ........................................................................................................... 134 Wykorzystanie interfejsu Futon ............................................................................................... 135 Podsumowanie CouchDB ......................................................................................................... 140 Wprowadzenie do SQLite ................................................................................................................. 141 Podsumowanie SQLite ............................................................................................................... 149 Podsumowanie ................................................................................................................................... 149
Rozdział 8.
Integracja z bazami danych. Część II ...................................................................151 Wprowadzenie do rozszerzenia MySQLi ....................................................................................... 151 Podsumowanie rozszerzenia MySQLi ..................................................................................... 158 Wprowadzenie do PDO .................................................................................................................... 158 Podsumowanie PDO .................................................................................................................. 161 Wprowadzenie do ADOdb ............................................................................................................... 161 Podsumowanie ADOdb ............................................................................................................. 165 Wyszukiwanie pełnotekstowe przy wykorzystaniu Sphinksa ..................................................... 165 Podsumowanie ................................................................................................................................... 173
Rozdział 9.
Integracja z bazami danych. Część III ..................................................................175 Wprowadzenie do Oracle ................................................................................................................. 175 Podstawy. Połączenie i wykonywanie zapytań .............................................................................. 177 Interfejs tablicowy .............................................................................................................................. 180 Procedury i kursory w PL/SQL ........................................................................................................ 183 Praca z typami LOB ........................................................................................................................... 186 Inne podejście do połączeń — pule połączeń ................................................................................ 190 Zestawy znaków w bazie danych i PHP .......................................................................................... 192 Podsumowanie ................................................................................................................................... 193
Rozdział 10. Biblioteki .............................................................................................................195 SimplePie ............................................................................................................................................. 196 TCPDF ................................................................................................................................................. 199 Pobieranie danych ze stron internetowych ............................................................................. 204 Integracja z Mapami Google ............................................................................................................ 209 Wiadomości e-mail i SMS ................................................................................................................ 211 gChartPHP — biblioteka wykorzystująca Google Chart API ..................................................... 215 Podsumowanie ................................................................................................................................... 219
Rozdział 11. Bezpieczeństwo ...................................................................................................221 Nigdy nie ufaj danym ........................................................................................................................ 221 register_globals ........................................................................................................................... 222 Białe i czarne listy ....................................................................................................................... 222 Dane formularzy ......................................................................................................................... 223 $_COOKIES, $_SESSION i $_SERVER .................................................................................. 224 Żądania Ajax ................................................................................................................................ 224 Powszechne ataki ............................................................................................................................... 225 Polityka tego samego pochodzenia .......................................................................................... 225 XSS (Cross Site Scripting) .......................................................................................................... 225 CSRF (Cross-Site Request Forgery) ......................................................................................... 228 Sesje ...................................................................................................................................................... 229 Zapobieganie atakom typu SQL injection ...................................................................................... 229
Rozdział 12. Programowanie zwinne z wykorzystaniem Zend Studio dla Eclipse, Bugzilli, Mylyn i Subversion ..............................................................................................237 Zasady programowania zwinnego .................................................................................................. 237 Rajd programowania zwinnego ....................................................................................................... 238 Wprowadzenie do programu Bugzilla ............................................................................................ 239 Mylyn dla Eclipse ............................................................................................................................... 240 Bugzilla i Mylyn w połączeniu z Eclipse ......................................................................................... 242 Maksymalizowanie korzyści ............................................................................................................. 245 Podsumowanie ................................................................................................................................... 246
Rozdział 13. Refaktoryzacja, testy jednostkowe i ciągła integracja .........................................249 Refaktoryzacja .................................................................................................................................... 249 Niewielka refaktoryzacja ............................................................................................................ 250 Większy przykład ........................................................................................................................ 253 Testy jednostkowe ............................................................................................................................. 265 Ciągła integracja ................................................................................................................................. 279 Serwer ciągłej integracji ............................................................................................................. 280 System kontroli wersji ................................................................................................................ 280 Analiza statyczna ........................................................................................................................ 281 Budowanie automatyzacji .......................................................................................................... 282 Uruchomienie serwera Jenkins ................................................................................................. 282 Podsumowanie ................................................................................................................................... 285
Rozdział 14. XML .....................................................................................................................287 Podstawy XML ................................................................................................................................... 287 Schematy ............................................................................................................................................. 288 SimpleXML ......................................................................................................................................... 289 Parsowanie XML z tekstu .......................................................................................................... 289 Parsowanie XML z pliku ............................................................................................................ 290 Przestrzenie nazw ....................................................................................................................... 294 RSS ................................................................................................................................................ 296 Generowanie dokumentów XML za pomocą SimpleXML .................................................. 298 DOMDocument ................................................................................................................................. 303 XMLReader i XMLWriter ................................................................................................................ 305 Podsumowanie ................................................................................................................................... 306
Rozdział 15. JSON i Ajax ..........................................................................................................307 JSON .................................................................................................................................................... 308 PHP i JSON .................................................................................................................................. 309 Ajax ...................................................................................................................................................... 312 Tradycyjny model WWW ......................................................................................................... 313 Model Ajax ................................................................................................................................... 313 Zdarzenia synchroniczne kontra asynchroniczne ................................................................. 315 8
SPIS TREŚCI
Obiekt XMLHttpRequest ........................................................................................................... 316 Wykorzystanie obiektu XMLHttpRequest .............................................................................. 317 API JavaScript wyższego poziomu ........................................................................................... 322 Przykłady jQuery ........................................................................................................................ 322 Przesyłanie danych z Ajaksa do skryptu PHP ........................................................................ 327 Prosty program graficzny ................................................................................................................. 328 Utrzymanie stanu ....................................................................................................................... 330 Podsumowanie ................................................................................................................................... 335
Peter MacIntyre ma ponaddwudziestoletnie doświadczenie w przemyśle IT, głównie w obszarze tworzenia oprogramowania. Posiada certyfikat Zend (ZCE). Opublikował wiele prac związanych z IT, między innymi: Using Visual Objects (Que 1995), Using PowerBuilder 5 (Que 1996), ASP.NET Bible (Wiley 2001), Zend Studio for Eclipse Developer’s Guide (Sams 2008), PHP5. Programowanie (Helion 2007) i PHP: The Good Parts (O’Reilly Media 2010). Był prelegentem na międzynarodowych konferencjach, włączając w to CA-World w Nowym Orleanie, CA-TechniCon w Kolonii i CA-Expo w Melbourne. Mieszka na Wyspie Świętego Edwarda w Kanadzie, gdzie piastuje stanowisko starszego konsultanta w firmie OSSCube (www.osscube.com), będącej liderem w dziedzinie tworzenia oprogramowania open source. Pomaga w prowadzeniu Centrum Doskonalenia Zend. Można się z nim skontaktować mailowo pod adresem [email protected]. Brian Danchilla jest certyfikowanym inżynierem Zend i okazjonalnym programistą Java. Posiada licencjat w dziedzinie informatyki i matematyki. Programy komputerowe pisze już ponad połowę swojego życia, włączając w to aplikacje internetowe, analizy numeryczne oraz programy VOIP (voice over IP). Szybko wdraża się w nowe technologie i API. Jest zagorzałym czytelnikiem publikacji technicznych. Pracując jako asystent na uniwersytecie, prywatny nauczyciel i kierownik zespołów PHP Danchilla, nauczył się przekazywać swoją wiedzę. Udziela się także w ramach społeczności Stack Overflow. Kiedy nie programuje, lubi spędzać czas na świeżym powietrzu albo grać na gitarze. Mladen Gogala jest bardzo doświadczonym specjalistą w dziedzinie baz danych. Może się pochwalić długą i udaną karierą. Był administratorem bazy Oracle oraz systemów Linux i Unix, administratorem VAX/VMS, a ostatnio architektem wydajności baz danych. Od roku 1990 pracował z bazami zawierającymi terabajty danych — głównie z bazami Oracle. Zna Linuksa, Perl i PHP. PHP stało się jego ulubionym językiem na początku roku 2000. Jest autorem książki Easy Oracle PHP: Create Dynamic Web Pages with Oracle Data (Rampant TechPress, 2006). Napisał także kilka artykułów dotyczących PHP, Oracle i Symfony. Urodził się w roku 1961 w chorwackim Zagrzebiu.
PHP. ZAAWANSOWANE PROGRAMOWANIE
12
O korektorze merytorycznym
Thomas Myer jest autorem, konsultantem i programistą. Większość czasu spędza, pracując nad projektami PHP (głównie nad programami CodeIgniter, ExpressionEngine, WordPress i MojoMotor), jednak zdarza mu się także pracować z Pythonem, Perlem i Objective-C. Możesz subskrybować jego kanał na Twitterze (jeśli się odważysz): @myerman. Nie zapomnij także odwiedzić strony www.tripledogs.com, aby dowiedzieć się więcej na temat założonej przez niego firmy Triple Dog Dare Media. Aktualnie mieszka w Austin (Teksas) z żoną Hope i psami Kafką i Marlowe’em.
PHP. ZAAWANSOWANE PROGRAMOWANIE
14
Przedmowa
Ponieważ PHP rozpoczynało swój żywot jako projekt hakerów — próba opracowania łatwego i przyjemnego języka pozwalającego na tworzenie stron internetowych — nikt nie przypuszczał, że zyska ono popularność, jaką cieszy się dzisiaj. Przez lata próbowano badać powodzenie PHP różnymi metodami, sprawdzając, ile stron używa tego języka, liczbę sprzedanych książek o nim w sklepie Amazon, liczbę znaczących firm, które go wykorzystują, liczbę związanych z nim projektów, wielkość społeczności itd. Wtedy pojawił się nowy, o wiele mniej naukowy sposób. W 2008 roku, kiedy byłem w podróży poślubnej z żoną Any, mieszkaliśmy w małym hotelu o nazwie Noster Bayres, w Buenos Aires. Przybyliśmy po długim locie do zupełnie nowego miejsca, pełnego twarzy, których nigdy nie widzieliśmy. Wyobraź sobie moje zdziwienie, kiedy po wypełnieniu przez nas dokumentów recepcjonista zapytał mnie, czy to ja jestem Suraski, „ten gość od PHP”. Okazało się, że tworzył w PHP sieć społecznościową dla San Telmo. Wszystkie wskaźniki udowadniały wprawdzie duży zasięg i popularność PHP, ale to zdarzenie, mające miejsce w małym hotelu po drugiej stronie globu, przekonało mnie ostatecznie. Skoro ten recepcjonista tworzył oprogramowanie w PHP, zdecydowanie byliśmy w głównym nurcie. Obecnie, prawie trzy lata później, zaawansowana znajomość PHP jest kluczowa dla twórców aplikacji internetowych i dla wszystkich innych — w związku z ogromnym wzrostem popularności usług HTTP. PHP. Zaawansowane programowanie prezentuje kilka ważnych aspektów współczesnego programowania PHP, włączając w to obiektowość, tworzenie aplikacji mobilnych oraz skalowalne źródła danych, które mogą mieć istotny wpływ na przydatność aplikacji do pracy w chmurze. Jestem pewien, że wiedza, którą zdobędziesz, będzie dla Ciebie ważnym narzędziem i pomoże Ci wykorzystać wszystkie możliwości PHP 5.3. Szczęśliwego „pehapowania”! Zeev Suraski, CTO, Zend
PHP. ZAAWANSOWANE PROGRAMOWANIE
16
Wprowadzenie do PHP
To kolejna książka dotycząca języka PHP. Jest ona unikalna, ponieważ skupia się na zaawansowanych zagadnieniach i na nowościach. Staraliśmy się, aby była tak nowoczesna, jak to tylko możliwe w szybko zmieniającym się świecie internetu. Przeprowadzamy Czytelnika z poziomu średnio zaawansowanego do wysoko zaawansowanego użytkownika języka PHP.
Geneza PHP PHP rozpoczęło swój żywot jako projekt prowadzony przez Rasmusa Lerdorfa. W czerwcu 1995 r. Lerdorf opublikował wersję 1.0 osobistych narzędzi do tworzenia stron (ang. Personal Home Page Tools — stąd nazwa „PHP”). Była to kolekcja funkcji pozwalająca na automatyzację tworzenia i utrzymywania małych stron internetowych. Od tamtej pory PHP znacznie się rozwinęło, ewoluując do aktualnej wersji 5.3.4 (gdy powstawała książka). PHP był jednym z pierwszych języków pozwalających na tworzenie stron internetowych typu open source. Lerdorf okazał się wizjonerem, który zauważył potencjał języka, który mógłby rozwijać się razem ze społecznością internetową.
Czym jest PHP? Czym dokładnie jest PHP? Jak wygląda w aktualnej wersji? Najprościej mówiąc, PHP jest generatorem kodu HTML. Jeśli spojrzysz na kod źródłowy strony wygenerowanej za pomocą PHP, zobaczysz tylko znaczniki HTML; może jeszcze trochę skryptów JavaScript, jednak żadnego kodu PHP. Oczywiście, jest to zbytnie uproszczenie definicji języka, którego udział w rynku języków wykorzystywanych do tworzenia stron internetowych obejmuje od 39 do 59% (według różnych źródeł). Bez względu na to, którą liczbę uznasz za prawdziwą, PHP jest aktualnie najpopularniejszym językiem tworzenia i utrzymywania stron internetowych. Należy także zauważyć, że PHP jest darmowe. Tak, darmowe! Jest projektem open source. A więc jak na produkt, który nie jest monitorowany ani prowadzony przez żadną instytucję, PHP poradziło sobie świetnie, jeżeli chodzi o popularność. UWAGA. Więcej informacji dotyczących oprogramowania open source znajduje się w publikacji The Cathedral and the Bazaar Erica S. Raymonda, dostępnej pod adresem http://www.catb.org/~esr/writings/cathedral-bazaar/. Jest tam porównanie produktów tradycyjnych i open source.
PHP. ZAAWANSOWANE PROGRAMOWANIE
Aktualnie Zend Corporation jest prawdopodobnie światowym liderem PHP. Firma opracowała wiele dodatkowych produktów wspierających PHP i rozszerzających jego możliwości. Jest także głównym graczem, jeżeli chodzi o rozwój tego języka, odkąd założyciele firmy — Zeev Suraski i Andi Gutmans — podnieśli rękawicę (od PHP3). PHP jest bardzo otwartym i wyrozumiałym językiem między innymi dlatego, że jest słabo typowany. Oznacza to, że zmienne nie muszą być deklarowane z typem danych, jaki będzie w nich przechowywany — co jest konieczne w niektórych innych językach programowania. PHP próbuje raczej „wywnioskować” typ danych zapisanych w zmiennej z kontekstu i jej zawartości. Oznacza to na przykład, że zmienna o nazwie $informacje może podczas wykonywania programu przechowywać wiele różnych wartości. W niektórych przypadkach może to być wadą, ponieważ dane mogą ulegać modyfikacjom, powodując błędy w liniach, które np. oczekują liczby całkowitej, a otrzymują tekst. Programy PHP można także tworzyć w modelu zorientowanym obiektowo (OOP). Klasy, właściwości, metody, dziedziczenie, polimorfizm i enkapsulacja są częściami języka. To zwiększa możliwości powtórnego użycia kodu tworzonego w PHP oraz ułatwia korzystanie z niego. Oczywiście podejście obiektowe istnieje od długiego czasu, a PHP adaptuje i rozszerza możliwości obiektowe od kilku lat. Kolejną cenną cechą PHP jest możliwość wykonywania skryptów z poziomu wiersza poleceń (w systemach Linux i Windows), dzięki czemu język ten może być wykorzystywany w zadaniach CRON; nie musisz uczyć się innego języka programowania w celu realizacji innych zadań w środowisku serwerowym. Możesz tworzyć strony internetowe w tym samym języku, z którego korzystasz przy manipulowaniu plikami (jeżeli będziesz chciał). PHP ma także wiele punktów integracyjnych. Jest bardzo otwartym językiem. PHP służy nie tylko do tworzenia stron internetowych — może mieć wiele innych zastosowań. Połącz go z bazą danych poprzez odpowiednie rozszerzenie, a otrzymasz interfejs webowy lub nawet aplikację webową. Wykorzystaj dodatkową bibliotekę (np. tcpdf), a będziesz mógł w locie generować dokumenty PDF. Są to tylko dwa przykłady; w książce omówimy znacznie więcej dodatkowych bibliotek.
Szczegółowy przegląd treści książki Co w takim razie znajdziesz w tej książce? Staraliśmy się, aby zawierała najświeższe informacje dotyczące PHP, tak abyś umiał korzystać z najnowszych możliwości i rozszerzeń tego języka. Nie poświęcaliśmy miejsca na proste zagadnienia związane z językiem, takie jak to, czym jest zmienna lub w jaki sposób tworzyć pętle. Chcemy, abyś stał się wysoko zaawansowanym programistą oraz aby ten materiał pomógł Ci się przygotować do egzaminu certyfikacyjnego pozwalającego uzyskać tytuł ZCE (ang. Zend Certified Engineer). Poniżej znajdziesz krótkie omówienie tematyki poszczególnych rozdziałów.
Rozdział 1. Obiektowość Celem pierwszego rozdziału jest przygotowanie Cię do tego, byś mógł zrozumieć informacje i przykłady kodu, które będą prezentowane w dalszej części książki. Przedstawiamy podstawy modelu obiektowego oraz jego implementacji w PHP, następnie od razu przechodzimy do zaawansowanych zagadnień. Ważne jest, abyś dobrze zrozumiał ten rozdział, zanim przejdziesz do kolejnych.
Rozdział 2. Wyjątki i referencje W tym rozdziale omawiamy zagadnienia związane z programowaniem obiektowym i prezentujemy sposoby obsługi błędów za pomocą bloków try i catch. Jest to elegancki sposób zarządzania błędami w aplikacjach PHP — jego opanowanie daje ogromne możliwości. Następnie omawiane są referencje i ich znaczenie w odniesieniu do klas i funkcji, z których będziesz korzystał.
Rozdział 3. Mobilne PHP Urządzenia mobilne są coraz popularniejsze, stają się coraz mniejsze i wydajniejsze. W tym powiększającym się rynku swoje udziały zdobywają firmy takie jak Apple, RIM czy HTC. Do urządzeń tych są jednak potrzebne aplikacje — pokażemy w tym rozdziale, w jaki sposób PHP może przydać się w ich tworzeniu. 18
WPROWADZENIE DO PHP
Rozdział 4. Media społecznościowe Podobnie prężnie rozwijają się media społecznościowe, a PHP znacznie się do tego rozwoju przyczynia. Wiele aplikacji dostępnych na Facebooku jest napisanych właśnie w PHP. Liczne inne strony, np. Flickr, częściowo Yahoo!, a nawet aplikacje blogowe, są zależne od PHP. W tym rozdziale omówimy niektóre z interfejsów dostępnych dla integracji z portalami społecznościowymi.
Rozdział 5. Nowości technologiczne W aktualnej (gdy powstawała ta książka) wersji 5.3.4 PHP udostępnia nowe funkcje. Wiele z nich pochodzi z długo oczekiwanej wersji 6.0, ale ponieważ niektóre funkcje zostały ukończone przed innymi, początkowa kolekcja została wydana jako wersja 5.3. W tym rozdziale przeanalizujemy najlepsze nowe funkcje oraz wyjaśnimy, w jaki sposób możesz z nich skorzystać w swoich projektach.
Rozdział 6. Tworzenie formularzy i zarządzanie nimi W tym rozdziale poświęcimy trochę miejsca na omówienie funkcjonalności i technik, które mogą zostać wykorzystane przy tworzeniu formularzy. Ponadto przybliżymy kontrolowanie wpisywanych danych, reagowanie na błędnie wpisane dane (np. błędny format daty) oraz sposób przekazywania danych do aplikacji.
Rozdziały 7. i 8. Integracja z bazami danych. Część I i II Oczywiście jednym z głównych aspektów tworzenia współczesnych stron internetowych jest możliwość przechowywania i wyświetlania danych pochodzących ze źródeł danych. W tych dwóch rozdziałach pokażemy wiele sposobów manipulowania danymi. Omówimy systemy zarządzania bazami — od mniejszych baz danych, takich jak bazy NoSQL, do dużych silników bazodanowych, takich jak MySQLi. Wyjaśnimy także, jak można skorzystać z dodatkowych narzędzi, np. PDO i Sphinx.
Rozdział 9. Integracja z bazami danych. Część III PHP i Oracle łączy wyjątkowa więź, jeżeli w grę wchodzą duże kolekcje danych. W tym rozdziale omówimy sprawy dotyczące tego związku oraz podpowiemy, jak go wykorzystać.
Rozdział 10. Biblioteki Jak już wspomniano, PHP jest bardzo otwarte na inne biblioteki. W rozdziale 10. przybliżymy niektóre z nich, te najpopularniejsze i najbardziej zaawansowane. Oprócz tego zostaną poruszone między innymi: możliwość generowania dokumentów PDF, przeglądanie kanałów RSS, tworzenie profesjonalnych wiadomości e-mail, integracja z mapami Google.
Rozdział 11. Bezpieczeństwo Książka nie byłaby kompletna, gdybyśmy nie przeanalizowali najnowszych technik z dziedziny bezpieczeństwa. Na ten obszerny temat przeznaczyliśmy rozdział 11. Omawiamy w nim najbezpieczniejszy (obecnie) algorytm szyfrujący, SHA1, oraz zagadnienia związane z ochroną danych wprowadzanych w formularzach i danych udostępnianych przez system.
19
PHP. ZAAWANSOWANE PROGRAMOWANIE
Rozdział 12. Programowanie zwinne z wykorzystaniem Zend Studio dla Eclipse, Bugzilli, Mylyn i Subversion Rozdział ten nie jest ściśle związany z PHP. Wyjaśniamy w nim, jak korzystać z jednego z najpopularniejszych środowisk programistycznych (IDE) dla PHP — Zend Studio for Eclipse. Zobaczymy, w jaki sposób może współpracować zespół programistów opierający swoje działanie na koncepcji programowania zwinnego (słyszałeś o programowaniu ekstremalnym?). Omówimy używanie narzędzi SVN, Bugzilla i Mylyn oraz ich współpracę mającą na celu zwiększenie produktywności zespołu.
Rozdział 13. Refaktoryzacja, testy jednostkowe i ciągła integracja Jest to rozszerzenie poprzedniego rozdziału. Nacisk został tu położony na zagadnienia związane z poprawianiem jakości programów PHP, aczkolwiek głównym tematem rozdziału są refaktoryzacja i testy jednostkowe. Dowiesz się ponadto, jak poprawnie z nich korzystać w swojej codziennej pracy.
Rozdział 14. XML W ostatnich latach popularność XML znacznie wzrosła. W tym rozdziale wyjaśnimy, w jaki sposób wykorzystać SimpleXML do obsługi dokumentu XML pochodzącego ze źródła zewnętrznego. Powiemy także o możliwości tworzenia dokumentów XML wewnątrz systemu.
Rozdział 15. JSON i Ajax Ponownie oddalamy się nieco od czystego PHP. Tym razem przybliżymy bibliotekę JSON. Wyjaśnimy, jak używać jej z Ajaksem, by strony były przyjazne dla użytkowników.
Rozdział 16. Konkluzja W tym rozdziale prezentujemy dodatkowe źródła wiedzy dotyczącej PHP. Wymieniamy strony internetowe, magazyny oraz konferencje, dzięki którym możesz pogłębić swoje wiadomości i lepiej zrozumieć język PHP i związaną z nim społeczność.
Przyszłość PHP Ponieważ PHP jest produktem open source, trudno przewidzieć kierunek, w którym będzie rozwijane w bliskiej i dalekiej przyszłości. Bardzo wierzę jednak w społeczność. Odkąd jestem programistą PHP, nie widziałem jeszcze błędnego kroku w wykonaniu tego kolektywu. Wiem, że urządzenia mobilne będą się rozwijały. PHP już podejmuje działania, aby nadążyć za tym rozwojem. Co jeszcze stanie się w niedalekiej przyszłości? Może zostanie poprawiona integracja z telefonią, jeśli chodzi o smartfony, i kompatybilność danych. Być może rozwinie się rozpoznawanie mowy. Kto wie? Na podstawie swojego dotychczasowego doświadczenia w programowaniu w języku PHP sądzę, że społeczność będzie trzymała rękę na pulsie i że nas nie zawiedzie. Patrzenie w przyszłość PHP jest bardzo pocieszające; jest jak patrzenie na piękny wschód słońca z przekonaniem, że kolejny dzień może być tylko lepszy.
20
ROZDZIAŁ 1
Obiektowość
Rozdział ten jest wprowadzeniem do podstawowych zagadnień obiektowości. Co właściwie oznacza stwierdzenie, że PHP jest zorientowane obiektowo? Najprościej mówiąc, PHP pozwala tworzyć i porządkować hierarchicznie typy tworzone przez użytkownika. Książka ta dotyczy PHP 5.3, które wprowadza nowe elementy do repertuaru narzędzi obiektowych. PHP przeszło radykalne zmiany od wersji 4., która także zawierała podstawowe mechanizmy obiektowości. W wersji 4. nie można było na przykład definiować widoczności metod i właściwości. W PHP 5.3 zostały dodane przestrzenie nazw. Omówimy tu klasy, dziedziczenie, tworzenie obiektów oraz definiowanie interfejsów. Przedstawimy również zaawansowane pojęcia, takie jak iteratory. Zaczynajmy.
Klasy Klasy są po prostu typami definiowanymi przez użytkownika. W językach obiektowych klasa wykorzystywana jest jako szablon do tworzenia obiektu lub instancji (kopii funkcjonalnej) tej klasy. Klasa zawiera opis standardowych właściwości wszystkich należących do niej elementów. Celem klas jest enkapsulacja definicji obiektu, jego zachowanie oraz ukrycie jego implementacji przed użytkownikiem końcowym, a także pozwolenie temu użytkownikowi na wykorzystanie obiektów klas w sposób udokumentowany i oczekiwany. Enkapsulacja zmniejsza rozmiary programów i czyni je łatwiejszymi w zarządzaniu, ponieważ obiekty zawierają potrzebną do tego logikę. Istnieje także mechanizm autoładowania, który pozwala rozbić skrypty na mniejsze, łatwiejsze w zarządzaniu części. Zanim przeanalizujemy prosty przykład klasy, zapoznaj się z terminologią. • Element klasy lub właściwość — zmienna, część danych klasy. • Metoda — funkcja zdefiniowana wewnątrz klasy. Teraz zdefiniujemy klasę dla punktu na dwuwymiarowej przestrzeni, określonego przy użyciu współrzędnych kartezjańskich (listing 1.1). Omawiana klasa została zaprojektowana wyłącznie do celów demonstracyjnych i ma poważne wady. Zalecamy, abyś nie wykorzystywał jej jako podstawy do tworzenia własnego kodu. Listing 1.1. Przestrzeń dwuwymiarowa x=$x; $this->y=$y;
ROZDZIAŁ 1. OBIEKTOWOŚĆ
} function get_x() { return($this->x); } function get_y() { return($this->y); } function odleglosc($p) { return(sqrt( pow($this->x-$p->get_x(),2)+pow($this->y-$p->get_y(),2))); } } // koniec definicji klasy $p1=new Punkt(2,3); $p2=new Punkt(3,4); echo $p1->odleglosc($p2),"\n"; $p2->x=5; echo $p1->odleglosc($p2),"\n"; ?>
Przedstawiona klasa zawiera sporo elementów wartych przeanalizowania i poprawienia. Jak już wcześniej stwierdziliśmy, opisuje ona punkt na płaszczyźnie, określony współrzędnymi kartezjańskimi $x i $y. Przy zmiennych jest umieszczone słowo kluczowe public, do którego wrócimy później. Zaimplementowana została także metoda konstruktora __construct, wywoływana w momencie tworzenia w pamięci nowego obiektu (lub instancji) klasy Punkt poprzez wywołanie operatora new. Innymi słowy, kiedy wywoływana jest linia $p1=new Punkt(2,3), metoda __construct jest wywoływana automatycznie, a argumenty w nawiasach po nazwie klasy są przekazywane do metody __construct i mogą być w niej wykorzystane. Metoda __construct odwołuje się do zmiennej $this. Zmienna $this umożliwia w językach obiektowych odwołanie się do instancji klasy. Odnosi się zawsze do aktualnie obsługiwanego obiektu. Jest obiektowym odpowiednikiem „ja”. Różne odmiany tej zmiennej wykorzystywane są w niemal wszystkich językach obiektowych; w niektórych jest nazywana „self”. Konstruktor klasy jest metodą inicjalizującą (tworzącą instancje) obiekty danej klasy. W tym przypadku przypisuje koordynaty. Koordynaty (zmienne $x i $y) są właściwościami klasy. Zdefiniowane są także inne metody, dwie metody get oraz metoda odleglosc, która oblicza odległość między dwoma punktami. Kolejną rzeczą wartą zauważenia jest słowo kluczowe public. Oznaczanie elementów klasy jako „publiczne” pozwala na uzyskanie do nich pełnego dostępu. W naszym skrypcie znajduje się linia: $p2->x=5; — koordynat x jest w niej modyfikowany bezpośrednio. Taki dostęp nie pozwala na kontrolowanie go i we wszystkich przypadkach, poza najprostszymi, nie powinien mieć miejsca. Dobrą praktyką jest tworzenie akcesorów (metod get i set), które będą odczytywały i zapisywały właściwości w sposób kontrolowany. Innymi słowy, za pośrednictwem metod get i set można kontrolować wartości właściwości. Dla właściwości publicznych metody get i set są nadmiarowe, ponieważ możliwy jest bezpośredni dostęp do nich, tak jak w przypadku $p2->x=5;. Jednakże właściwości publiczne nie pozwalają kontrolować wartości właściwości. Metody get i set mogą być pisane ręcznie dla każdej właściwości, jednak PHP umożliwia wykorzystanie tak zwanych „magicznych” metod, które mogą być stosowane zamiast metod tworzonych ręcznie. Możliwe jest lepsze chronienie elementów klasy poprzez użycie słów kluczowych private i protected. Dokładne znaczenie tych dwóch słów kluczowych będzie wyjaśnione w następnym podrozdziale. Należy także zauważyć, że public jest domyślną widocznością. Jeżeli widoczność dla elementu klasy nie będzie wyszczególniona — zostanie ustawiona na public. Kod: class C { $member; function method() {...} …. }
jest równoznaczny z kodem: class C { public $member;
22
ROZDZIAŁ 1. OBIEKTOWOŚĆ
pubic function method() {…} …. }
W przeciwieństwie do elementów publicznych prywatne właściwości i metody klasy są widoczne tylko dla metod tej klasy. Metody, które nie są częścią klasy, nie mają dostępu do prywatnych właściwości, nie mogą także wywoływać żadnych prywatnych metod. Jeżeli dla właściwości $x i $y słowo kluczowe public zostanie zamienione na protected i nastąpi próba uzyskania dostępu, wyświetli się informacja o błędzie: PHP Fatal error: Cannot access private property Point::$x in script2.1 on line 25
Innymi słowy, nasza mała sztuczka w linii 25., czyli $p2->x=5, przestanie działać. Metoda konstruktora, podobnie jak metody get_x() i get_y(), nie będzie miała żadnego problemu z dostępem do właściwości tak długo, jak długo będzie częścią klasy. Jest to poprawna sytuacja, ponieważ nie będzie możliwości modyfikowania wartości właściwości bezpośrednio z potencjalnym zmodyfikowaniem zachowania klasy w sposób, jaki nie powinien mieć miejsca. W skrócie — klasa jest bardziej hermetyczna, podobnie jak autostrada, na której jest określona liczba wjazdów i zjazdów. Właściwości publiczne i prywatne zostały już opisane. Czym jednak są właściwości i metody oznaczone jako protected? Do takich właściwości i metod dostęp może uzyskać metoda klasy, do której one należą, oraz metoda klasy dziedzicząca po klasie bazowej, do której one należą. Przyjrzymy się temu bliżej w kolejnym podrozdziale.
Dziedziczenie i przeciążanie Zgodnie ze stwierdzeniem z początku tego rozdziału klasy mogą być organizowane hierarchicznie. Hierarchia osiągana jest poprzez dziedziczenie. W celu zademonstrowania dziedziczenia utworzymy kolejną klasę, nazwaną pracownik. Część pracowników firmy to menedżerowie — klasa menedzer będzie klasą dziedziczącą po ogólniejszej klasie pracownik. Dziedziczenie bywa także nazywane specjalizacją. Przyjrzyj się klasie z listingu 1.2. Listing 1.2. Przykład klasy pracownik nazwisko = $nazwisko; $this->pensja = $pensja; } function daj_podwyzke($wartosc) { $this->pensja += $wartosc; printf("Pracownik %s otrzymał podwyżkę w kwocie %d złotych\n", $this->nazwisko, $wartosc); printf("Nowa pensja wynosi %d złotych\n", $this->pensja); } function __destruct() { printf("Żegnaj okrutny świecie: PRACOWNIK:%s\n", $this->nazwisko); } } class menedzer extends pracownik { protected $wydzial; function __construct($nazwisko, $pensja, $wydzial) { parent::__construct($nazwisko, $pensja); $this->wydzial = $wydzial; } function daj_podwyzke ($wartosc) { parent::daj_podwyzke($wartosc); print "Ten pracownik jest menedżerem\n";
23
ROZDZIAŁ 1. OBIEKTOWOŚĆ
} function __destruct() { printf("Żegnaj okrutny świecie: MENEDŻER:%s\n", $this->nazwisko); parent::__destruct(); } } // koniec definicji klas $mgr = new menedzer("Kowalski", 300, 20); $mgr->daj_podwyzke(50); $emp = new pracownik("Nowak", 100); $emp->daj_podwyzke(50); ?>
Klasa ta została utworzona do celów demonstracyjnych i nie powinna być wykorzystywana jako szablon. Warto zauważyć, że konstruktory obu klas są publiczne. Gdyby były prywatne, utworzenie nowego obiektu klasy byłoby niemożliwe. Po uruchomieniu skrypt zwróci następujący wynik: Pracownik Kowalski otrzymał podwyżkę w kwocie 50 złotych Nowa pensja wynosi 350 złotych Ten pracownik jest menedżerem Pracownik Nowak otrzymał podwyżkę w kwocie 50 złotych Nowa pensja wynosi 150 złotych Żegnaj okrutny świecie: PRACOWNIK:Nowak Żegnaj okrutny świecie: MENEDŻER:Kowalski Żegnaj okrutny świecie: PRACOWNIK:Kowalski
Na powyższym przykładzie doskonale można wytłumaczyć pojęcie dziedziczenia. Każdy menedżer jest pracownikiem. Słowo „jest” jest charakterystyczne dla dziedziczenia. W tym przypadku klasa pracownik jest klasą nadrzędną dla klasy menedzer. W przeciwieństwie do prawdziwych relacji pokrewieństwa klasa w PHP może mieć tylko jednego rodzica, wielokrotne dziedziczenie nie jest dozwolone. Ponadto metody klasy nadrzędnej mogą być wywoływane poprzez wykorzystanie konstrukcji parent::, pokazanej w klasie menedzer. Kiedy tworzony jest obiekt klasy dziedziczącej, konstruktor klasy nadrzędnej nie jest wywoływany automatycznie. Wywołanie konstruktora klasy nadrzędnej w klasie podrzędnej należy do zadań programisty. To samo dotyczy metody destruktora. Jest ona dokładnym przeciwieństwem klasy konstruktora. Konstruktor jest wywoływany w momencie tworzenia obiektu w pamięci; destruktor jest wywoływany, gdy obiekt nie jest już potrzebny lub kiedy dla obiektu zostanie wywołana funkcja unset. Jawne wywoływanie funkcji unset nie jest powszechne, zazwyczaj jest wykorzystywane w celu oszczędzania pamięci. To oznacza, że destruktor jest automatycznie wywoływany dla wszystkich obiektów w momencie zakończenia działania skryptu. Metody destruktorów są używane przeważnie do zwolnienia zasobów, na przykład zamknięcia otwartych plików lub połączeń z bazą danych. Zauważ także, że metoda destruktora w klasie klasy menedzer ma pełny dostęp do właściwości nazwisko, pomimo że jest ona właściwością klasy nadrzędnej pracownik. Jest to właściwy cel wykorzystania modyfikatora dostępu protected. Gdyby nazwisko było właściwością prywatną klasy pracownik, powyższy przykład by nie działał. Metoda daj_podwyzke istnieje w obu klasach. PHP „wie”, która metoda powinna zostać wykorzystana dla danej klasy; jest to jeden z aspektów fundamentalnej zasady obiektowości — enkapsulacja. Obiekt $x jest typu menedzer, a metoda daj_podwyzke wyświetliła tekst „Ten pracownik jest menedżerem” po wyświetleniu standardowych informacji wyjściowych. Inaczej mówiąc, metoda daj_podwyzke w klasie menedzer przeciąża lub zastępuje metodę daj_podwyzke w klasie pracownik. Zauważ, że pojęcie przeciążania w PHP jest rozumiane inaczej niż w językach C++ lub Python, w których oznacza ono funkcję (nie metodę klasy) o takiej samej nazwie, ale innych argumentach. Wracając do PHP — jeżeli metoda zostanie oznaczona słowem kluczowym final, nie będzie mogła być przeciążona. Gdyby metoda daj_podwyzke została zadeklarowana w ten sposób: final function daj_podwyzke($wartosc) { …. }
24
ROZDZIAŁ 1. OBIEKTOWOŚĆ
to przeciążenie w klasie menedzer nie byłoby możliwe. Polecamy poćwiczyć podstawowe zagadnienia obiektowości na małym skrypcie oraz poeksperymentować z ustawianiem dla różnych elementów klas modyfikatorów dostępu: public, protected i private, aby zaznajomić się z efektami. Mówiąc o dziedziczeniu, nie można pominąć klas abstrakcyjnych. Dla klas abstrakcyjnych nie można tworzyć instancji — nie można tworzyć żadnych obiektów tej klasy. Klasy te są wykorzystywane głównie jako szablony w celu wymuszenia na klasach dziedziczących określonej struktury. Klasa jest abstrakcyjna, jeżeli zostanie oznaczona słowem kluczowym abstract w następujący sposób: abstract class A { …. }
Żaden obiekt tej klasy nie może być utworzony. PHP zwróci błąd czasu wywoływania i zatrzyma wykonywanie skryptu. Możliwe jest także tworzenie metod abstrakcyjnych wewnątrz klasy abstrakcyjnej: abstract class A { abstract protected metoda(...); }
Metody abstrakcyjne wykorzystywane są do wymuszania implementacji metody w klasach dziedziczących. Klasy abstrakcyjne są z reguły używane jako szablony dla klas dziedziczących po nich. Dobry przykład klasy abstrakcyjnej można znaleźć w standardowej bibliotece PHP (SPL — Standard PHP Library). Klasy dla sterty sortowanej (SplMinHeap, SplMaxHeap) dziedziczą po klasie abstrakcyjnej SplHeap i implementują metodę compare w inny sposób. SplMinHeap posortuje elementy od najmniejszego do największego, natomiast SplMaxHeap posortuje je odwrotnie. Wspólne cechy obu klas są zawarte w klasie abstrakcyjnej SplHeap, której dokumentację można znaleźć pod adresem http://ca2.php.net/manual/en/class.splheap.php. Zamiast tworzyć sztuczne przykłady klas abstrakcyjnych, zobaczmy, w jaki sposób są używane w SPL. Oto proste zastosowanie klasy SplMinHeap: insert('Piotr'); $sterta->insert('Adam'); $sterta->insert('Marcin'); foreach ($sterta as $s) { print "$s\n"; } ?>
Wykonanie tego kodu zwróci następujący wynik: Adam Marcin Piotr
Imiona zostały posortowane alfabetycznie — ich kolejność jest inna, niż była przy wstawianiu. W dalszej części wyjaśnimy, w jaki sposób można wykorzystać obiekt klasy SplMaxHeap w pętli; potraktujemy go jak tablicę. Zwróćmy teraz uwagę na praktyczne techniki programowania obiektowego. Być może zastanawiasz się, jak sprawić, aby klasa była dostępna dla skryptu PHP. Klasy są z reguły pisane tak, aby mogły być wykorzystywane wielokrotnie. Oczywistą odpowiedzią jest utworzenie odrębnego pliku, który następnie może zostać dołączony do skryptu za pomocą dyrektywy require lub include, jednak ten sposób może szybko stać się kłopotliwy, kiedy liczba plików zacznie rosnąć. Okazuje się, że PHP wyposażone jest w narzędzie mające pomóc w rozwiązaniu tego problemu — jest to funkcja __autoload. Przyjmuje ona jako parametr nazwę klasy i będzie wywołana, kiedy PHP nie będzie mogło znaleźć odpowiedniej klasy w aktualnie wykonywanym skrypcie. Zasadniczo __autoload jest funkcją obsługującą przy wystąpieniu wyjątku „klasa nie została odnaleziona”. Do wyjątku tego wrócimy później. Przykład z listingu 1.2 może być przepisany w dwóch plikach (listing 1.3).
25
ROZDZIAŁ 1. OBIEKTOWOŚĆ
Listing 1.3. Listing 1.2 przepisany w dwóch plikach Plik listing1_3.php daj_podwyzke (50); $y = new pracownik("Nowak", 100); $y->daj_podwyzke(50); ?>
Plik ACMEmanager.php: nazwisko = $nazwisko; $this->pensja = $pensja; } function daj_podwyzke($wartosc) { $this->pensja+= $wartosc; printf("Pracownik %s otrzymał podwyżkę w kwocie %d złotych\n", $this->nazwisko, $wartosc); printf("Nowa pensja wynosi %d złotych\n", $this->pensja); } function __destruct() { printf("Żegnaj okrutny świecie: PRACOWNIK:%s\n", $this->nazwisko); } } // koniec klasy "pracownik" class menedzer extends pracownik { protected $wydzial; function __construct($nazwisko, $pensja, $wydzial) { parent::__construct($nazwisko, $pensja); $this->wydzial = $wydzial; } function daj_podwyzke($wartosc) { parent::daj_podwyzke($wartosc); print "Ten pracownik jest menedżerem\n"; } function __destruct() { printf("Żegnaj okrutny świecie: MENEDŻER:%s\n", $this->nazwisko); parent::__destruct(); } } // koniec klasy "menedzer"
Powyższy kod jest całkowicie równoważny z kodem zaprezentowanym na listingu 1.2, jest jednak czytelniejszy, ponieważ najistotniejsza część zawarta jest w pliku listing1_3.php. Drugi plik, ACMEmanager.php, zawiera tylko deklaracje klas. Jeżeli nie interesuje nas zawartość klas, nie musimy ich czytać; musimy tylko wiedzieć, w jaki sposób zachowują się ich obiekty. Zauważ także, że nazwa pliku jest zgodna z nazwą klasy, której obiekt jest tworzony jako pierwszy. Kiedy plik zostanie załadowany, klasa pracownik także będzie zdefiniowana, ponieważ obie klasy znajdują się w tym samym pliku. Drugą rzeczą wartą zauważenia jest to, że nazwa pliku jest poprzedzona przedrostkiem „ACME”. Przedrostek został nadany w celu zwrócenia Twojej uwagi na możliwość tworzenia specjalistycznych klas, przypisanych do projektu bibliotek. Zaimplementowana funkcja __autoload wykorzystuje require_once zamiast include.
26
ROZDZIAŁ 1. OBIEKTOWOŚĆ
Powodem tego jest zachowanie PHP, które zatrzyma wykonywanie skryptu, jeżeli plik wyszczególniony przez require nie jest dostępny. Wykonywanie skryptu zależnego od definicji klas nie ma bez nich sensu. Ponadto pliki zawierające definicje klas nie powinny kończyć się parą znaków ?>, ponieważ mogą być załadowane lub załączone w nagłówku pliku, zanim strona zostanie złożona. Wszystkie białe znaki pomiędzy znakami ?> a znakiem końca pliku będą wstawione na początku wynikowego kodu HTML strony. PHP radzi sobie z brakiem końcowych znaków ?>, więc dobrą praktyką jest pomijanie ich na końcu pliku. Jest to prawdopodobnie najczęstszy powód błędów typu „przesyłanie już się rozpoczęło”, występujących podczas stosowania funkcji header() przy przesyłaniu nagłówka HTTP do przeglądarki.
„Magiczne” funkcje Większość metod nazywanych zazwyczaj „magicznymi” funkcjami odnosi się do brakujących właściwości i metod niezdefiniowanych w klasie. Powodem tego jest szeroko stosowane definiowanie właściwości jako tabeli asocjacyjnej zamiast jako odrębnych zmiennych. Takie podejście jest proste w realizacji, łatwo rozszerzalne oraz wygodne w zarządzaniu, co przełożyło się na popularność definiowania właściwości jako tablicy. Bez „magicznych” funkcji nie byłoby łatwego dostępu do takich właściwości. Pierwsza para tych specjalnych metod warta wspomnienia to metody __get i __set.
Metody __get i __set Metoda __set jest wywoływana w momencie przypisania wartości do nieistniejącej właściwości. Metoda __get jest wywoływana w momencie odczytu wartości nieistniejącej właściwości (listing 1.4). Listing 1.4. Przykład wykorzystania funkcji __get i __set # Przykład wykorzystania funkcji __get i __set # Nieistniejąca właściwość "limit_predkosci" jest zapisywana i odczytywana. wlasciwosci)) { return ($this->wlasciwosci[$arg]); } else { return ("Brak właściwości!\n"); } } public function __set($klucz, $wartosc) { $this->wlasciwosci[$klucz] = $wartosc; } public function __isset($arg) { return (isset($this->wlasciwosci[$arg])); } } $x = new test1(); print $x->limit_predkosci; $x->limit_predkosci = "100 km/h\n"; if (isset($x->limit_predkosci)) { printf("Ograniczenie prędkości wynosi %s\n", $x->limit_predkosci); } $x->limit_predkosci = NULL; if (empty($x->limit_predkosci)) { print "Metoda __isset() została wywołana.\n"; } else { print "Metoda __isset() nie została wywołana.\n"; } ?>
27
ROZDZIAŁ 1. OBIEKTOWOŚĆ
W wyniku działania powyższego skryptu otrzymamy: Brak właściwości! Ograniczenie prędkości wynosi 100 km/h Metoda __isset() została wywołana.
Właściwość limit_predkosci nie jest zdefiniowana, jednak odwołanie do niej nie kończy się błędem, ponieważ w momencie odwołania wykonana została metoda __get. Metoda __set została natomiast wywołana w momencie przypisania wartości do nieistniejącej właściwości. Definiowanie wszystkich właściwości klasy jako tabeli asocjacyjnej i odwoływanie się do nich jak do odrębnych właściwości jest częstą praktyką. Taki sposób definiowania właściwości pozwala na łatwe rozwijanie klasy.
Metoda __isset Poza metodami __get i __set na listingu 1.4 zademonstrowano wykorzystanie funkcji __isset, która jest stosowana do sprawdzania, czy nieistniejąca właściwość, zazwyczaj definiowana jako tablica, została ustawiona (ma przypisaną wartość). Oczywiście dostępna jest także funkcja __unset. Jest ona wywoływana w momencie wywołania unset dla niezdefiniowanej właściwości. Metoda __isset jest także wywoływana podczas sprawdzania, czy zmienna nie jest pusta, za pomocą funkcji empty(). Funkcja empty() sprawdza, czy argument jest ustawiony oraz czy jego długość jest większa od 0. Zwraca prawdę, jeżeli argument nie jest ustawiony lub jego długość jest równa 0, w przeciwnym wypadku zwraca fałsz. Jeżeli argument zostanie ustawiony na pusty ciąg znaków, funkcja empty() zwróci prawdę.
Metoda __call Dla nieistniejących właściwości funkcja __call wywoływana jest w momencie odwołania się do nieistniejącej metody. Jak pokazuje doświadczenie, funkcja wykorzystywana jest raczej rzadko. Listing 1.5 przedstawia krótki przykład, obrazujący to w pełni. Listing 1.5. Funkcja __call jest wykonywana w momencie odwołania się do nieistniejącej metody nieistniejaca_metoda(1, 2, 3); ?>
Metoda nieistniejaca_metoda oczywiście nie została zdefiniowana, jednak mimo to wywołanie jest poprawne.
28
ROZDZIAŁ 1. OBIEKTOWOŚĆ
Metoda __toString() Ostatnia opisywana tutaj „magiczna” funkcja, __toString(), jako jedyna nie ma nic wspólnego z nieistniejącymi elementami klasy. Wykorzystywana jest w momencie, kiedy obiekt jest konwertowany na ciąg znaków poprzez bezpośrednie rzutowanie lub pośrednio, poprzez przekazanie go jako parametru funkcji, która oczekuje argumentów łańcuchowych, np. print (listing 1.6). Listing 1.6. Przykład wykorzystania metody __toString wlasciwosc = $wlasciwosc; } function __toString() { return ("element test2.\n"); } } $x = new test2(1); print $x; ?>
Wykonanie skryptu zwróci następujący wynik: element test2.
Wypisywana jest wartość zwracana przez metodę __toString. Funkcja jest wywoływana w momencie wykorzystania obiektu jak zmiennej łańcuchowej. Funkcja ta jest bardzo pożyteczna, gdy konieczne jest wyświetlenie złożonych obiektów zawierających nietypowe elementy, takie jak połączenia sieciowe lub bazodanowe albo inne obiekty binarne.
Kopiowanie, klonowanie oraz porównywanie obiektów Na początku tego rozdziału wyjaśniono, czym są klasy i w jaki sposób tworzyć i wykorzystywać złożone obiekty. Teraz przyszedł czas na przedstawienie niektórych aspektów wewnętrznej obsługi obiektów. Kiedy tworzony jest obiekt przy zastosowaniu dyrektywy w rodzaju $x=new class(....), zmienna $x jest referencją na obiekt. Co się stanie, kiedy wywołamy polecenie $x=$y? To bardzo proste — obiekt, na który dotychczas wskazywała zmienna $x, jest usuwany, wywoływany jest jego destruktor, a zmienna $x od tej chwili wskazuje na obiekt $y. Listing 1.7 przedstawia skrypt demonstrujący powyższe zachowanie. Listing 1.7. Wykonanie $x=$y wlasciwosc = $wlasciwosc; } function __destruct() { printf("Usuwanie obiektu %s...\n", $this->wlasciwosc); } }
29
ROZDZIAŁ 1. OBIEKTOWOŚĆ
$x = new test3("obiekt 1"); $y = new test3("obiekt 2"); print "Przypisywanie zmiennej:\n"; $x = $y; print "Koniec skryptu\n"; ?>
Wykonanie skryptu zwróci następujący wynik: Przypisywanie zmiennej: Usuwanie obiektu obiekt 1... Koniec skryptu Usuwanie obiektu obiekt 2...
Obiekt 1 jest usuwany z pamięci podczas przypisywania $x=$y. Dlaczego usunięty został obiekt 2? Odpowiedź na to pytanie jest bardzo prosta — destruktor wywoływany jest zawsze, gdy obiekt przestaje być potrzebny. Kiedy skrypt się kończy, wszystkie obiekty przestają być potrzebne i dla każdego z nich jest wywoływany jego destruktor. To powód umieszczenia przypisania pomiędzy dwoma poleceniami print. Ponadto zauważ, że destruktor wywoływany jest tylko raz, pomimo że do obiektu istnieją dwie referencje, $x i $y. Destruktor wywoływany jest raz dla obiektu, nie dla każdej referencji do niego. Ten sposób kopiowania obiektów jest nazywany płytkim kopiowaniem, ponieważ nie jest tworzona prawdziwa kopia obiektu, zmieniane są tylko referencje. Oprócz płytkiego kopiowania istnieje także kopiowanie głębokie, tworzące nowy obiekt. Głębokie kopiowanie wykonywane jest przy użyciu operatora clone. Przykład takiego kopiowania pokazano na listingu 1.8. Listing 1.8. Głębokie kopiowanie przy wykorzystaniu operatora clone wlasciwosc = $wlasciwosc; $this->kopie = $kopie; } function __destruct() { printf("Usuwanie obiektu %s...\n", $this->wlasciwosc); } function __clone() { $this->wlasciwosc.= ":KLON"; $this->kopie++; } function pobierz_kopie() { printf("Obiekt %s ma %d kopii.\n", $this->wlasciwosc, $this->kopie); } } $x = new test3a("obiekt 1"); $x->pobierz_kopie(); $y = new test3a("obiekt 2"); $x = clone $y; $x->pobierz_kopie(); $y->pobierz_kopie(); print "Koniec skryptu, uruchamianie destruktorów.\n"; ?>
Głębokie kopiowanie ma miejsce w linii $x = clone $y. W momencie wykonywania tej linii tworzona jest nowa kopia obiektu $y i wywoływana jest funkcja __clone w celu ustawienia nowej kopii obiektu w sposób wymagany przez skrypt. Wynik działania skryptu wygląda następująco:
30
ROZDZIAŁ 1. OBIEKTOWOŚĆ
Obiekt obiekt 1 ma 0 kopii. Usuwanie obiektu obiekt 1... Obiekt obiekt 2:KLON ma 1 kopii. Obiekt obiekt 2 ma 0 kopii. Koniec skryptu, uruchamianie destruktorów. Usuwanie obiektu obiekt 2... Usuwanie obiektu obiekt 2:KLON...
Nowo utworzona kopia zapisana w zmiennej $x ma wartość właściwości obiekt 2:KLON i liczbę kopii ustawioną na 1 w rezultacie czynności, które zostały wykonane wewnątrz metody __clone. Zauważ także, że konstruktor został wywołany dwukrotnie — raz dla obiektu oryginalnego i raz dla kopii. Klonowanie nie jest używane tak często jak przypisanie przez referencję, jednak dobrze jest mieć taką możliwość. W jaki sposób porównywane są obiekty? Należy rozważyć kilka przypadków, zależnie od kryteriów porównania. Kiedy dokładnie nazwiemy dwie zmienne obiektowe $x i $y „równymi”? Są następujące trzy poprawne logicznie możliwości: • Obiekty tej samej klasy mają równe wartości dla wszystkich właściwości. • Obiekty są referencją do tego samego obiektu tej samej klasy. • Wykorzystywanych jest kilka innych niestandardowych kryteriów. Standardowy operator porównania == testuje pierwszy wariant. Wyrażenie $x==$y jest prawdziwe wtedy i tylko wtedy, gdy odpowiednie właściwości obu obiektów są sobie równe. Druga możliwość — czyli $x i $y są referencjami na ten sam obiekt — sprawdzana jest za pomocą specjalnego operatora === (trzy znaki równości pisane łącznie). Wyrażenie $x===$y jest prawdziwe wtedy i tylko wtedy, gdy obie zmienne są referencją na ten sam obiekt. Zwróć uwagę, że zwyczajowe przypisanie $x=$y spowoduje, że wyrażenie $x===$y będzie prawdziwe, klonowanie natomiast sprawi, że wyrażenie będzie fałszywe. Jeżeli nie została zdefiniowana niestandardowa metoda __clone, oryginalny obiekt i klon będą równe dla wyrażenia z wykorzystaniem operatora ==. Co możemy zrobić w trzecim przypadku, niestandardowej definicji równości? Musimy utworzyć niestandardową funkcję porównującą i porównać zwracane wartości. Podczas pisania funkcji przyjmujących argumenty konkretnych klas możliwe jest egzekwowanie typu zmiennej przekazywanej do funkcji poprzez podanie nazwy klasy przed formalną nazwą argumentu. Wygląda to w ten sposób: function funkcja_testowa(test3a $a) {….}
Wymagane jest tutaj, aby argument $a był typu test3a. Jest to możliwe wyłącznie dla typów obiektowych i tablicowych poprzez wstawienie słowa array zamiast nazwy klasy. PHP5 nadal jest słabo typowanym językiem i wymuszenie typów argumentów dla standardowych typów takich jak int nadal nie jest wspierane.
Interfejsy, iteratory i klasy abstrakcyjne Kolejnym standardowym typem w programowaniu obiektowym jest interfejs. Interfejs jest obiektem opisującym zestaw metod, które klasa może zaimplementować. Interfejs wygląda następująco: interface interf { public function f1($x,$y,...,); public function f2(....); …. public function fn(...); }
Zauważ, że kod metody nie jest wyspecyfikowany, wyszczególnione są tylko jej nazwa i argumenty. Klasa może zaimplementować interfejs w następujący sposób: class c extends klasaNadrzedna implements interf { (wszystkie funkcje wyszczególnione w interfejsie muszą zostać zaimplementowane) … )
31
ROZDZIAŁ 1. OBIEKTOWOŚĆ
Interfejsy mogą po sobie dziedziczyć jak klasy. Składnia jest identyczna: interface interf2 extends interf1 { function f1(...); }
Nowy interfejs interf2 będzie zawierał wszystkie funkcje z interfejsu interf1 oraz nowe, zdefiniowane w interf2 (listing 1.9). Listing 1.9. Przykład nowego interfejsu interf2 wlasciwosc = $wlasciwosc; } function f2($x) { printf("Wywołanie F2 dla %s z argumentem: %s\n", $this->wlasciwosc, $x); } } $x = new c1("test"); $x->f2('a');
Wywołanie powyższego skryptu spowoduje błąd, ponieważ funkcja f1 z interfejsu i1 nie została zdefiniowana. Błąd będzie miał postać: Fatal error: Class c1 contains 1 abstract method and must therefore be declared abstract or ´implement the remaining methods (i1::f1) in /home/mgogala/work/book/script2.6.php on line 16
Interfejsy są standardowymi strukturami w języku Java i są rzadziej spotykane w językach skryptowych takich jak PHP. Przykład, który przedstawimy poniżej, dotyczy interfejsu Iterator, który jest integralną częścią języka PHP. Iterator jest obiektem klasy implementującej wewnętrzny interfejs PHP o nazwie Iterator. Iterator definiowany jest zgodnie z poniższym: interface Iterator { public function rewind(); public function next(); public function key(); public function current(); public function valid(); }
// Wróć do początku iteratora. // Przejdź do kolejnego elementu. // Pobierz klucz aktualnego obiektu. // Pobierz wartość aktualnego obiektu. // Czy aktualny indeks jest poprawny?
Każda klasa implementująca interfejs Iterator może być wykorzystywana w pętlach; jej obiekty są nazywane iteratorami (listing 1.10). Listing 1.10. Każda klasa implementująca interfejs Iterator może być stosowana w pętlach foreach elementy = $elementy;
32
ROZDZIAŁ 1. OBIEKTOWOŚĆ
} function rewind() { $this->index = 0; } function current() { return ($this->elementy[$this->indeks]); } function key() { return ($this->indeks); } function next() { $this->indeks++; if (isset($this->elementy[$this->indeks])) { return ($this->elementy[$this->indeks]); } else { return (NULL); } } function valid() { return (isset($this->elementy[$this->indeks])); } } $x = new iter(range('A', 'D')); foreach ($x as $klucz => $wartosc) { print "klucz=$klucz\twartość=$wartosc\n"; }
Jest to bardzo prosty, ale bardzo typowy przykład iteratora w PHP. Wykonany skrypt zwróci następujący wynik: klucz=0 klucz=1 klucz=2 klucz=3
wartość=A wartość=B wartość=C wartość=D
Przyjrzyjmy się głównej części tego przykładu, czyli pętli na końcu skryptu. Taka składnia jest z reguły stosowana w odniesieniu do tablic, ale $x nie jest tablicą, lecz obiektem klasy iter. Iteratory są obiektami, które mogą zachowywać się jak tablice. Jest to osiągane poprzez implementację interfejsu Iterator. W jakich sytuacjach można wykorzystać takie zachowanie? Po liniach w pliku lub wierszach zwracanych przez kursor można iterować z łatwością. Zauważ, że nie można używać takich wyrażeń jak $x[$indeks]. Zmienna licząca wykorzystywana jest jedynie podczas przechodzenia do kolejnych elementów tablicy. Względnie prostym ćwiczeniem będzie zaimplementowanie takiego zachowania (listing 1.11). Listing 1.11. Implementacja interfejsu Iterator fp = $fp; $this->linia = rtrim(fgets($this->fp), "\n"); } function rewind() {
33
ROZDZIAŁ 1. OBIEKTOWOŚĆ
$this->indeks = 0; rewind($this->fp); $this->linia = rtrim(fgets($this->fp), "\n"); } function current() { return ($this->linia); } function key() { return ($this->indeks); } function next() { $this->indeks++; $this->linia = rtrim(fgets($this->fp), "\n"); if (!feof($this->fp)) { return ($this->linia); } else { return (NULL); } } function valid() { return (feof($this->fp) ? FALSE : TRUE); } } $x = new iterator_pliku("qbf.txt"); foreach ($x as $numer_linii => $wartosc) { print "$numer_linii:\t$wartosc\n"; }
Plik sbl.txt zawiera następujące trzy wiersze: szybki brązowy lis przeskoczył przez leniwego psa
Skrypt odczyta plik, a następnie wypisze na ekranie poszczególne linie poprzedzone ich numerami. Wykorzystuje on standardowe operacje na plikach, takie jak fopen, fgets oraz rewind. rewind jest nie tylko nazwą metody interfejsu iteratora, lecz także nazwą głównej metody wykorzystywanej podczas wykonywania operacji na pliku. Funkcja modyfikuje uchwyt do pliku, aby wskazywał na początek tego pliku. Numery linii zaczynają się od 0, aby pliki były jak najbardziej podobne do tablic. Jak dotąd, przedstawialiśmy sposób zamieniania plików i tablic w iteratory. Każdy typ posiadający metody „pobierz kolejny” oraz „czy skończyłem?” może zostać zaimplementowany jako iterator i będzie można zastosować go w pętli. Przykładem może być kursor bazodanowy. Posiada on metodę „pobierz kolejny”, fetch. Można też zdecydować, kiedy jest pobierany ostatni rekord przy wykorzystaniu statusu uchwytu. Implementacja iteratora jest bardzo podobna do tej z listingu 1.11. Klasa iterator_pliku jest tylko przykładem. PHP5 zawiera zestaw wewnętrznych klas nazwany standardową biblioteką PHP (SPL — Standard PHP Library), podobny do biblioteki STL w C++. Dużo bardziej złożoną klasą należącą do SPL jest klasa SplFileObject. Klasa ta implementuje interfejs iteratora. Nasz cały skrypt mógł zostać napisany w łatwiejszej formie: $wartosc) { if (!empty($val)) {print "$numer_linii:\t$wartosc"; } } ?>
Zauważ, że znaki nowej linii nie są usuwane z linii i że musimy sprawdzać, czy linie nie są puste. Klasa SplFileObject mogłaby wyjść poza koniec pliku, gdybyśmy ominęli sprawdzanie pustych wierszy. Mimo to jest to klasa znacznie ułatwiająca pracę. Jedyną bardzo użyteczną funkcją, której brakuje w klasie SplFileObject, jest fputcsv, zwracająca tablice w formacie CSV. Można jednak z łatwością ją sobie napisać.
34
ROZDZIAŁ 1. OBIEKTOWOŚĆ
W bibliotece SPL jest wiele innych przydatnych klas i interfejsów. Pełny opis biblioteki SPL wykracza poza zakres niniejszej książki. Odpowiednią dokumentację możesz znaleźć pod adresem http://www.php.net/ manual/pl/book.spl.php. Istnieje także standardowy zestaw klas implementujących iterator dla kursorów i zapytań bazodanowych. Zestaw ten nazwano ADOdb. Pozwala on programiście wykorzystywać wyniki zapytań w pętlach foreach, podobnie jak pliki i tablice. Zestaw ADOdb będzie omówiony szczegółowo w dalszej części książki. Jaka jest różnica między klasami abstrakcyjnymi a interfejsami? Jedne i drugie wykorzystuje się jako szablony dla innych klas dziedziczących po nich lub implementujących je; klasa abstrakcyjna jest jednak dużo bardziej restrykcyjna i znacznie ściślej definiuje strukturę. Dodatkowo poza metodami abstrakcyjnymi klasy abstrakcyjne mogą mieć nieabstrakcyjne właściwości i metody — nawet metody ze słowem kluczowym final, które nie mogą być przeciążane.
Kontekst klasy i elementy statyczne Do tej pory pracowaliśmy wyłącznie z właściwościami i metodami, które są definiowane w kontekście obiektu — każdy obiekt posiadał własne właściwości i metody. Są jeszcze właściwości i metody działające w kontekście klasy. Oznacza to, że są wspólne dla wszystkich obiektów danej klasy. Problem, który próbujemy rozwiązać, jest następujący: w jaki sposób możemy policzyć wszystkie obiekty danej klasy utworzone w skrypcie. Oczywiście potrzebujemy licznika, który będzie działał raczej na poziomie klasy niż obiektu. Zmienne i metody zadeklarowane w kontekście klasy, nie w kontekście obiektu, nazywają się zmiennymi statycznymi (listing 1.12). Listing 1.12. Przykład zmiennych statycznych liczba_obiektow); } } $x = new test4(); printf("X: %d obiekt został utworzony\n", $x->get_liczba_obiektow()); $y = new test4(); printf("Y: %d obiekty zostały utworzone\n", $y->get_liczba_obiektow()); print "Ponownie sprawdźmy zmienną x:\n"; printf("X: %d obiekty zostały utworzone\n", $x->get_liczba_obiektow()); print "Jeżeli odwołamy się do właściwości obiektu, PHP utworzy nową liczbę dla X...\n"; printf("i zainicjuje ją na:%d\n", $x->blad()); ?>
Wykonanie skryptu zwróci następujący wynik: X: 1 obiekt został utworzony Y: 2 obiekty zostały utworzone Ponownie sprawdźmy zmienną x: X: 2 obiekty zostały utworzone Jeżeli odwołamy się do właściwości obiektu, PHP utworzy nową liczbę dla X... i zainicjuje ją na:0
35
ROZDZIAŁ 1. OBIEKTOWOŚĆ
Zmienna test4::$liczba_obiektow jest zmienną statyczną rezydującą w kontekście klasy. Kiedy została zwiększona do dwóch podczas tworzenia obiektu $y, zmiana była widoczna także dla obiektu $x. Jeżeli podjęta będzie próba odczytania zmiennej jako właściwości obiektu, tak jak w przypadku funkcji blad(), PHP utworzy nową publiczną właściwość o takiej nazwie. Wszystko stało się teraz bardziej zagmatwane. Fakt, że element klasy został oznaczony jako statyczny, nie ma nic wspólnego z jego widocznością. Mogą występować elementy statyczne typu public, private i protected, z takimi samymi restrykcjami jak przy właściwościach obiektu. Zwróć także uwagę, że do tej samej zmiennej odwoływaliśmy się zarówno poprzez self::$liczba_obiektow, jak i test4::$liczba_obiektow. Słowo kluczowe self jest skrótem dla „ta klasa” i zawsze dotyczy klasy, w której zostało wykorzystane. Innymi słowy, nie uwzględnia dziedziczenia — zawsze jest takie samo (listing 1.13). Listing 1.13. Słowo kluczowe self zawsze odnosi się do klasy, w której zostało zdefiniowane get_wlasciwosc()); printf("B:%d\n", $y->get_wlasciwosc()); ?>
Jeśli kod metody get_wlasciwosc w klasie B jest oznaczony jako komentarz, oba wiersze będą wyświetlały liczbę 4, ponieważ obie funkcje będą wywoływane w kontekście klasy A. Jeżeli nie będzie oznaczony jako komentarz, linia printf("B:%d\n", $y->get_wlasciwosc()); wyświetli liczbę 9. Zmienne klas najlepiej wywoływać poprzez ich nazwy. Takie odwołanie jest jednoznaczne i sprawia, że kod jest czytelniejszy. Oprócz właściwości statycznych istnieją metody statyczne. One także są wywoływane w kontekście klasy: class::metoda_statyczna(...). Należy wspomnieć, że brak jest jakiejkolwiek serializacji, leży ona całkowicie w gestii użytkownika.
Podsumowanie Z tego rozdziału dowiedziałeś się całkiem sporo o klasach i obiektach w PHP. Powinieneś już znać pojęcia: klasa, obiekt, metoda, właściwość, konstruktor, destruktor, dziedziczenie, przeciążenie, interfejs, klasa abstrakcyjna, metoda statyczna oraz iterator. Rozdział ten nie jest w żadnym wypadku kompletnym spisem elementów obiektowych w PHP5, zawiera jednak główne pojęcia i może być solidną podstawą dalszej nauki. Oficjalna dokumentacja zamieszczona pod adresem www.php.net jest doskonałym źródłem wiedzy i zawiera wszystko, co zostało pominięte w tym rozdziale.
36
ROZDZIAŁ 2
Wyjątki i referencje
W tym rozdziale przedstawimy wyjątki i referencje — dwa podstawowe aspekty nowoczesnego programowania obiektowego (OOP — Object-oriented programming). Wyjątki są zdarzeniami synchronicznymi. Słowo „synchroniczne” oznacza, że wyjątki występują w konsekwencji zdarzeń w kodzie, a nie zdarzeń zewnętrznych, takich jak sygnały. Kiedy na przykład operator stosuje kombinację klawiszy Ctrl+C, sygnał przesyłany jest do wykonywanego programu. Wyjątki są wykorzystywane do obsługi błędów w sposób uporządkowany i zgodny ze standardami. Kiedy program (lub jak w przypadku PHP — skrypt) próbuje wykonać dzielenie przez zero, zgłaszany jest wyjątek. Wyjątki mogą być zgłaszane (lub wywoływane) oraz przechwytywane. Wywołanie wyjątku oznacza przekierowanie kontroli programu do części kodu przeznaczonej do obsługi tych zdarzeń. Współczesne języki programowania, takie jak PHP, mają możliwość wykonania tego w logiczny i uporządkowany sposób.
Wyjątki Wyjątki są obiektami klasy Exception lub dowolnej klasy dziedziczącej po niej. Na pewno pamiętasz z poprzedniego rozdziału, że dziedziczenie jest hierarchicznym związkiem między klasami. Definicja klasy Exception zgodnie ze specyfikacją jest następująca: Exception { /* Właściwości */ protected string $message ; protected int $code ; protected string $file ; protected int $line ; /* Metody */ public __construct ([ string $message = "" [, int $code = 0 [, Exception $previous = NULL ]]] ) final public string getMessage ( void ) final public Exception getPrevious ( void ) final public int getCode ( void ) final public string getFile ( void ) final public int getLine ( void ) final public array getTrace ( void ) final public string getTraceAsString ( void ) public string __toString ( void ) final private void __clone ( void ) }
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
Wyjątki są zatem obiektami mieszczącymi przynajmniej powyższe informacje w momencie wystąpienia błędu, czyli zawierają: treść błędu, kod błędu, nazwę pliku oraz linię, w której wystąpił wyjątek. Wyjątki są bardzo przydatne podczas szukania i usuwania błędów w programach. Listing 2.1 przedstawia przykład. Listing 2.1. Przykład wyjątku wartosc = $wartosc; } public function informacja() { printf($this->wiadomosc, $this->wartosc); } } try { $a = "mój string"; if (!is_numeric($argv[1])) { throw new WyjatekNieLiczba($argv[1]); } if (!is_numeric($argv[2])) { throw new WyjatekNieLiczba($argv[2]); } if ($argv[2] == 0) { throw new Exception("Niedozwolone dzielenie przez zero.\n"); } printf("Wynik: %f\n", $argv[1] / $argv[2]); } catch(NonNumericException $exc) { $exc->informacja(); exit(-1); } catch(Exception $exc) { print "Wyjątek:\n"; $kod = $exc->getCode(); if (!empty($kod)) { printf("Kod błędu:%d\n", $kod); } print $exc->getMessage() . "\n"; exit(-1); } print "Zmienna a=$a\n"; ?>
Skrypt wykonany z wiersza poleceń z innymi argumentami zwróci następujące wyniki: ./listing2_1.php 4 2 Wynik: 2.000000 Zmienna a:mój string ./listing2_1.php 4 A Błąd: wartość A nie jest liczbą!
38
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
./listing2_1.php 4 0 Wyjątek: Niedozwolone dzielenie przez zero.
Z punktu widzenia wyjątków ten niewielki skrypt zawiera sporo ważnych rzeczy. Tablica $argv jest predefiniowaną tablicą globalną z argumentami wiersza poleceń. Istnieje także predefiniowana zmienna globalna $argc zawierająca liczbę argumentów wiersza poleceń, jak w języku C. Teraz zwróćmy uwagę na wyjątki i ich składnię. Najpierw zdefiniowaliśmy klasę wyjątku, która ignoruje istniejącą strukturę klasy Exception i nie wywołuje nawet metody konstruktora klasy nadrzędnej. Nie jest to dobrą praktyką programistyczną i zostało tu przedstawione jedynie jako przykład. W konsekwencji nasza klasa nie ma metod getMessage oraz getCode, przez co jest trudniejsza w wykorzystaniu. Zwyczajowa składnia wyjątku nie odnosi się do naszej klasy, co może powodować problemy, jeżeli ktoś spróbuje na przykład wywołać metodę getMessage(). Następnie utworzyliśmy blok try, w którym wystąpił wyjątek. Pojawienie się wyjątku sprawdzane jest w blokach catch (zwanych także blokami obsługi wyjątków), występujących bezpośrednio po bloku try. Blok try nie jest zwykłym blokiem programu; zmienne zdefiniowane wewnątrz bloku try będą zdefiniowane także poza nim. Zmienna $a jest wyświetlana po wykonaniu pierwszej części, po dzieleniu 4 przez 2. Kolejną sprawą jest składnia instrukcji throw. W tej instrukcji „rzucany” jest wyjątek. Bloki obsługi błędów są bardzo podobne do funkcji przyjmujących jeden argument — obiekt wyjątku. Kolejność bloków obsługi błędów jest istotna. PHP przekaże wyjątek do pierwszego bloku obsługi przyjmującego wyjątek danego typu. Blok obsługi dla wyjątku klasy Exception musi zawsze być ostatni, ponieważ przechwyci wszystkie zgłaszane wyjątki dowolnego typu. Kiedy zgłaszany jest wyjątek, PHP znajduje pierwszy pasujący blok obsługi i wykorzystuje go. Gdyby blok obsługujący wyjątki typu Exception został utworzony przed blokiem obsługującym wyjątki typu WyjatekNieLiczba, to ten drugi nie zostałby nigdy wywołany. Bloki obsługi błędów lub bloki catch wyglądają jak funkcje. Podobieństwo nie jest przypadkowe. PHP posiada także „magiczną” metodę set_exception_handler, dzięki której możliwe jest ustawienie obsługi dla wszystkich nieprzechwyconych wyjątków. Przepiszmy skrypt z listingu 2.1 (listing 2.2). Listing 2.2. Przepisany skrypt z listingu 2.1 getCode(); if (!empty($kod)) { printf("Kod błędu:%d\n", $kod); } print $exc->getMessage() . "\n"; exit(-1); } set_exception_handler('domyslna_obsluga'); class WyjatekNieLiczba extends Exception { private $wartosc; private $wiadomosc = "Błąd: wartość %s nie jest liczbą!\n"; function __construct($wartosc) { $this->wartosc = $wartosc; } public function informacja() { printf($this->wiadomosc, $this->wartosc); } } try { if (!is_numeric($argv[1])) { throw new WyjatekNieLiczba($argv[1]); }
39
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
if (!is_numeric($argv[2])) { throw new WyjatekNieLiczba($argv[2]); } if ($argv[2] == 0) { throw new Exception("Niedozwolone dzielenie przez zero.\n"); } printf("Wynik: %f\n", $argv[1] / $argv[2]); } catch(WyjatekNieLiczba $exc) { $exc->informacja(); exit(-1); } ?>
Wynik powyższego skryptu będzie identyczny z wynikiem działania skryptu z listingu 2.1. Blok obsługi wyjątków zadeklarowany przy wykorzystaniu funkcji set_exception_handler jest funkcją przyjmującą jeden argument typu Exception i wykonywaną po wszystkich innych blokach obsługi wyjątków. ./listing2_2.php 4 A Błąd: wartość A nie jest liczbą!
Powyższy kod pochodzi z bloku obsługi wyjątku WyjatekNieLiczba, a nie z bloku domyślnego. Jeżeli drugi argument zostałby zastąpiony przez 0 podczas wykonywania skryptu, otrzymalibyśmy wynik zgodny z poprzednim skryptem: ./listing2_2.php 4 0 Wyjątek: Niedozwolone dzielenie przez zero.
Teraz wykonany został domyślny blok obsługi wyjątków. Domyślna obsługa wyjątków jest szczególnie użyteczna podczas pracy z klasami napisanymi przez kogoś innego, takimi jak klasa SplFileObject z poprzedniego rozdziału. Jeśli pójdzie coś nie tak, obiekty tej klasy zgłoszą wyjątki typu Exception, analogicznie do ADOdb. UWAGA. Klasy z repozytorium PEAR zgłaszają wyjątki klasy PEAR_Exception w momencie wystąpienia błędu. Klasa PEAR_Exception posiada wszystkie elementy standardowej klasy Exception, wzbogacone o dodatkową zmienną $trace. PEAR_Exception spróbuje także pokazać stos wywołań, kiedy zostanie zgłoszony i przechwycony.
Listing 2.3 pokazuje przykład skryptu, który próbuje otworzyć nieistniejący plik, wykorzystując klasę SplFileObject. Zadeklarowany został także domyślny blok obsługi błędów, który przechwyci wyjątek zgłoszony przez klasę SplFileObject, pomimo że nie ma w kodzie jawnych bloków try { ..} catch {...}.
Listing 2.3. Przykład skryptu próbującego otworzyć nieistniejący plik przy wykorzystaniu klasy SplFileObject getCode(); if (!empty($kod)) { printf("Kod błędu:%d\n", $kod); } print $exc->getMessage() . "\n"; print "Plik:" . $exc->getFile() . "\n"; print "Linia:" . $exc->getLine() . "\n"; exit(-1); } set_exception_handler('domyslna_obsluga'); $plik = new SplFileObject("nieistniejacy_plik.txt", "r"); ?>
40
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
Wykonanie skryptu zwróci następujący wynik: Exception: SplFileObject::__construct(nieistniejacy_plik.txt) [splfileobject.--construct]: failed to open ´stream: No such file or directory Plik:C:\ksiazka\Rozdzial02\listing2_3.php Linia:14
Przy korzystaniu z klas napisanych przez kogoś innego domyślny blok obsługi wyjątków może być bardzo użytecznym, chociaż niewymagającym narzędziem. Domyślny blok obsługi wyjątków przechwyci wszystkie nieprzechwycone w inny sposób wyjątki. Przeważnie służy on do kończenia działania programu oraz ułatwia proces identyfikacji błędów. Oczywiście, w pewnych sytuacjach programista może przeprowadzić specjalną obsługę i wykonać coś takiego: try { $file = new SplFileObject("nieistniejacy_plik.txt", "r"); } catch (Exception $e) { $plik=STDIN; }
Jeżeli wystąpi problem z otwarciem pliku do odczytu, zostaną zwrócone standardowe dane wejściowe. Nie będzie to obiekt klasy SplFileObject, a programista będzie musiał uporać się z ewentualnymi konsekwencjami. Ponieważ domyślny blok obsługi zostanie wykonany jako ostatni dla nieprzechwyconych wyjątków, nie ma żadnych przeszkód, aby dokładnie obsłużyć wyjątki i napisać własne bloki obsługi. Jest jeszcze jedna rzecz warta wspomnienia — zagnieżdżanie wyjątków. PHP nie wspiera zagnieżdżania, jeżeli także bloki try nie zostaną zagnieżdżone. Innymi słowy, w poniższej sytuacji obsługa dla ExcB nie zostanie wywołana, jeżeli wyjątek jest zgłoszony wewnątrz obsługi ExcA: class ExcA extends Exception {...} class ExcB extends Exception {...} try {... throw new ExcA(..) } catch(ExcA $e) { throw new ExcB(); } catch(ExcB $e) { // Nie zostanie wykonany, jeżeli wyjątek będzie zgłoszony wewnątrz ExcA. }
Jedynym sposobem na zagnieżdżanie wyjątków jest zagnieżdżenie bloków try. W kontekście zagnieżdżania wyjątków PHP5 nie różni się od Javy czy C++.
Referencje Kolejnym ważnym typem obiektów w PHP są referencje. Referencje w PHP nie są wskaźnikami. PHP w odróżnieniu od Perla nie ma typu referencyjnego, który może być wykorzystany do adresowania obiektów przez dereferencje. W PHP referencja oznacza inną nazwę dla obiektu. Przyjrzyjmy się skryptowi z listingu 2.4. Listing 2.4. W PHP referencje są obiektami wlasciwosc = $wlasciwosc; } function get_wlasciwosc() { return ($this->wlasciwosc); } function set_wlasciwosc($wlasciwosc) { $this->wlasciwosc = $wlasciwosc; } }
41
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
function funkc(test5 $x) { $x->set_wlasciwosc(5); } $x = new test5(10); printf("Element X posiada właściwość %s\n", $x->get_wlasciwosc()); funkc($x); printf("Element X posiada właściwość %s\n", $x->get_wlasciwosc()); $arr = range(1, 5); foreach ($arr as $a) { $a*= 2; } foreach ($arr as $a) { print "$a\n"; } ?>
Wywołanie skryptu zwraca następujący wynik: Element X posiada właściwość 10 Element X posiada właściwość 5 1 2 3 4 5
Dla zmiennej obiektowej $x wartość została zmieniona przez operacje wewnątrz metody funkc, a dla tablicy $arr wartości nie zostały zmienione przez operacje wewnątrz pętli foreach. Dzieje się tak, ponieważ PHP przekazuje parametry przez kopiowanie. Oznacza to, że dla typów nieobiektowych, takich jak liczby, łańcuchy znaków czy tablice, tworzona jest nowa, identyczna kopia obiektu, dla obiektów natomiast tworzona jest referencja lub, inaczej mówiąc, nowa nazwa dla obiektu. Kiedy do metody funkc został przekazany argument $x typu test5, powstała nowa nazwa dla obiektu. Poprzez operacje na nowej zmiennej modyfikowaliśmy zawartość pierwotnego obiektu, a nowa zmienna była tylko nową nazwą dla obiektu już istniejącego. Więcej szczegółów znajdziesz w rozdziale 1. Pomimo że PHP w przeciwieństwie do Perla nie zapewnia bezpośredniego dostępu do referencji, nadal umożliwia w pewnym stopniu kontrolę nad sposobem kopiowania obiektów. Listing 2.5 przedstawia przykład kopiowania przez referencję. Listing 2.5. Kopiowanie przez referencję
Powyższy skrypt składa się z dwóch części — zwykłego przypisania oraz przypisania przez referencję. W pierwszej części podczas normalnego przypisania tworzona jest nowa kopia zmiennej $y. Kopia ta jest przypisywana do zmiennej $x, poprzednia zawartość zmiennej $x jest usuwana. Zwiększenie zmiennej $y nie miało żadnego wpływu na zmienną $x. W drugiej części przez przypisanie przez referencję poprzednia zawartość zmiennej $x także została usunięta, jednak zmienna stała się aliasem (referencją) zmiennej $y. Zwiększenie wartości zmiennej $y o jeden było widoczne także w zmiennej $x, która zwróciła 3 zamiast 2. Analogiczna operacja może także dotyczyć pętli. Na listingu 2.4 mieliśmy następujący fragment kodu: $arr = range(1, 5); foreach ($arr as $a) { $a*= 2; } foreach ($arr as $a) { print "$a\n";
Wynikiem były niezmienione liczby od 1 do 5. Zmieńmy teraz ten fragment, korzystając z operatora referencji &: $arr = range(1, 5); foreach ($arr as $a) { $a*= 2; } print_r($arr);
Wynikiem będzie zmieniona tablica: Array ( [0] [1] [2] [3] [4] )
=> => => => =>
2 4 6 8 10
Innymi słowy, dodając operator & do zmiennej $a, nie utworzyliśmy kopii elementu tablicy — inaczej niż przy wyrażeniu foreach($arr as $a), gdzie kopia była tworzona. Zamiast tego utworzyliśmy referencję do elementów tablicy, co oznacza, że wszystko, co zrobimy ze zmienną $a wewnątrz pętli, zmodyfikuje element tablicy, a nie jego kopię. Niemożliwe jest utworzenie referencji do funkcji. UWAGA. Gdy po referencji do tablicy wykonywana jest pętla, należy zachować szczególną ostrożność. Jeżeli kod zmieni tablicę, efekty mogą być nieprzewidywalne.
Możliwe jest jednakże zwrócenie referencji jako wyniku działania funkcji oraz przekazanie referencji jako parametru funkcji. Argumenty powinny być przekazywane do funkcji przez referencję, jeżeli funkcja może zmienić pierwotną zmienną. Składnia jest taka sama jak w przypadku pętli — zmienna przekazywana przez referencję jest poprzedzana ampersandem (&). Listing 2.6 przedstawia przykład. Listing 2.6. Możliwe jest zwracanie referencji jako wyniku działania funkcji oraz przekazywanie referencji do tej funkcji
43
Kiedy została wywołana funkcja f1, nastąpiło przekazanie argumentu przez wartość. Polecenie print wewnątrz funkcji wyświetliło liczbę 8, jednak pierwotna zmienna nie uległa zmianie, pozostała ustawiona wartość 5. Kiedy nastąpiło wywołanie funkcji f2, pierwotna zmienna uległa modyfikacji. Widać to po ostatniej wyświetlonej wartości. Referencje można także zwracać jako wyniki funkcji. Nie powinno się tego stosować w celu zwiększenia wydajności, ponieważ PHP robi to automatycznie. Powtórzmy: referencja jest po prostu inną nazwą dla zmiennej. Referencje mogą być wykorzystane do obejścia zabezpieczenia widoczności zmiennej, ustawionego za pomocą słowa kluczowego private lub protected (listing 2.7). Listing 2.7. Referencje mogą być wykorzystane do obejścia zabezpieczenia widoczności zmiennej x = $x; } function &get_x() { // Zauważ znak "&" w deklaracji funkcji. return $this->x; } function set_x($x) { $this->x = $x; } } $a = new test6(); $b = &$a->get_x(); // $b jest referencją na $x->a. Jest to obejście zabezpieczenia // ustawionego za pośrednictwem kwalifikatora "private". print "b=$b\n"; $a->set_x(15); print "b=$b\n"; // $b zmieni wartość po wywołaniu "set_x". $b++; print '$a->get_x()='.$a->get_x() . "\n"; // $a->x zmieni wartość po zwiększeniu wartości $b. ?>
Po wykonaniu skryptu wynik jest zgodny z oczekiwaniami: b=10 b=15 $a->get_x()=16
W tym przykładzie zmienna $b jest ustawiana jako referencja na $a->x, która jest prywatną właściwością klasy test6. Zostało to wykonane poprzez zwrócenie referencji w wyniku działania metody get_x(). Oczywiście
44
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
deklarowanie publicznej referencji do prywatnej właściwości przeczy celowi kontroli widoczności. Zwracanie wartości przez referencję jest rzadko stosowaną praktyką. Wykorzystywanie tej metody powinno być dobrze przemyślane — łatwo można zezwolić na niepowołany dostęp do referencji zwróconych przez funkcję.
Podsumowanie Z tego rozdziału dowiedziałeś się o wyjątkach i referencjach w PHP. Oba elementy funkcjonują także w innych nowoczesnych językach programowania. Celem wyjątków jest zapewnienie łatwego systemu obsługi błędów. Głównym zadaniem referencji jest zwiększenie szybkości wykonywania kodu oraz (sporadycznie) umożliwienie wykorzystania sztuczek programistycznych. Oba elementy języka są bardzo przydatne i mogą ułatwić pracę programiście. Obsługa wyjątków pozwala na sprawdzanie błędów w elegancki sposób, co zostanie zademonstrowane w rozdziale dotyczącym integracji z bazami danych.
45
ROZDZIAŁ 2. WYJĄTKI I REFERENCJE
46
ROZDZIAŁ 3
Mobilne PHP
Tworzenie aplikacji mobilnych staje się z roku na rok coraz popularniejsze. O zwiększenie udziału w rynku walczą iPhone, Android czy BlackBerry. Każdy producent smartfona potrzebuje aplikacji dla swojego urządzenia, by przyciągnąć jak najwięcej użytkowników. Ponadto istnieją tablety, takie jak iPad, PlayBook i Galaxy, oraz czytniki, np. Kindle czy Nook. Nawet standardowe telefony komórkowe mają przeglądarki i różne dodatki. Na każdym urządzeniu mobilnym posiadającym dostęp do internetu można przeglądać strony internetowe i uruchamiać aplikacje utworzone w technologii PHP. Dlatego potrzebny jest sposób sensownego prezentowania zawartości stron na mniejszych urządzeniach. Z tego rozdziału dowiesz się, jak rozpoznać urządzenie klienckie za pomocą żądania HTTP, poznasz WURFL i Tera-WURFL. Obecnie działają tysiące urządzeń mobilnych umożliwiających przeglądanie stron internetowych. Może się wydawać, że tworzenie oprogramowania dla starszych przeglądarek jest trudne, ale urządzenia mobilne są jeszcze mniej ustandaryzowane. Na szczęście są dostępne systemy pomocne w procesie tworzenia takiego oprogramowania. Mając na uwadze renderowanie na urządzenia mobilne, pokażemy, jak sprawić przy użyciu WALL, by znaczniki były bardziej abstrakcyjne, jak automatycznie zmieniać rozmiar obrazków i spowodować, by CSS był bardziej płynny. Zaprezentujemy także emulatory urządzeń, omówimy tworzenie aplikacji PHP na urządzenia z Androidem i program Flash Builder dla PHP.
Różnorodność urządzeń Podczas pracy z urządzeniami mobilnymi jednym z największych wyzwań jest zapewnienie czytelności strony po jej wyrenderowaniu. Przy tworzeniu aplikacji internetowych na komputery osobiste sprawdzamy najpopularniejsze przeglądarki, takie jak Chrome, Firefox, Safari, Opera czy Internet Explorer, i być może inne systemy, np. Windows XP, Windows 7, Linux bądź Mac OS X. Zapewnienie wsparcia dla różnych kombinacji przeglądarek i systemów może być pracochłonne. W przypadku urządzeń mobilnych renderowanie jest jeszcze mniej ustandaryzowane i dużo bardziej złożone. Na przykład prawie wszystkie współczesne komputery osobiste umożliwiają wyświetlanie tysięcy kolorów i zapewniają rozdzielczość minimum 800 na 600 pikseli. Jednakże telefony komórkowe, smartfony, tablety, czytniki e-booków i inne urządzenia mobilne mogą mieć ograniczoną paletę kolorów lub tylko skalę szarości. Rozmiary wyświetlaczy także znacznie się różnią. To są tylko trzy parametry — istnieją setki innych, którymi mogą się różnić poszczególne urządzenia. Omówimy kilka z tych parametrów w dalszej części tego rozdziału. W przeciwieństwie do tworzenia stron przeznaczonych dla komputerów stacjonarnych naiwne próby programowania dla każdego urządzenia oddzielnie są nierealne lub przynajmniej zajęłyby zbyt wiele czasu i pracy, niż ktokolwiek byłby skłonny na to poświęcić. Zamiast tego omówimy systemy pozwalające na określenie urządzenia i wyrenderowanie strony dynamicznie oraz płynne zmienianie stylów CSS.
ROZDZIAŁ 3. MOBILNE PHP
Rozpoznanie urządzenia Pierwszym krokiem różnicowania zawartości strony jest sprawdzenie, dla jakiego urządzenia ta strona będzie renderowana. Przeanalizujemy kilka technik pozwalających na ustalenie, jakie urządzenie jest używane.
Aplikacja kliencka U podstaw każdego systemu detekcji urządzeń leży nagłówek aplikacji klienckiej wysyłany w standardowym zapytaniu HTTP. W PHP możemy uzyskać dostęp do informacji o aplikacji klienckiej dzięki superglobalnej zmiennej serwerowej $_SERVER['HTTP_USER_AGENT']. Nagłówek takiej aplikacji może zawierać informacje o przeglądarce, silniku renderującym i systemie operacyjnym. Ma postać podobną do poniższego, wygenerowanego dla przeglądarki Firefox 4: Mozilla/5.0 (Windows NT 5.1; rv:2.0) Gecko/20100101 Firefox/4.0
Z tego nagłówka możemy wyczytać, że systemem operacyjnym klienta jest Windows, silnikiem renderującym jest Gecko, natomiast przeglądarką jest Firefox w wersji 4.0. UWAGA. Rozpoznawanie urządzeń nie jest pewne. Nagłówki dla dwóch różnych urządzeń mogą nie być unikalne, chociaż rzadko tak bywa; mogą także być fałszowane, co zostanie omówione w dalszej części rozdziału.
Wbudowane funkcje PHP PHP ma funkcję get_browser, która próbuje uzyskać informacje o wykorzystywanej przeglądarce. Funkcja działa na podstawie pliku browscap.ini. W tym kontekście jest jak prostsza, ograniczona wersja systemu WURFL, który będzie omówiony dalej. UWAGA. Ta funkcja wymaga zainstalowanego w systemie pliku browscap.ini oraz ustawienia ścieżki do niego w pliku php.ini, na przykład: browscap = "C:\twoja\sciezka\do\browscap.ini"
Więcej informacji o funkcji get_browser można uzyskać pod adresem http://php.net/manual/pl/function.get-browser.php, natomiast aktualne pliki browscap.ini pod adresem http://browsers.garykeith.com/downloads.asp.
Jeżeli pierwszy parametr będzie ustawiony na wartość null lub przekazany zostanie nagłówek aplikacji klienckiej, uzyskamy informacje o przeglądarce. Możemy także przekazać inny nagłówek, aby uzyskać informacje o nim. Drugi parametr jest opcjonalny. Ustawienie go na wartość true spowoduje zwrócenie wyniku w postaci tabeli zamiast domyślnego obiektu (listingi 3.1 i 3.2). Listing 3.1. Wykorzystanie funkcji get_browser
Listing 3.2. Wynik dla przeglądarki Chrome Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.24
Jak widać, funkcja nie zwraca żadnych informacji dla tej nowej przeglądarki. Dzieje się tak dlatego, że plik browscap.ini załączony do serwera WAMP (Windows, Apache, MySQL, PHP) ma już ponad rok. Rozwiązaniem problemu jest pobranie aktualnej wersji pliku. Może także być potrzebny restart serwera, jeżeli plik zostanie złożony w pamięci podręcznej. Po aktualizacji pliku otrzymamy dokładniejsze informacje, pokazane na listingu 3.3. Listing 3.3. Wynik dla przeglądarki Chrome po aktualizacji pliku browscap.ini Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.24 array 'browser_name_regex' => string '§^mozilla/5\.0 \(.*windows nt 6\.1.*wow64.*\) ´applewebkit/.*\(khtml, like gecko\).*chrome/11\..*safari/.*$§' (length=108) 'browser_name_pattern' => string 'Mozilla/5.0 (*Windows NT 6.1*WOW64*) AppleWebKit/* (KHTML, ´like Gecko)*Chrome/11.*Safari/*' (length=90) 'parent' => string 'Chrome 11.0' (length=11) 'platform' => string 'Win7' (length=4) 'win32' => string '' (length=0) 'win64' => string '1' (length=1) 'browser' => string 'Chrome' (length=6) 'version' => string '11.0' (length=4) 'majorver' => string '11' (length=2) 'frames' => string '1' (length=1) 'iframes' => string '1' (length=1) 'tables' => string '1' (length=1) 'cookies' => string '1' (length=1)
Jeżeli zależy Ci na sprawdzeniu tylko kilku głównych urządzeń mobilnych, możesz wykorzystać wyrażenia regularne w celu przeszukania nagłówka aplikacji klienckiej. Na listingu 3.4 sprawdzamy, czy zapytanie przyszło z jednego z popularnych telefonów. Jeżeli łańcuch zostanie znaleziony, przekierowujemy żądanie do odrębnej strony przeznaczonej dla urządzeń mobilnych oraz ładujemy alternatywny szablon i arkusz stylów. Opcja /i w wyrażeniu regularnym (Regex) powoduje, że nasze zapytanie ignoruje wielkość znaków. Znak | oznacza „lub” — zostanie znaleziony zarówno łańcuch „iPhone”, jak i „iPad”, ale nie „iPod”. Podobnie będą znalezione „windows ce” oraz „windows phone”, ale nie „windows xp”. Zajrzyj do dodatku „Wyrażenia regularne”. Listing 3.4. Wykorzystanie wyrażeń regularnych w celu zweryfikowania urządzeń mobilnych
Aby wykryć więcej urządzeń, potrzebujemy znacznie większego wyrażenia regularnego. Możemy skorzystać z popularnej strony http://detectmobilebrowsers.com/, pozwalającej generować wyrażenie regularne w kilku różnych językach programowania i dla różnych frameworków. Wygenerowany skrypt przekieruje klienta na witrynę przeznaczoną dla urządzeń mobilnych. Listing 3.5 pokazuje przykładowy skrypt wygenerowany przez wyżej wymienioną stronę. Listing 3.5. Wyrażenie regularne wygenerowane przez stronę detectmobilebrowsers.com
50
To rozwiązanie będzie poprawne tylko w niektórych przypadkach. Aby uzyskać dokładniejsze wyniki oraz rozpoznać możliwości urządzenia, potrzebny jest zaawansowany system. Ten system to WURFL, omówiony w kolejnym podrozdziale.
Rozpoznawanie możliwości urządzenia System WURFL pozwala wyjść poza proste wykrywanie rodzaju urządzenia i ustalić, jakie są jego możliwości.
WURFL Wireless Universal Resource FiLe (WURFL) jest plikiem XML opracowanym przez Luca Passaniego, zawierającym informacje o możliwościach urządzeń mobilnych.
Wprowadzenie Aktualnie w pliku WURFL jest ponad pięćset różnych właściwości urządzeń mobilnych. Implementacje WURFL zostały opracowane w wielu językach i na wiele platform, włączając w to Javę i .NET. Dla PHP oficjalne API to The New PHP WURFL API, dostępne pod adresem http://wurfl.sourceforge.net/nphp/. Właściwości urządzeń są ułożone hierarchicznie. Jeżeli właściwość nie jest wyszczególniona dla danego modelu, to sprawdzany jest ogólniejszy typ urządzenia. Jeśli właściwość ponownie nie zostanie znaleziona, WURFL sprawdza kolejny ogólniejszy typ urządzenia i powtarza ten proces, dopóki nie dotrze do korzenia pliku. Struktura hierarchiczna oszczędza przestrzeń na dysku i przyspiesza wyszukiwanie. WURFL próbuje także wykorzystywać wersje pliku XML spakowaną za pomocą ZipArchiwe, pakietu dostępnego w PHP od wersji 5.2.0. Ponieważ wersja ZIP pliku ma aktualnie poniżej megabajta (MB), a wersja rozpakowana pliku ponad 16 MB, jest to duże usprawnienie. Niektóre użyteczne właściwości to rozdzielczość ekranu, kodeki i formaty lub wsparcie dla JavaScriptu, Javy czy Flasha. UWAGA. Plik XML jest tworzony głównie przez developerów i użytkowników, więc może zawierać błędy. Ponadto na rynek ciągle trafiają nowe urządzenia. Pomimo rozmiarów i kompletności pliku WURFL nie powinniśmy nigdy ufać mu w stu procentach. Jeżeli potrzebujesz szybko wykorzystać informacje o urządzeniu, możesz wyszczególnić jego właściwości oraz umieścić informacje o nim w głównym pliku XML. Jeżeli dokładność informacji ma kluczowe znaczenie, można użyć systemów o znacznie większej skuteczności.
Instalacja Na potrzeby wszystkich przykładów z tego rozdziału umieścimy bibliotekę WURFL w katalogu wurfl, który będzie się znajdował w głównym katalogu web jako ./wurfl/. Wykorzystamy standardowy plik konfiguracyjny, a obiekt WURFLManager będziemy za każdym razem pozyskiwać za pomocą kodu z listingu 3.6.
51
ROZDZIAŁ 3. MOBILNE PHP
Listing 3.6. Utworzenie obiektu WURFLManager: wurflSetup.php create(); } ?>
Rozpoznawanie urządzeń za pomocą WURFL W naszym pierwszym przykładzie rozpoznawania urządzenia wyświetlimy stos urządzenia przy wykorzystaniu WURFL PHP API. Wyświetlimy hierarchię aplikacji klienckiej z zastosowaniem właściwości fallback oraz id (listing 3.7). Listing 3.7. Wyświetlenie stosu dla aplikacji klienckiej od szczegółowej do ogólnej getDeviceForHttpRequest($_SERVER); print "
Poniżej znajduje się wynik działania skryptu na komputerze stacjonarnym dla przeglądarki Firefox 4: Stos Id: firefox_1 firefox generic_web_browser generic_xhtml generic
oraz dla przeglądarki Chrome: Stos Id: google_chrome_1 google_chrome generic_web_browser generic_xhtml generic
52
ROZDZIAŁ 3. MOBILNE PHP
UWAGA. Uruchomienie skryptu po raz pierwszy może zająć dużo czasu, ponieważ WURFL buduje bufor. Konieczne może być zwiększenie wartości dla dyrektywy max_execution_time w pliku php.ini.
Jeżeli chcemy emulować inne urządzenie, możemy wprowadzić inną zmienną serwerową zawierającą nagłówek aplikacji klienckiej. Zmodyfikowana wersja listingu 3.7 pokazana jest na listingu 3.8. Wynik skryptu przedstawiono na listingu 3.9. Listing 3.8. Emulowanie innego urządzenia — wprowadzenie innej zmiennej serwerowej getDeviceForHttpRequest( $_SERVER ); print "
Listing 3.9. Wynik działania WURFL dla emulowanego iPhone’a 4 Stos ID: apple_iphone_ver4_sub405 apple_iphone_ver4 apple_iphone_ver3_1_3 apple_iphone_ver3_1_2 apple_iphone_ver3_1 apple_iphone_ver3 apple_iphone_ver2_2_1 apple_iphone_ver2_2 apple_iphone_ver2_1 apple_iphone_ver2 apple_iphone_ver1 apple_generic generic_xhtml generic
Rozpoznawanie i wyświetlanie właściwości urządzenia za pomocą WURFL Na listingu 3.10 pokazano dostępne grupy właściwości, które możemy sprawdzić. Wyświetlimy wszystkie dostępne właściwości dla grup display i css. Wynik pokazany jest na listingu 3.11. Listing 3.10. Wyświetlanie dostępnych grup atrybutów
53
Listing 3.11. Wynik działania skryptu z listingu 3.10 ajax bearer bugs cache chtml_ui css display drm flash_lite html_ui image_format j2me markup mms object_download pdf playback product_info rss security sms sound_format storage streaming transcoding wap_push wml_ui wta xhtml_ui
Aby wyświetlić listę wszystkich dostępnych właściwości, możemy zmodyfikować skrypt z listingu 3.10, tak by wykorzystywał metodę getCapabilitiesNameForGroup. Zmodyfikowany skrypt pokazany jest na listingu 3.12. Początkowa część wyniku widnieje na listingu 3.13. Listing 3.12. Wyświetlanie listy wszystkich właściwości, które mogą być sprawdzone getDeviceForHttpRequest( $_SERVER ); $grupy_wlasciwosci = $wurflManager->getListOfGroups(); asort( $grupy_wlasciwosci ); foreach ( $grupy_wlasciwosci as $c ) { print "" . $c . " "; var_dump( $wurflManager->getCapabilitiesNameForGroup( $c ) ); } ?>
Możemy zmodyfikować skrypt z listingu 3.12 tak, aby wyświetlały się tylko niektóre właściwości urządzenia, by wspierane właściwości były oznaczone kolorem zielonym i znacznikami (renderowanymi przez encje HTML) oraz by niewspierane właściwości były oznaczone kolorem czerwonym i przekreśleniem (listing 3.14). Wynik pokazany jest na rysunku 3.1. Listing 3.14. Wyświetlanie właściwości urządzeń wraz z kolorowaniem getDeviceForHttpRequest ( $_SERVER ); $grupy_wlasciwosci = $wurflManager->getListOfGroups (); asort ( $grupy_wlasciwosci ); foreach ( $grupy_wlasciwosci as $grupa ) { //Wyświetlamy właściwości tylko z niektórych grup. if (in_array ( $grupa, array ("ajax", "css", "image_format" ) )) { print "" . $grupa . " "; print "
Rysunek 3.1. Wynik działania skryptu z listingu 3.14, wyświetlającego właściwości urządzenia w przejrzystej formie W ostatnim skrypcie wykorzystującym WURFL API wyświetlimy niektóre właściwości urządzenia (listing 3.15). Listing 3.15. Wyświetlenie niektórych właściwości iPhone’a 4 dotyczących dźwięku i wyświetlania getDeviceForHttpRequest($_SERVER); //wyświetlenie interesujących nas pól //informacje dotyczące wyświetlania print "
Tera-WURFL Implementacja WURFL, nazwana Tera-WURFL, dostępna jest pod adresem http://www.tera-wurfl.com. Poprzednio opisane PHP WURFL API budowane jest główne z myślą o trafności wyników. Tera-WURFL skupione jest na wydajności. Aby osiągnąć wyższą wydajność, zamiast dużego pliku XML jest wykorzystywana baza danych. Aktualne Tera-WURFL wspiera MySQL, Microsoft SQL Server i MongoDB. Tera-WURFL może być do pięciu razy wydajniejszy niż zwykły WURFL (sprawdza się to w 99% przypadków), zapewnia też lepsze wykrywanie właściwości dla urządzeń stacjonarnych. Dodatkowo Tera-WURFL pozwala na pokazanie zdjęcia urządzenia, które zostało użyte. W dalszej części wyjaśnimy, jak wyświetlić zdjęcie urządzenia.
Instalacja Aby zainstalować Tera-WURFL, należy wykonać następujące kroki: 1. Utworzyć bazę danych oraz zmodyfikować dane dostępowe w pliku konfiguracyjnym TeraWurflConfig.php. 2. Otworzyć stronę administracyjną http://localhost/Tera-WURFL/admin/. Jeżeli pojawi się błąd dotyczący brakujących tabel, nie przejmuj się — tabele zostaną utworzone, kiedy będą wczytywane dane. 3. Możesz wczytać lokalny plik XML lub zdalny plik XML. UWAGA. Jeżeli pojawi się informacja o błędzie, np. „fatal error maximum function nesting level of '100' reached aborting”, musisz tymczasowo wyłączyć opcję xdebug w pliku php.ini lub zwiększyć liczbę możliwych zagłębień xdebug poprzez ustawienie w pliku php.ini dyrektywy xdebug.max_nesting_level=100 na wyższą wartość, np. 500.
Rozpoznawanie urządzeń za pomocą Tera-WURFL W pierwszym przykładzie, pokazanym na listingu 3.16, wykorzystamy nagłówek urządzenia klienckiego dla iPhone’a 4 i sprawdzimy, czy Tera-WURFL został zainstalowany poprawnie i czy go rozpozna. Listing 3.16. Kod Tera-WURFL sprawdzający urządzenie klienckie
57
ROZDZIAŁ 3. MOBILNE PHP
require_once('Tera-WURFL/TeraWurfl.php'); $teraWURFL = new TeraWurfl(); $iphone = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_0 like Mac OS X; en-us) AppleWebKit/532.9 ´KHTML, like Gecko) Version/4.0.5 Mobile/8A293 Safari/6531.22.7"; if ( $teraWURFL->getDeviceCapabilitiesFromAgent( $iphone ) ) { print "ID: ".$teraWURFL->capabilities['id']." "; } else { print "urządzenie nie zostało rozpoznane"; } ?>
Wynik działania skryptu to: ID: apple_iphone_ver4_sub405
Gdybyśmy nie przekazali nagłówka urządzenia klienckiego jako parametru, tak jak w przypadku WURFL, wynikiem byłby klient, który został wykorzystany do otwarcia strony.
Rozpoznawanie właściwości urządzenia i tworzenie ich listy za pomocą Tera-WURFL Skrypt z listingu 3.17 tworzy listę właściwości dotyczących wyświetlania i dźwięku dla iPhone’a 4. Jest to wersja skryptu z listingu 3.15 wykorzystująca Tera-WURFL. Listing 3.17. Wyświetlenie właściwości dotyczących dźwięku i wyświetlania dla iPhone’a 4 przy użyciu Tera-WURFL getDeviceCapabilitiesFromAgent( $iphone ) ) { $marka_urzadzenia = $teraWURFL->getDeviceCapability( "brand_name" ); $model_urzadzenia = $teraWURFL->getDeviceCapability( "model_name" ); $model_dodatkowe_info = $teraWURFL->getDeviceCapability( "model_extra_info" ); //Wyświetlenie informacji, które nas interesują. print "
} else { print "urządzenie nie zostało rozpoznane"; } ?>
Wynik działania skryptu to: Apple iPhone 4.0 Informacje dotyczące wyświetlania: 320 x 480 : 65536 kolorów podwójna orientacja: 1 Wspierane formaty audio: aac mp3
Wyświetlenie zdjęcia urządzenia przy wykorzystaniu Tera-WURFL W ostatnim przykładzie dotyczącym Tera-WURFL pokażemy, jak wyświetlić zdjęcie urządzenia. Najpierw należy pobrać zestaw zdjęć urządzeń znajdujący się pod adresem http://sourceforge.net/projects/wurfl/files/WURFL%20Device%20Images/. Następnie trzeba rozpakować archiwum i umieścić jego zawartość w miejscu dostępnym dla usługi web. My utworzymy folder zdjecia_urzadzen w katalogu /Tera-WURFL/. Rozbudujemy poprzedni przykład z listingu 3.17. Najpierw dodamy do skryptu kolejny plik z biblioteki Tera-WURFL, a później kod pobierający odpowiedni obrazek i wyświetlający go (listing 3.18). Wynik jego działania jest pokazany na rysunku 3.2. Listing 3.18. Wyświetlenie zdjęcia urządzenia getDeviceCapabilitiesFromAgent ( $iphone )) { $marka_urzadzenia = $teraWURFL->getDeviceCapability( "brand_name" ); $model_urzadzenia = $teraWURFL->getDeviceCapability( "model_name" ); $model_dodatkowe_info = $teraWURFL->getDeviceCapability( "model_extra_info" ); //Wyświetlenie informacji, które nas interesują. print "
"; //zdjęcie $zdjecie = new TeraWurflDeviceImage ( $teraWURFL ); //adres na serwerze $zdjecie->setBaseURL ( '/Tera-WURFL/zdjecia_urzadzen/' ); //adres w systemie plików $zdjecie->setImagesDirectory ( $_SERVER ['DOCUMENT_ROOT'] . '/Tera-WURFL/zdjecia_urzadzen/' ); $zdjecie_src = $zdjecie->getImage (); if ($zdjecie_src) { print ''; } else {
59
ROZDZIAŁ 3. MOBILNE PHP
Rysunek 3.2. Wynik działania skryptu z listingu 3.18 — wyświetlenie zdjęcia urządzenia echo "Zdjęcie niedostępne"; } //informacje dotyczące wyświetlania print "
"; } else { print "urządzenie nie zostało rozpoznane"; } ?>
Narzędzia renderujące Aby dynamicznie modyfikować zawartość strony dla różnych urządzeń mobilnych, możemy wykorzystać abstrakcyjne znaczniki przy zastosowaniu Wireless Abstraction Library (WALL), automatyczną zmianę rozmiarów obrazków i właściwości CSS dotyczące mediów.
WALL Luca Passani poza WURFL opracował także WALL — bibliotekę tworzącą abstrakcyjne znaczniki dla urządzeń mobilnych. Co to dokładnie znaczy? Najpierw trzeba zauważyć, że w odróżnieniu od stron dla normalnych przeglądarek na urządzenia stacjonarne, pisanych w HTML lub XHTLM, dla urządzeń mobilnych jest więcej wariantów znaczników zawierających rozbieżności.
60
ROZDZIAŁ 3. MOBILNE PHP
Najpopularniejszymi zestawami znaczników dla urządzeń mobilnych są: • XHTML MP (Mobile Profile), • CHTML (Compact HTML), • HTML. Część wspólna akceptowanych tagów we wszystkich tych językach jest ograniczona. Np. znacznik nowej linii utworzony jako zamiast lub na odwrót może zostać zignorowany bądź wywołać błąd w zależności od zastosowanego języka. Korzystając z WALL, możemy zapisać nową linię jako . Dzięki WURFL znajdziemy język odpowiedni dla danego urządzenia poprzez sprawdzenie właściwości preffered_markup. Mając tę informację, WALL wyrenderuje odpowiednie znaczniki lub . WALL napisano pierwotnie dla Java Server Pages (JSP). Biblioteka WALL4PHP, dostępna pod adresem http://laacz.lv/dev/Wall/, została utworzona na potrzeby PHP. Istnieje także zaktualizowana wersja biblioteki, utrzymywana przez twórców Tera-WURFL, dostępna pod adresem https://github.com/kamermans/WALL4PHP-by-Tera-WURFL. Na potrzeby przykładów zastosujemy pierwotną implementację. Dokładne instrukcje integracji bibliotek WALL i WURFL można znaleźć pod adresem http://www.tera-wurfl.com/wiki/index.php/WALL4PHP. Plik PHP wykorzystujący WALL mógłby wyglądać jak na listingu 3.19. Listing 3.19. Dokument wykorzystujący WALL WALL jest superNagłówekTo będzie paragraf w HTMLAB
Efekt renderowania będzie zależny od urządzenia. Jeżeli urządzenie wspiera HTML, kod zostanie wyrenderowany zgodnie z listingiem 3.20. Listing 3.20. Kod wyrenderowany dla urządzenia wspierającego HTML WALL jest super
Reagujący CSS Aby rozmieszczenie elementów na stronie było odpowiednie dla urządzenia, możemy zastosować pływające kontenery oraz zmieniać rozmiar zdjęć, jak pokazano powyżej. Możemy także wykorzystać arkusze przeznaczone specjalnie dla urządzeń mobilnych. Niedawnym usprawnieniem w CSS są zapytania o media. Właściwości, jakie można sprawdzić, to width, height, device-width, device-height, orientation, aspect-ratio, device-aspect-ratio, color, color-index, monochrome, resolution, scan i grid (listing 3.21). Listing 3.21. Przykładowe zapytania o media za pomocą CSS3 @media screen and (min-device-width:400px) and (max-device-width:600px){ /* ograniczenie szerokości urządzenia */ } @media screen and (orientation:landscape){ /* dobre dla urządzeń mogących działać w dwóch płaszczyznach, takich jak iPad i Kindle */ }
Głębsza analiza CSS wyszłaby poza zakres tej książki, jednakże pod adresem http://www.netmagazine.com/ tutorials/adaptive-layouts-media-queries dostępny jest doskonały artykuł. Opisane tam techniki mogą usprawnić wyświetlanie stron na urządzeniach mobilnych. Więcej informacji dotyczących zapytań o media w CSS3 znajduje się pod adresem http://www.w3.org/TR/css3-mediaqueries/.
Emulatory i SDK W ramach pomocy dla twórców mających ograniczony budżet, niepozwalający na zakup telefonów na potrzeby testów, oraz w celu ułatwienia pracy powstało wiele emulatorów i zestawów narzędzi dla programistów (Software Developer Kit — SDK) piszących aplikacje przeznaczone dla urządzeń mobilnych. Niektóre narzędzia emulują jedno urządzenie, inne kilka jednocześnie. Oto wybrane adresy, pod którymi są one dostępne: • Android: http://developer.android.com/guide/developing/tools/emulator.html • Apple: http://developer.apple.com/devcenter/ios/index.action • BlackBerry: http://www.blackberry.com/developers/downloads/simulators/ • Kindle: http://www.amazon.com/kdk/ • Opera Mini: http://www.opera.com/mobile/demo/ • Windows: http://create.msdn.com/en-us/resources/downloads
Tworzenie dla systemu Android System Android wydany przez Google może uruchamiać aplikacje w Java i natywnym C. Projekt warstwy skryptów dla Androida (SL4A) dostępny pod adresem http://code.google.com/p/android-scripting/ pozwala na tworzenie aplikacji w językach skryptowych. Jednakże PHP aktualnie nie należy do wspieranych języków. Aby tworzyć aplikacje dla Androida w PHP, możemy wykorzystać projekt open source PHP for Android, dostępny pod adresem http://www.phpforandroid.net/. Narzędzie to oferuje nieoficjalne wsparcie dla PHP wewnątrz SL4A poprzez plik APK (Android Package).
Adobe Flash Builder dla PHP Niedawno Zend ogłosiło połączenie sił z Adobe w celu wprowadzenia wsparcia dla PHP w aplikacji Flash Builder 4.5 (rysunek 3.3). Więcej informacji o Flash Builder dla PHP można znaleźć pod adresem http://www.zend.com/en/ products/studio/flash-builder-for-php/. Flash Builder dla PHP zawiera Zend Studio w zintegrowanym środowisku programistycznym (IDE). Jako frontend może być wykorzystany Flex, natomiast jako backend — PHP. 62
ROZDZIAŁ 3. MOBILNE PHP
Rysunek 3.3. Ogłoszenie dotyczące programu Flash Builder na stronie Zend IDE ma ułatwiać tworzenie kodu i zapewniać lepszą jego przenośność pomiędzy platformami mobilnymi. Może nawet skompilować kod Flex, tak aby był wykonywany natywnie na urządzeniach opartych na systemie iOS, takich jak iPhone i iPad.
Kody QR Kody QR (Quick Response) są czymś w rodzaju dwuwymiarowego kodu kreskowego. Zostały wprowadzone w Japonii ponad dwadzieścia lat temu w celu katalogowania części samochodowych. Nowoczesne urządzenia mobilne z wbudowanymi aparatami fotograficznymi przyczyniły się do rozpowszechnienia tego rozwiązania. Kod QR zazwyczaj reprezentuje adres URL, ale może zawierać więcej tekstu. Pokażemy, w jaki sposób łatwo wygenerować kody QR za pomocą trzech różnych bibliotek. Dwie z nich, TCPDF i Google Chart API, są omówione dokładniej w rozdziale 10. Pierwsza biblioteka, za której pośrednictwem wygenerujemy kod QR, jest dostępna pod adresem http://www.tcpdf.org/. Za pomocą TCPDF możemy wygenerować kod QR jako PDF, nie możemy jednak generować kodów jako odrębnych plików graficznych. Zobacz listing 3.22 i rysunek 3.4 przedstawiający wygenerowany kod QR. Listing 3.22. Generowanie kodu QR w pliku PDF przy wykorzystaniu biblioteki TCPDF AddPage(); //dodanie nowej strony $pdf->write2DBarcode( 'Witaj, świecie kodów QR', 'QRCODE' ); //napisz 'Witaj, świecie kodów QR' jako kod QR $pdf->Output( 'qr_code.pdf', 'I' ); //wygeneruj i wyślij plik pdf ?>
63
ROZDZIAŁ 3. MOBILNE PHP
Rysunek 3.4. Kod QR dla ciągu znaków „Witaj, świecie kodów QR”. Każda biblioteka powinna wygenerować taki sam obrazek Aby zapisać kod QR do pliku, możemy wykorzystać bibliotekę phpqrcode, dostępną pod adresem http://phpqrcode.sourceforge.net/index.php (listing 3.23). Listing 3.23. Generowanie kodów QR bezpośrednio do pliku lub przeglądarki za pośrednictwem phpqrcode
Możemy także wykorzystać Google Chart API znajdujące się pod adresem http://code.google.com/p/gchartphp/ (listing 3.24). Listing 3.24. Generowanie kodów QR za pośrednictwem biblioteki qrcodephp setQRCode( 'Witaj, świecie kodów QR' ); echo "getUrl()."\" />"; ?>
Podsumowanie W tym rozdziale omówiliśmy rozpoznawanie urządzeń mobilnych i ich właściwości. Obecnie nie ma idealnego systemu wykrywania urządzeń, jednak to, co mamy, jest względnie niezawodne. Programista powinien być czujny i mieć aktualne pliki, niezależnie od tego, czy wykorzystuje browscap, WURFL, czy inny system. Pokazaliśmy także narzędzia do tworzenia abstrakcyjnego kodu, automatycznej zmiany rozmiarów obrazków oraz płynnej zmiany rozmiarów zawartości strony. Kiedy tylko jest to możliwe, używaj narzędzi, które wykonają pracę za Ciebie. Mogą one sprawdzić, jakie urządzenie zostało użyte i co ono potrafi; mogą też pomóc w ustaleniu, w jaki sposób przetransformować istniejące style, obrazki i kod. Automatyzacja i przydatne biblioteki powinny być stosowane we wszystkich dziedzinach tworzenia oprogramowania. Technologie mobilne ciągle się rozwijają, więc także i metody tworzenia oprogramowania szybko ulegają zmianie. Aby zostać dobrym twórcą oprogramowania dla urządzeń mobilnych, musisz przyswajać sobie dobre praktyki, poznawać najnowsze technologie oraz wchodzące na rynek SDK i API.
64
ROZDZIAŁ 4
Media społecznościowe
Media społecznościowe wykorzystują technologię na potrzeby społecznej interakcji i współpracy. Dwa najpopularniejsze portale społecznościowe, Twitter i Facebook, zrzeszają miliony użytkowników. Nakręcono nawet film o powstaniu Facebooka. Z kolei Twitter, który zaczął działać w roku 2006, stał się najpopularniejszą na świecie platformą mikroblogową. Pojawiły się już miliardy twittów (wiadomości mających mniej niż 140 znaków), wpisywanych przez internet lub za pośrednictwem SMS-ów. Facebook, nieprawdopodobnie szybko rozwijające się przedsięwzięcie Marka Zuckerberga, skupia na sobie powszechną uwagę — magazyn „Time” ogłosił Zuckerberga człowiekiem roku, film The Social Network ma wysoką oglądalność, w mediach nieustannie mówi się o portalu (często jest dyskutowana tematyka prywatności). Zarówno Facebook, jak i Twitter uwierzytelniają użytkowników za pomocą OAuth. W tym rozdziale będzie wyjaśnione, czym jest OAuth i jak się z nim połączyć. Twitter udostępnia trzy interfejsy programistyczne (API). Istnieje API publicznego wyszukiwania, GET wykorzystujący zapytania oraz dwa REST API, z których jedno dostarcza informacje o użytkownikach i akcjach wewnątrz sieci prywatnej, a drugie zapewnia strumieniowanie dużych wolumenów z niską latencją. Pokażemy tu, w jaki sposób używać publicznego oraz prywatnego API po wcześniejszym zalogowaniu. Na Twitterze możesz mieć przyjaciół zdefiniowanych jako osoby, które Cię śledzą (follow); podobnie Ty możesz śledzić ich. Pokażemy Ci, jak wygenerować listę przyjaciół z Twittera wraz z ich statusami. Omówimy także zaawansowane sposoby powiązania loginu z Twittera z uwierzytelnianiem na Twojej stronie przy wykorzystaniu bazy danych do przechowywania danych użytkowników. Przeanalizujemy ponadto zastosowanie bazy jako bufora służącego do ochrony przed zbyt dużą liczbą zapytań do Twittera. Facebook udostępnia dobrze zaprojektowane API i oficjalne PHP SDK. Dowiesz się z tego rozdziału, jak opracować nową aplikację na Facebooku, poznasz metodę uwierzytelniania oraz przykładowe zapytania wykonywane przez API.
OAuth Nazwa „OAuth” jest skrótem od Open Authentication („otwarte uwierzytelnianie”). OAuth wykorzystuje klucze wygenerowane dla aplikacji, które są ważne przez dany okres. Uwierzytelnianie to zachodzi pomiędzy aplikacją kliencką a usługodawcą. Podstawowe etapy uwierzytelniania za pośrednictwem OAuth są następujące: 1. Aplikacja OAuth wysyła token klienta do usługodawcy (np. Facebooka lub Twittera) — w zamian odbiera tokeny żądania. 2. Użytkownik udziela żądanych uprawnień. 3. Do sprawdzenia zapytania o uprawnienia jest wykorzystywany URL zwrotny lub numer identyfikacyjny użytkownika (PIN).
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
4. Tokeny żądania i PIN są wymieniane na tokeny dostępu. 5. Użytkownik może korzystać z aplikacji z tokenami dostępu. UWAGA. Więcej informacji dotyczących OAuth można uzyskać pod adresem http://oauth.net/. Dobrym źródłem w Twitterze jest strona https://dev.twitter.com/docs/auth. Gdy powstawała ta książka, dwoma najpopularniejszymi modułami OAuth dla PHP były PECL OAuth i Zend_Oauth. Informacje dotyczące PECL OAuth można znaleźć pod adresem http://www.php.net/manual/en/book.oauth.php, natomiast o Zend_Oauth pod adresem http://framework.zend.com/manual/en/zend.oauth.html.
Twitter zapewnia trzy mechanizmy dotyczące OAuth. Jeżeli potrzebujesz tylko połączyć się z kontem właściciela aplikacji, otrzymasz tokeny dostępowe, które można wykorzystać bezpośrednio. Dzięki temu będziesz mógł pominąć pierwsze cztery kroki wymienione powyżej. Jeżeli Twoja aplikacja pozwala wielu użytkownikom uzyskać dostęp do ich kont, możesz albo zastosować metodę sprawdzania numeru PIN dla aplikacji klienckich, albo zdefiniować stronę z odwołaniem zwrotnym, która wyeliminuje konieczność uwierzytelniania się użytkownika. Zajmiemy się tymi metodami w dalszej części tego rozdziału.
Twitter API publicznego wyszukiwania Aby wyszukiwać tweety, nie musimy być uwierzytelniani. Zgodnie z dokumentacją Twittera, dostępną pod adresem https://dev.twitter.com/docs/api/1/get/search, adres URL wyszukiwania to http://search.twitter.com/search.format, natomiast przyjęte formaty to JSON i Atom. Można ustawić opcjonalne parametry, takie jak język, geokod, początkowy i końcowy interwał czasowy oraz miejsce. Zapytanie wyszukujące reprezentowane jest przez q. Wykorzystywana jest standardowa notacja Twittera — @ dla użytkowników, # dla tagów. Zapytanie http://search.twitter.com/search.json?q=montreal&lang=en&until=2010-10-21 zwróciłoby wszystkie posty ze słowem „montreal” napisane po angielsku utworzone przed 21 października 2010 r. Listing 4.1 przedstawia prosty formularz wyszukiwania postów na Twitterze. Listing 4.1. Przykład wyszukiwania twitter_get_search.php
66
Pierwsza część skryptu z listingu 4.1 wyświetla prosty formularz, zawierający pole tekstowe przeznaczone do wprowadzania zapytania oraz przycisk zatwierdzający wysłanie formularza. Kolejna część sprawdza zatwierdzenie formularza i koduje zapytanie. Następnie funkcja file_get_contents pobiera wynik z wygenerowanego adresu URL. Aby przekonwertować obiekt w formacie JSON na obiekt PHP, stosujemy funkcję json_dekode. Zostawiliśmy część kodu w postaci komentarza, który może posłużyć w zidentyfikowaniu dostępnych pól. Rezultat wykorzystywany jest w pętli, a jego elementy wyświetlane są w formie tabeli HTML. Zwróć uwagę na wywołanie error_reporting(E_ALL ^ E_NOTICE); na początku skryptu, które powoduje wyświetlenie wszystkich informacji o błędach poza zawiadomieniami (notice). To pomoże nam w znalezieniu błędu, gdyby coś poszło nie tak. Więcej informacji o JSON można znaleźć w rozdziale 15.
Prywatne REST API W PHP napisano wiele bibliotek przeznaczonych do współpracy z API Twittera. Większość z tych rozwiązań wykorzystuje kombinację loginu i hasła jako metodę uwierzytelniania w celu połączenia. Od sierpnia 2010 r. Twitter używa do uwierzytelniania mechanizmu OAuth i nie obsługuje uwierzytelniania podstawowego. W związku z tym większość bibliotek do współpracy z Twitterem jest przestarzała lub wymaga aktualizacji. Powodem zmiany uwierzytelniania podstawowego na uwierzytelnianie OAuth była potrzeba zwiększenia bezpieczeństwa. Jedną z najczęściej stosowanych bibliotek współpracujących z API Twittera i OAuth jest biblioteka twitteroauth, dostępna pod adresem https://github.com/abraham/twitteroauth/downloads. W tym rozdziale będziemy korzystać właśnie z tej biblioteki. Biblioteka twitteroauth składa się z dwóch głównych plików, twitteroauth.php i oauth.php. Na potrzeby przykładów umieść oba pliki w katalogu /twitteroauth/ będącym podkatalogiem głównego katalogu web. Musisz także mieć zainstalowaną bibliotekę CURL dla PHP. Wymagane pliki będą się różniły w zależności od tego, jaki masz system. W systemie Windows biblioteka to php_curl.dll; w pliku php.ini musisz dodać lub włączyć linię extension=php_curl.dll. W przypadku Linuksa plik biblioteki to curl.so, a w php.ini niezbędna jest linia extension=php_curl.dll. Możesz także sprawdzić zainstalowane moduły przy wykorzystaniu komendy php –m w wierszu poleceń lub przez wywołanie funkcji php_info.
Zakładanie konta na Twitterze Aby wykonywać przykłady z tego rozdziału, będziesz potrzebował konta na Twitterze. Rejestracja jest szybka i prosta, przeprowadza się ją pod adresem https://twitter.com/signup. Otrzymasz wiadomość e-mail z potwierdzeniem rejestracji. Po uaktywnieniu swojego konta przejdź do części dla developerów: http://dev. twitter.com/. Pod adresem http://dev.twitter.com/apps/new utworzymy nową aplikację do współpracy z OAuth (rysunek 4.1).
67
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Rysunek 4.1. Tworzenie nowej aplikacji UWAGA. Twitter wymaga, aby pole dotyczące strony aplikacji zostało wypełnione. Jeżeli testujesz aplikację lokalnie lub nie masz własnej strony, wpisz po prostu dowolny poprawny adres WWW, np. http://test.pl.
Na potrzeby pierwszego przykładu zastosujemy poniższe ustawienia: Default Access Type: Read-only
Dostępne są dwa typy aplikacji: „client” oraz „browser”. Aplikacja typu „browser” wykorzystuje publiczny adres odwołania zwrotnego; pod tym adresem są odbierane informacje w procesie uwierzytelniania. Aplikacja „client” nie wymaga zewnętrznego dostępu dla usługi OAuth, aby się z nią skomunikować. Zamiast tego przyznawany jest kod PIN, który użytkownik musi podać, aby zakończyć proces uwierzytelniania. Typ dostępu może zostać ustawiony na „read-only” (tylko odczyt — domyślnie) lub „read and write” (odczyt i zapis). Typ „read-only” pozwala na żądanie i odczytywanie informacji, a tryb „read and write” także na wysyłanie informacji z powrotem do aplikacji. Twitter wygeneruje klucz klienta i tajny token klienta, które będziemy wykorzystywać w naszych przykładach (rysunek 4.2). Większość przykładów z tego rozdziału wymaga wykorzystania tokenów użytkownika, więc dla wygody umieścimy je w zewnętrznym pliku (listing 4.2). Listing 4.2. Zdefiniowanie tokenów użytkownika w pliku twitter_config.php
68
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Rysunek 4.2. Wygenerowane przez Twitter tokeny użytkownika
Uwierzytelnianie przy użyciu tokenu dostępu Na dole strony naszej aplikacji widnieje link tworzący token dostępu (Create my access token). Dzięki temu otrzymamy token dostępu i tajny token dostępu — będziemy mogli przeprowadzić uwierzytelnianie bez standardowych kroków OAuth (rysunek 4.3).
Rysunek 4.3. Tokeny bezpośredniego dostępu dla użytkownika Dzięki tokenom możemy nawiązać połączenie za pośrednictwem biblioteki twitteroauth (listing 4.3). Listing 4.3. Proste uwierzytelnianie za pośrednictwem twitteroauth oraz tokenów dostępu — twitter_direct_access.php
69
Powyższy skrypt ładuje bibliotekę twitteroauth i przekazuje token dostępu, tajny token dostępu oraz token użytkownika jako parametry. Musisz oczywiście wstawić prawdziwe wartości w miejsce TOKEN_DOSTEPU i TAJNY_TOKEN_DOSTEPU. Poprawne wykonanie powyższego skryptu spowoduje wyświetlenie linii: Witaj, bdanchilla!
UWAGA. Ważne jest, aby chronić swoje tokeny użytkownika i tokeny dostępu przed osobami niepowołanymi.
Uwierzytelnianie przy wykorzystaniu osobistego numeru identyfikacyjnego (PIN) W tym przykładzie założymy, że uwierzytelnić się próbuje inny użytkownik. W związku z tym nie mamy naszych tokenów dostępu. Do konstruktora twitteroauth przekażemy nasze tokeny klienta, otrzymamy tokeny żądania, a następnie przekierujemy żądanie do części deweloperskiej Twittera. Otrzymamy informację, że skrypt próbuje uzyskać dostęp do aplikacji, i pojawi się pytanie o akceptację lub odrzucenie dostępu. Oczywiście zaakceptujemy prośbę o uzyskanie dostępu i otrzymamy numer PIN. Wprowadzenie tego siedmiocyfrowego PIN-u w drugim skrypcie zakończy rejestrację. Te czynności wykonujemy tylko jeden raz. Metoda ta jest stosowana przy uwierzytelnianiu w niepublicznych skryptach, takich jak aplikacja stacjonarna, lub lokalnie, bez udostępnionej publicznej domeny. W bibliotece twitteroauth możemy wykorzystać PIN, przekazując go jako parametr do funkcji getAccessToken: function getAccessToken($oauth_verifier = FALSE);
W celu uzyskania PIN-u musimy wymienić tokeny klienta na tokeny żądania, a następnie je zarejestrować. Kiedy już będziemy mieć PIN, musimy użyć go wraz z tokenami klienta do uzyskania tokenów dostępu. Gdy otrzymamy tokeny dostępu, będziemy mogli się uwierzytelnić i korzystać z API.
Skrypt otrzyma tokeny żądania, które zostaną zapisane w zmiennej $_SESSION, i przekieruje nas na stronę zawierającą nasz numer PIN (rysunek 4.4).
Rysunek 4.4. PIN podany po przekierowaniu na stronę rejestracji
Krok 2. Sprawdzenie numeru PIN w celu uzyskania tokenów dostępu W celu uzyskania tokenów uruchom skrypt z listingu 4.5, przekazując PIN jako parametr GET. Listing 4.5. Sprawdzenie numeru PIN — twitter_pin_validation.php getAccessToken( $_GET["pin"] ); if ($accessOAuthTokens && $accessOAuthTokens['oauth_token']) { //zapisanie tokenów dostępu w $_SESSION $_SESSION["access_token"] = $accessOAuthTokens['oauth_token']; $_SESSION["access_token_secret"] = $accessOAuthTokens['oauth_token_secret']; return true; } else { print "Błąd: czas na wykorzystanie numeru PIN dobiegł końca!"; return false; } } ?>
71
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
W skrypcie z listingu 4.5 wprowadziliśmy dodatkowe zabezpieczenie, sprawdzające, czy podany numer PIN jest prawidłowy. W przypadku poprawnego zadziałania skryptu otrzymamy informację: „Skrypt walidacji numeru PIN został uruchomiony”; w razie wystąpienia błędu wyświetli się stosowny komunikat. Błąd może być spowodowany brakiem numeru PIN, wprowadzeniem niepoprawnego numeru PIN lub zakończeniem czasu jego ważności. Skrypt ładuje tokeny zapisane podczas wykonywania skryptu z listingu 4.4. Tworzy nowy obiekt TwitterOAuth, tym razem przekazując tokeny żądania oraz dodatkowe parametry. Możemy następnie wywołać metodę getAccessToken, przekazując nasz PIN jako parametr. Otrzymamy tokeny dostępu. Na końcu zapisujemy uzyskane tokeny w sesji lub zwracany jest błąd w przypadku zakończenia ważności numeru PIN. UWAGA. Numer PIN wygenerowany za pomocą skryptu z listingu 4.5 ma określony czas ważności. Jeżeli wywołanie skryptu twitter_sprawdzenie_pin.php nastąpi zbyt późno, pojawi się komunikat o błędzie.
Mimo że OAuth jest bezpieczniejsze niż standardowa autoryzacja oparta na kombinacji loginu i hasła, nadal należy zachować ostrożność. Jeżeli komuś uda się dotrzeć do Twoich tokenów dostępu, będzie mógł uzyskać dostęp do Twojego konta.
Krok 3. Uwierzytelnianie przy zastosowaniu tokenów dostępu dla Twitter API Mamy już dostęp do tokenów, które zostały zapisane w sesji. Pozwolą nam one na uwierzytelnianie i użycie API (listing 4.6). Listing 4.6. Przykładowe wykorzystanie Twittera — twitter_usage.php get( "account/verify_credentials" ); if ( $user_info && !$user_info->error ) { print "Witaj, ".$user_info->screen_name."! "; print "Wstawianie informacji o statusie."; // wysłanie nowego statusu $twitterOAuth->post( 'statuses/update', array( 'status' => "Wstawianie statusu…foobar." ) ); //inne odwołania do API }else{ die( "Błąd weryfikacji danych dostępowych." ); } ?>
Jeżeli uwierzytelnianie przebiegnie prawidłowo, otrzymamy następujący wynik: Witaj, bdanchilla! Wstawianie informacji o statusie.
powinna wstawić informację o statusie. Jeżeli jednak wejdziemy na nasze konto, zobaczymy, że nic nie zostało opublikowane. Dzieje się tak dlatego, że nasza aplikacja nie ma uprawnień do zapisu. Metody REST API są albo typu GET, albo typu POST. Metody typu GET odczytują dane. Metody typu POST zapisują dane. Większość naszych wywołań funkcji będzie dotyczyć odczytu danych i dlatego funkcje te będą wywoływane za pomocą GET. Edytowanie informacji, np. wystawianie nowego statusu lub śledzenie kogoś nowego, wymaga uprawnień do zapisu, czyli wywołania POST. Aby to poprawić, musimy przejść z powrotem na stronę dla developerów i zmienić dla naszej aplikacji domyślny typ dostępu z Read only na Read and Write (rysunek 4.5). Upewnij się, że zapisałeś zmiany w konfiguracji. UWAGA. Jeżeli zmieniasz typ aplikacji lub typ dostępu, może być konieczne usunięcie danych sesji.
Rysunek 4.5. Modyfikacja typu dostępu dla aplikacji Uruchom ponownie poprzedni skrypt. Nowy status powinien zostać przekazany.
Przykładowe wykorzystanie API — statusy przyjaciół W kolejnym przykładzie, pokazanym na listingu 4.7, wyświetlimy ostatnie statusy naszych przyjaciół. Połączymy się z API tak jak poprzednio. Następnie wywołamy statuses/friends, co zwróci ostatnie statusy naszych przyjaciół. Jest to zbliżone do przykładu wyszukiwania za pośrednictwem publicznego API, jednak pokazuje naszych przyjaciół, a nie wszystkie osoby. Wywołamy także shuffle($friends), aby lista przyjaciół była losowa. Listing 4.7. Wyświetlenie ostatniego statusu przyjaciół — friend_status.php
73
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
require_once("twitter_config.php"); session_start(); //Skoro znamy tokeny dostępu, możemy je przekazać do konstruktora. $twitterOAuth = new TwitterOAuth( CONSUMER_KEY, CONSUMER_SECRET, $_SESSION["access_token"], $_SESSION["access_token_secret"] ); //sprawdzenie danych dostępowych za pośrednictwem API $user_info = $twitterOAuth->get( "account/verify_credentials" ); if ( $user_info && !$user_info->error ) { echo '
Uwierzytelnianie poprzez odwołanie zwrotne Podczas realizacji poprzedniego przykładu na pewno pomyślałeś, że musi być lepsza metoda uwierzytelniania niż kopiowanie numeru PIN i ręczne wstawianie go w odrębnym kroku. Tak rzeczywiście jest. Ten sposób wymaga serwera web dostępnego publicznie, do którego usługa OAuth (Twitter) będzie mogła wykonać odwołanie zwrotne do klienta (nasza aplikacja) (rysunek 4.7). Mając miejsce, do którego OAuth może się odwołać, musimy wykonać o jeden krok mniej niż podczas uwierzytelniania z numerem PIN, ponieważ skrypt odwołania zadziała jako weryfikacja. Jest dowodem, że jesteś właścicielem aplikacji. Można to porównać z rejestrowaniem się na nowej stronie, podczas którego adres e-mail weryfikowany jest przez przesłanie e-maila aktywacyjnego. Gdyby to odwołanie nie zostało wykonane, mógłbyś podszywać się pod kogoś innego. UWAGA. Pamiętaj, że odwołanie musi być wykonane na publicznie dostępny adres, więc serwer lokalny (np. http://localhost/) nie zadziała.
74
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Rysunek 4.6. Przykładowy wynik działania skryptu z listingu 4.7
Rysunek 4.7. Zmiana typu aplikacji i ustawienie adresu odwołania zwrotnego Pierwsza część uwierzytelniania wykorzystuje skrypt twitter_registration.php z listingu 4.4. Zostaniemy przekierowani na stronę podobną do pokazanej na rysunku 4.8, na której będziemy musieli podać login i hasło. Po kliknięciu Sign In zostaniemy przekierowani z powrotem do naszej funkcji odwołania zwrotnego, którą zarejestrowaliśmy w ustawieniach aplikacji. Powinniśmy otrzymać następujący wynik: Witaj, bdanchilla! Zalogowałeś się przy wykorzystaniu Twittera.
Rysunek 4.8. Przekierowanie na stronę logowania //Przekazanie naszych tokenów żądania przechowywanych w $_SESSION. $twitterOAuth = new TwitterOAuth( CONSUMER_KEY, CONSUMER_SECRET, $_SESSION["request_token"], $_SESSION["request_token_secret"] ); $accessToken = $twitterOAuth->getAccessToken(); //Sprawdzenie, czy user_id jest liczbą. if ( isset($accessToken["user_id"]) && is_numeric($accessToken["user_id"]) ) { //Zapisanie tokenów dostępu w sesji. $_SESSION["access_token"] = $accessToken["oauth_token"]; $_SESSION["access_token_secret"] = $accessToken["oauth_token_secret"]; // Operacja zakończona powodzeniem! Przekierowanie na stronę z powitaniem. header( "location: welcome.php" ); } else { // Niepowodzenie. Wróć na stronę logowania. header( "location: login.php" ); } }else{ die( "błąd: brak dostępu" ); } ?>
Na stronie z powitaniem lub dowolnej innej w ramach sesji możemy teraz utworzyć obiekt TwitterOAuth i połączyć się z Twitterem (listing 4.9). Listing 4.9. Strona powitania — welcome.php
76
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
require_once("twitter_config.php"); session_start(); if( !empty( $_SESSION["access_token"] ) && !empty( $_SESSION["access_token_secret"] ) ) { $twitterOAuth = new TwitterOAuth( CONSUMER_KEY, CONSUMER_SECRET, $_SESSION["access_token"], $_SESSION["access_token_secret"] ); //Sprawdzenie, czy jesteśmy podłączeni. $user_info = $twitterOAuth->get( 'account/verify_credentials' ); if ( $user_info && !$user_info->error ) { echo "Witaj, " . $user_info->screen_name."!"; //Wykonaj inne //odwołania do API. } else { die( "Błąd: złe dane dostępowe." ); } } else { die( "Błąd: token dostępu nie został znaleziony w zmiennej \$_SESSION." ); } ?>
Wykorzystanie mechanizmu OAuth w celu powiązania strony z systemem logowania Podobnie jak w przypadku OpenID, możesz wykorzystać mechanizm OAuth Twittera do logowania użytkowników na Twojej stronie. Pewnie widziałeś na różnych stronach poniższy obrazek, dostępny pod adresem http://dev.twitter.com/pages/sign_in_with_twitter.
Aby dodać przycisk do poprzedniego przykładu, musimy tylko zmodyfikować nasz kod z listingu 4.4, aby zamiast automatycznego przekierowania wyświetlił się przycisk (listing 4.10). Listing 4.10. Rejestracja Twittera z przyciskiem do logowania — login.php getRequestToken(); //Zapisujemy tokeny w $_SESSION. $_SESSION['request_token'] = $requestTokens['oauth_token']; $_SESSION['request_token_secret'] = $requestTokens['oauth_token_secret'];
Wykorzystanie bazy danych do przechowywania danych o użytkownikach Rozszerzymy poprzedni przykład o przechowywanie danych dostępowych użytkowników w bazie danych (listing 4.11). Dla uproszczenia wykorzystamy SQLite. W celu poprawienia bezpieczeństwa powinieneś się upewnić, czy w środowisku produkcyjnym zapisany plik znajduje się poza głównym katalogiem aplikacji, lub wykorzystać bazę inną niż oparta na plikach płaskich. Więcej informacji o SQLite można znaleźć w rozdziale 7. Listing 4.11. Klasa połączenia z bazą danych — twitter_db_connect.php dbh = new PDO( 'sqlite:t_users' ); $this->dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); } catch ( PDOException $e ) { print "Błąd!: " . $e->getMessage() . "\n"; die (); } } public static function getInstance() { if ( !isset( Twitter_DBConnect::$db ) ) { Twitter_DBConnect::$db = new Twitter_DBConnect(); } return Twitter_DBConnect::$db->dbh; } } ?>
Ta klasa jest singletonem, aby mogła przechowywać tylko jedną instancję połączenia z bazą danych. Główną cechą singletonu jest prywatny konstruktor, a naszym zadaniem jest zapewnienie, że zwrócona zostanie jedna instancja połączenia. UWAGA. Wzorce projektowe nie są omawiane w niniejszej książce. Więcej informacji na temat singletonów można znaleźć pod adresem http://pl.wikipedia.org/wiki/Singleton_%28wzorzec_projektowy%29. Dobrym źródłem informacji o wzorcach projektowych jest książka PHP. Obiekty, wzorce, narzędzia. Wydanie III autorstwa Matta Zandstry (Helion 2011).
Listing 4.12 prezentuje klasę, w której zdefiniowane zostały akcje do obsługi bazy danych, natomiast listing 4.13 przedstawia zaktualizowany skrypt do obsługi odwołania zwrotnego, wykorzystujący bazę danych oraz klasę z listingu 4.12. Listing 4.12. Akcje do obsługi bazy danych (Select, Insert, Update) — twitter_db_actions.php
78
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
public function __construct() { $this->dbh = Twitter_DBConnect::getInstance(); $this->createTable(); } public function utworzTabele() { $query = "CREATE TABLE IF NOT EXISTS oauth_users( oauth_user_id INTEGER, oauth_screen_name TEXT, oauth_provider TEXT, oauth_token TEXT, oauth_token_secret TEXT )"; $this->dbh->exec( $query ); } public function zapiszUzytkownika( $accessToken ) { $users = $this->getTwitterUserByUID( intval($accessToken['user_id']) ); if ( count( $users ) ) { $this->updateUser( $accessToken, 'twitter' ); } else { $this->insertUser( $accessToken, 'twitter' ); } } public function pobierzUzytkownikowTwittera() { $query = "SELECT * from oauth_users WHERE oauth_provider = 'twitter'"; $stmt = $this->dbh->query( $query ); $rows = $stmt->fetchAll( PDO::FETCH_OBJ ); return $rows; } public function pobierzUzytkownikaPoUID( $uid ) { $query = "SELECT * from oauth_users WHERE oauth_provider= 'twitter' AND oauth_user_id = ?"; $stmt = $this->dbh->prepare( $query ); $stmt->execute( array( $uid ) ); $rows = $stmt->fetchAll( PDO::FETCH_OBJ ); return $rows; } public function wstawUzytkownika( $user_info, $provider = '' ) { $query = "INSERT INTO oauth_users (oauth_user_id, oauth_screen_name, oauth_provider, oauth_token, oauth_token_secret) VALUES (?, ?, ?, ?, ?)"; $values = array( $user_info['user_id'], $user_info['screen_name'], $provider, $user_info['oauth_token'], $user_info['oauth_token_secret'] ); $stmt = $this->dbh->prepare( $query ); $stmt->execute( $values ); echo "Wstawiony użytkownik: {$user_info['screen_name']}"; } public function aktualizujUzytkownika( $user_info, $provider = '' ) { $query = "UPDATE oauth_users SET oauth_token = ?, oauth_token_secret = ?, oauth_screen_name = ? WHERE oauth_provider = ? AND oauth_user_id = ?"; $values = array( $user_info['screen_name'], $user_info['oauth_token'], $user_info['oauth_token_secret'], $provider, $user_info['user_id'] ); $stmt = $this->dbh->prepare( $query ); $stmt->execute( $values ); echo "Zaktualizowany użytkownik: {$user_info['screen_name']}"; } } ?>
79
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Listing 4.13. Zaktualizowany skrypt odwołania zwrotnego — callback_with_db.php getAccessToken(); //Sprawdzenie, czy user_id jest liczbą. if ( isset( $accessToken["user_id"] ) && is_numeric( $accessToken["user_id"] ) ) { // Zapisanie tokenów dostępu do bazy danych. $twitter_bd_akcje = new Twitter_BD_Akcje(); $twitter_bd_akcje->zapiszUzytkownika($accessToken); //Operacja zakończona powodzeniem! Przekierowanie na stronę z powitaniem. //Strona powitanie.php także będzie musiała zostać zmodyfikowana, tak aby odczytywała tokeny z bazy, nie z sesji. header( "location: welcome.php" ); } else { // Niepowodzenie. Wróć na stronę logowania. header( "location: login.php" ); } } ?>
Zaletą integracji mechanizmów OAuth (lub OpenID) z Twoją stroną jest to, że internauci nie muszą wypełniać formularza rejestracyjnego i pamiętać kolejnego loginu i hasła. Dla większości znanych systemów zarządzania treścią (CMS), takich jak Wordpress czy Drupal, dostępne są gotowe wtyczki OAuth i OpenID.
Przechowywanie danych w pamięci aplikacji Aby wyeliminować wysyłanie zapytań do Twittera przy każdym odświeżeniu strony, pobrane dane są przeważnie przechowywane w aplikacji. REST API Twittera ogranicza liczbę dozwolonych zapytań użytkowników OAuth do 350 na godzinę; dla użytkowników anonimowych limit wynosi 150 zapytań. Na stronach z dużą liczbą użytkowników przechowywanie danych jest niezbędne. Nie opiszemy tu implementowania przechowywania danych, ale przedstawimy podstawową technikę. Dane są przechowywane po to, by mogły być wykorzystane w późniejszym czasie. Powinieneś co jakiś czas wysyłać zapytania do Twittera w celu aktualizacji przechowywanych danych. Czynność ta jest z reguły automatyzowana przez wykorzystanie zadań cron. Po każdym zapytaniu rezultaty wstawiane są do bazy danych, a niepotrzebne rekordy są z niej usuwane. Kiedy użytkownik odwiedza stronę, widzi wyniki z bazy danych, nie bezpośrednio z Twittera.
Dodatkowe metody API i przykłady jego wykorzystania API Twittera jest podzielone na następujące kategorie: Timeline (linia czasu), Status, User (użytkownik), List (lista), List Members (lista użytkowników), Direct Message (wiadomość bezpośrednia), Friendship (przyjaźń), Social Graph (graf społeczności), Account (konto), Favorite (ulubione), Notification (powiadomienia), Block (blokada), Spam Reporting (zgłaszanie spamu), Saved Searches (zapisane wyszukiwania), OAuth, Trends (trendy), Geo (geografia) i Help (pomoc).
80
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Nie możemy omówić w tej książce całego API, możemy jednak pokazać próbki wykorzystania niektórych metod. Dokładne opisy dostępne są w internecie. Na przykład dokumentacja metody friends_timeline znajduje się pod adresem https://dev.twitter.com/docs/api/1/get/statuses/friends_timeline. Możemy tam przeczytać, że metoda wywoływana jest za pomocą GET i może zwrócić wynik w formacie JSON, XML, RSS lub Atom, że wymaga uwierzytelniania i ma liczne parametry opcjonalne. Oczywiście nie musimy przechowywać tokenów dostępu w sesji — możemy je zapisywać w plikach. Ze względów bezpieczeństwa pliki powinny być umieszczone poza głównym katalogiem strony. Możemy zmodyfikować listing 4.5 tak, aby zapisywał tokeny na dysk (listing 4.14). Listing 4.14. Przechowywanie tokenów w pliku fizycznym //zapisanie tokenów do plików file_put_contents( "access_token", $accessOAuthTokens['oauth_token'] ); file_put_contents( "access_token_secret", $accessOAuthTokens['oauth_token_secret'] );
Po zmianie typu aplikacji na „client” uruchomienie skryptu twitter_registration.php wraz ze skryptem z listingu 4.14 spowoduje zapisanie tokenów dostępu na dysku. Teraz możemy przeprowadzić uwierzytelnianie, odczytując zapisane wcześniej tokeny przy wykorzystaniu funkcji file_get_contents (listing 4.15). Listing 4.15. Oddzielny plik wielokrotnego użytku służący do uwierzytelniania — twitter_oauth_signin.php
Powyższy plik możemy dołączać poprzez require do skryptów wykorzystujących API, aby je skrócić. Poniżej pobierzemy w sekwencji dane z Twittera dotyczące 20 aktualizacji wykonanych przez naszych przyjaciół i nas samych. Jest to analogiczne do widoku na naszej osobistej stronie Twittera (listing 4.16). Listing 4.16. Pobranie informacji z Twittera dotyczących aktualizacji wykonanych przez przyjaciół get( 'statuses/friends_timeline', array( 'count' => 20 ) ); var_dump( $przyjaciele_linia_czasu ); ?>
Listing 4.17 przedstawia nasze ostatnie posty wraz z numerami ID i statusami. Listing 4.17. Nasze ostatnie posty i ich numery ID get( 'statuses/user_timeline' ); foreach ( $posty as $t ) { echo $t->id_str . ": " . $t->text . " "; } ?>
81
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Przykładowy wynik działania skryptu z listingu 4.17 jest następujący: 68367749604319232: 68367690535940096: 47708149972602880: 43065614708899840: 39554975369658369: 39552206701072384:
850,000,000,000 centów za Skype Da da da da dat, da da da da, powiedział Jackie Wilson Czuję się świetnie we wtorkowy poranek Devendra Banhart - At the Hop http://bit.ly/sjMaa Nie mogę się doczekać nowej płyty Radiohead i Fleet Foxes: ) piszę rozdział... o Twitterze
Co zrobić, jeśli chcemy programistycznie usunąć post? Wystarczy wywołać metodę POST statuses/destroy, podając ID posta, który chcemy usunąć. Numer ID powinien zostać podany jako ciąg znaków, a nie liczba (listing 4.18). Listing 4.18. Usuwanie statusu post( 'statuses/destroy', array( 'id' => $tweet_id ) ); if ( $result ) { if ( $result->error ) { echo "Błąd (ID #" . $tweet_id . ") "; echo $result->error; } else { echo "Usuwanie posta: $tweet_id!"; } } ?>
Aby dodać użytkownika do naszej listy, najpierw musimy sprawdzić, czy nie jest już naszym przyjacielem (listing 4.19). Listing 4.19. Dodawanie i usuwanie przyjaciół get( "account/verify_credentials" ); //Sprawdzenie, czy użytkownik Snoopy jest naszym przyjacielem. Jeśli nie - dodajemy go do przyjaciół. if ( !$twitterOAuth->get( 'friendships/exists', array( 'user_a' => $user_info->screen_name, 'user_b' => 'peanutssnoopy' ) ) ) { echo 'NIE śledzisz użytkownika Snoopy. Dodawanie do przyjaciół!'; $twitterOAuth->post( 'friendships/create', array( 'screen_name' => 'Snoopy' ) ); } //Sprawdzanie, czy użytkownik Garfield jest naszym przyjacielem. Jeśli nie - dodajemy go do przyjaciół. if ( !$twitterOAuth->get( 'friendships/exists', array( 'user_a' => $user_info->screen_name, 'user_b' => 'garfield') ) ) { echo 'NIE śledzisz użytkownika Garfield. Dodawanie do przyjaciół!'; $twitterOAuth->post( 'friendships/create', array( 'screen_name' => 'Garfield' ) ); } //Sprawdzenie, czy użytkownik Garfield jest naszym przyjacielem. Jeżeli tak - usuwamy go z przyjaciół. if ( $twitterOAuth->get( 'friendships/exists', array( 'user_a' => $user_info->screen_name,
Na listingu 4.19 metoda exists jest metodą GET, natomiast metody destroy i create są typu POST.
Facebook Tworzenie aplikacji dla Facebook API jest podobne jak w przypadku Twitter API, ponieważ obydwa korzystają z uwierzytelniania OAuth. Najpierw wejdź na https://developers.facebook.com/apps i kliknij link Utwórz aplikację. Zostaniesz poproszony o zweryfikowanie konta za pośrednictwem karty kredytowej lub telefonu komórkowego — ekran weryfikacji tożsamości przedstawiono na rysunku 4.9. Twitter przeprowadza weryfikację za pośrednictwem poczty elektronicznej. UWAGA. Jeżeli zweryfikowałeś już swoje konto na Facebooku podczas korzystania z innych funkcji, nie zostaniesz poproszony o powtórną weryfikację.
Rysunek 4.9. Prośba o weryfikację konta na portalu Facebook Weryfikacja konta za pośrednictwem telefonu sprowadza się do odesłania kodu otrzymanego w wiadomości SMS i nie wymaga podawania danych karty kredytowej. W związku z tym Danchilla poleca właśnie tę metodę. Następnie wybierz nazwę aplikacji. Co ciekawe, bez pozwolenia portalu nie możesz w nazwie aplikacji wykorzystać słowa „face” (rysunek 4.10). Podobnie jak w przypadku Twittera, zostaną dla nas wygenerowane klucze klienckie OAuth, pokazane na rysunku 4.11. UWAGA. Ponieważ Facebook wykorzystuje OAuth, moglibyśmy wstawiać użytkowników do tabeli oauth_users utworzonej w sekcji dotyczącej Twittera. Jedyne, co musielibyśmy zmienić, to wartość parametru $provider na facebook.
W przeciwieństwie do Twittera Facebook posiada oficjalne SDK napisane w PHP. Dostępne jest ono pod adresem https://github.com/facebook/php-sdk/downloads i zawiera dwa pliki: base_facebook.php i facebook.php.
83
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Rysunek 4.10. Wybranie nazwy aplikacji oraz akceptacja regulaminu
Rysunek 4.11. Informacje o aplikacji i jej ustawienia Połączenie z portalem wymaga publicznie dostępnego miejsca odwołań zwrotnych, tak jak w drugim przykładzie dotyczącym Twittera. Musimy wpisać adres URL, pod którym będzie używana nasza aplikacja (rysunek 4.12). Domyślnym adresem odwołania jest adres aktualnie wykorzystywanego pliku skryptu. Mamy także możliwość utworzenia strony, która może zawierać naszą aplikację. Oficjalna dokumentacja, dostępna pod adresem http://developers.facebook.com/docs/guides/canvas/, opisuje taką stronę jako pustą stronę przeznaczoną do wyświetlenia aplikacji. Stronę zasilasz poprzez podanie adresu strony zawierającej HTML, JavaScript oraz CSS składające się na Twoją aplikację (rysunek 4.13). Strona, z której jest pobierana treść, może być dowolna, ale musi być związana z aplikacją Facebooka. Na rysunku 4.14 przedstawiono stronę wydawnictwa Helion jako stronę przypisaną do naszej aplikacji. Każda aplikacja ma skojarzoną stronę profilową, zawierającą linki, sugestie, ustawienia i opcje reklamy. Przykładowa strona profilowa została przedstawiona na rysunku 4.15. Dodatkowo mamy opcje umożliwiające uruchomienie aplikacji na iPhonie wyposażonym w system Android oraz pozwalające na pobieranie opłat za jej użytkowanie. Zobaczmy teraz pierwszy przykład wykorzystania API (listing 4.20).
84
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Rysunek 4.12. Ustawienia strony internetowej
Rysunek 4.13. Ustawienia integracji stron Listing 4.20. Skrypt logowania — login.php '103837646395925', 'secret' => 'cd964717db6fa6799e720eaf296dd7bc' ) ); //Strona logowania jest jednocześnie stroną odwołania, więc sprawdzamy, czy zostaliśmy uwierzytelnieni. $facebook_user = $facebook->getUser(); if ( !empty( $facebook_user ) ) { try { //Odwołanie do API w celu pobrania informacji o zalogowanym użytkowniku.
Rysunek 4.15. Strona profilowa aplikacji Na pewno zauważyłeś, że uwierzytelnianie za pośrednictwem Facebooka jest jeszcze prostsze niż w przypadku Twittera. Uruchomienie skryptu z listingu 4.20 spowoduje wyświetlenie pytania o uprawnienia, takiego jak na rysunku 4.16.
Rysunek 4.16. Pytanie o pozwolenie na połączenie Kiedy internauta kliknie przycisk Zaloguj się, aplikacja automatycznie pobierze dane o użytkowniku. Otrzymane informacje są zależne od globalnej polityki prywatności Facebooka, ustawień użytkownika oraz dodatkowych uprawnień, o które poprosimy. Po autoryzacji skrypt zwróci wynik jak poniżej: Witaj, Brian Danchilla! Płeć: male Miejscowość: Saskatoon, Saskatchewan
87
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Dodanie linku wylogowania z Facebooka Po wyświetleniu informacji przez funkcję wyswietlUserInfo z listingu dodamy link wylogowania, zgodnie z listingiem 4.21. Listing 4.21. Zmodyfikowany skrypt logowania login2.php, zawierający przycisk wylogowania '103837646395925', 'secret' => 'cd964717db6fa6799e720eaf296dd7bc' ) ); //Strona logowania jest jednocześnie stroną odwołania, więc sprawdzamy, czy zostaliśmy uwierzytelnieni. $facebook_user = $facebook->getUser(); if ( !empty( $facebook_user ) ) { try { //Odwołanie do API w celu pobrania informacji o zalogowanym użytkowniku. $user_info = $facebook->api( '/me' ); if ( !empty( $user_info ) ) { wyswietlUserInfo( $user_info ); //Zmodyfikowanie adresu, tak aby zgadzał się z ustawieniami aplikacji. $adres_wylogowania = (string) html_entity_decode( $facebook->getLogoutUrl(array( 'next' => 'http://www.foobar.com/logout.php' ) ) ); echo "Wyloguj"; } else { die( "Wystąpił błąd." ); } } catch ( Exception $e ) { print $e->getMessage(); } } else { //Próba utworzenia sesji poprzez przekierowanie do tej strony. $login_url = $facebook->getLoginUrl(); header( "Location: " . $login_url ); } function wyswietlUserInfo( $user_info ) { /* id, name, first_name, last_name, link, hometown, location, bio, quotes, gender, timezone, locale verified, updated_time */ echo "Witaj, " . $user_info['name'] . '! '; echo "Płeć: ".$user_info['gender']." "; echo "Miejscowość: ".$user_info['location']['name']." "; } ?>
Przekazaliśmy parametr next, oznaczający miejsce, do którego Facebook przekieruje użytkownika po wylogowaniu (listing 4.22). Listing 4.22. Plik wylogowania — logout.php
Żądanie dodatkowych uprawnień Jest wiele dodatkowych czynności, które możesz wykonać za pośrednictwem API, jednak niektóre wymagają dodatkowych uprawnień przyznanych przez użytkownika. Aby poprosić o dodatkowe uprawnienia, przekazujemy tablicę z kluczem scope do naszego adresu URL logowania. Wartością dla scope powinna być lista uprawnień oddzielonych przecinkami. Okno z prośbą o dodatkowe uprawnienia pokazano na rysunku 4.17. $login_url = $facebook->getLoginUrl( array( "scope" => "user_photos, user_relationships" ) );
Rysunek 4.17. Prośba o dodatkowe uprawnienia Możemy teraz dodać status matrymonialny do naszych informacji o użytkowniku, wstawiając na końcu funkcji displayUserInfo poniższy kod. echo $user_info['relationship_status'] . " (" .$user_info['significant_other'] ['name'].") "; Witaj, Brian Danchilla! Płeć: male Miejscowość: Saskatoon, Saskatchewan Engaged (Tressa Kirstein)
Funkcja getLoginUrl przyjmuje kilka opcjonalnych parametrów, które trzeba znać: next: URL, na który powinno nastąpić przekierowanie po zalogowaniu. cancel_url: URL, na który powinno nastąpić przekierowanie, jeżeli użytkownik nie wyrazi zgody. display: Może przyjmować wartość "page" (domyślnie, pełna strona) lub "popup"
Graph API Każdy obiekt na Facebooku jest dostępny poprzez jego Graph API. Dostępne obiekty to album, aplikacja, logowanie, komentarz, dokument, domena, zdarzenie, lista przyjaciół, grupa, spostrzeżenia, link, wiadomość, nota, strona, zdjęcie, recenzja, wiadomość statusu, subskrypcja, wątek, użytkownik i wideo. UWAGA. Więcej informacji na temat Graph API można uzyskać pod adresem http://developers.facebook.com/docs/reference/api/.
Jako przykład API można podać informacje w formacie JSON o użytkowniku Brian Danchilla, dostępne pod adresem http://graph.facebook.com/brian.danchilla:
Niektóre zabezpieczone obszary Graph API wymagają tokenu dostępu. Przykładowy URL wymagający takiego tokenu to http://graph.facebook.com/me, który w przypadku braku tokenu wyświetli informację: { "error": { "message": "An active access token must be used to query information about the current user.", "type": "OAuthException" } }
Wyszukiwanie albumów i fotografii Ostatni przykład w tym rozdziale będzie dotyczył wyświetlania zdjęć z albumów z Facebooka wraz ze zdjęciami okładek albumów, nazwami albumów i liczbą zdjęć. W skrypcie na listingu 4.21 zamień linię wyswietlUserInfo( $user_info ); na wyswietlAlbumy();. Zdefiniujemy teraz funkcję wyswietlAlbumy(); zgodnie z listingiem 4.23. Listing 4.23. Sposób wyświetlania albumów z Facebooka function wyswietlAlbumy( Facebook $facebook ) { $albums = $facebook->api( '/me/albums?access_token=' . $facebook_session['access_token'] ); $i = 0; print "
"; foreach ( $albums["data"] as $a ) { if ( $i == 0 ) { print "
W skrypcie z listingu 4.23 przekazujemy do funkcji wyswietlAlbumy() obiekt $facebook. Podajemy nasz token dostępu w celu uzyskania dostępu do wszystkich naszych albumów. Później wykonujemy pętlę, wyświetlając albumy w pięciu kolumnach tabeli. Wartość id dla cover_photo, którą otrzymaliśmy, wstawiliśmy do kolejnego odwołania API, aby pobrać szczegóły. Wynik działania skryptu przedstawia rysunek 4.18.
90
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
Rysunek 4.18. Przykładowe albumy wyświetlone przez skrypt z listingu 4.23
Podsumowanie Czytając ten rozdział, poznałeś uwierzytelnianie OAuth, z którego skorzystałeś przy użyciu API Twittera i Facebooka. Najlepszym sposobem przyswojenia sobie nowego API często jest po prostu jego praktyczne stosowanie. Jako deweloper z reguły nie musisz znać wszystkich niuansów danego API — naucz się rzeczy potrzebnych do realizacji bieżących zadań i rozszerzaj swoją wiedzę w miarę potrzeby. W środowisku testowym nie musisz bać się błędów i skryptów, które nie zadziałają przy pierwszym uruchomieniu. Tworzenie aplikacji przeznaczonych do współpracy z portalami społecznościowymi jest modne i mniej wymagające niż niektóre inne obszary programowania. Nie oznacza to jednak, że jest to proste zadanie. Tu też trzeba się uczyć nowych technik, bibliotek i zmieniających się API. Portale społecznościowe są bardzo wciągające — pomagają tworzyć i podtrzymywać więzi międzyludzkie. Dzięki temu powstają nowe możliwości dla twórców oprogramowania.
91
ROZDZIAŁ 4. MEDIA SPOŁECZNOŚCIOWE
92
ROZDZIAŁ 5
Nowości technologiczne
W tym rozdziale zaprezentujemy nowe możliwości PHP 5.3. Obejmują one przestrzenie nazw, funkcje anonimowe, nowy format tekstu — nowdoc — oraz instrukcję goto. Ostatnia nowość to coś w rodzaju „powrotu do przeszłości”; wciąż jest niedoceniana. Tak jak w przypadku pierwszych w pełni proceduralnych języków, takich jak Pascal, zaczęła jednak zdobywać popularność wśród programistów. Regularne wykorzystywanie goto nadal nie jest mile widziane; dla niektórych jest to jeden z grzechów głównych. Kontrowersje zostały zapoczątkowane przez artykuł Edsgera Dijkstra „Instrukcja goto uważana za szkodliwą” (Go To Statement Considered Harmful) z 1968 r. Od tamtej pory instrukcja ta ma niechlubną sławę. Warto mieć jednak własną opinię. Programowanie to nie religia; jego cele to prostota, przejrzystość i wydajność. Jeżeli instrukcja goto pomaga osiągnąć te cele, to jej wykorzystanie jest uzasadnione. Instrukcja goto bez względu na otaczające ją kontrowersje nie jest najważniejszą nowością w PHP 5.3. Najistotniejsze są bez wątpienia przestrzenie nazw. Ważne są także funkcje anonimowe, zwane funkcjami closure lub lambda, które otwierają nowe możliwości programistyczne bez wprowadzania chaosu do globalnej przestrzeni nazw. Wprowadzony został nowy format dokumentu zwany nowdoc, podobny do heredoc, jednak w pewnych zastosowaniach wszechstronniejszy. PHP 5.3 jest ponadto pierwszą wersją zawierającą SPL (Standard PHP Library) jako integralną część języka. We wcześniejszych wersjach SPL było rozszerzeniem. Istnieją także archiwa PHP, zwane phar, które pozwalają na tworzenie plików podobnych do archiwów JAR w Javie, zawierających całą aplikację.
Przestrzenie nazw Przestrzenie nazw są standardem w wielu językach programowania. Problem, który jest przez nie rozwiązywany, jest następujący: jedna z często wykorzystywanych metod komunikacji pomiędzy różnymi podprogramami używa zmiennych globalnych. Wiele bibliotek programistycznych posiada zmienne globalne wykorzystywane przez inne moduły. Kiedy język się rozwija i liczba bibliotek się zwiększa, prawdopodobieństwo powtórzenia nazwy zmiennej globalnej rośnie wykładniczo. Przestrzenie nazw pozwalają na podzielenie globalnej przestrzeni nazw i zapobiegają konfliktom spowodowanym przez powielenie nazw zmiennych, które mogłyby prowadzić do nieprzewidywalnego zachowania programu. Do wersji 3.5 PHP nie wspierało przestrzeni nazw. Konieczność ich wprowadzenia została wywołana rozwojem samego języka. Przestrzenie nazw są obiektami mogącymi przechowywać klasy, funkcje i stałe. Zorganizowane są hierarchicznie i mogą zawierać podrzędne przestrzenie nazw. Składnia przestrzeni nazw jest bardzo łatwa. Listing 5.1 zawiera trzy pliki demonstrujące sposób definiowania i wykorzystania przestrzeni nazw.
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
Listing 5.1. Wykorzystanie przestrzeni nazw domestic.php: type='pies'; } function get_type() { return($this->type); } } ?>
wild.php: type='tygrys'; } function get_type() { return($this->type); } } ?>
listing5_1.php: #!/usr/bin/env php get_type()); $b=new dzikie\zwierze(); printf("%s\n",$b->get_type()); use dzikie\zwierze as bestia; $c=new bestia(); printf("%s\n",$c->get_type()); ?>
Zgodnie z oczekiwaniami wywołanie skryptu spowoduje wyświetlenie wyniku: ./listing5_1.php pies tygrys tygrys
Przestrzeń dzikie jest zdefiniowana w pliku wild.php. Bez wykorzystania przestrzeni nazw nasza klasa zwracałaby zupełnie inne wyniki. Kiedy przestrzeń nazw zostanie zdefiniowana, do klasy można odwołać się wyłącznie za pośrednictwem konwencji przestrzeń nazw/nazwa klasy. Możliwy jest także import przestrzeni nazw do aktualnej przestrzeni i przekształcenie jej na alias o czytelniejszej nazwie. Przestrzenie nazw są definiowane przez bloki instrukcji. Jeżeli w pliku znajduje się wiele przestrzeni nazw — ich zawartość musi zostać otoczona nawiasami klamrowymi, tak jak na listingu 5.2. Listing 5.2. Plik z wieloma przestrzeniami nazw animals.php:
94
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
class zwierze { static function gdziejestem() { print __NAMESPACE__."\n"; } function __construct() { $this->type='tygrys'; } function get_typ() { return($this->type); } } } namespace zwierze\domowe { class zwierze { function __construct() { $this->type='pies'; } function get_typ() { return($this->type); } } } ?>
Powyżej możemy też zobaczyć podrzędną przestrzeń nazw, oddzieloną znakiem \ (lewym ukośnikiem). Użyta została także stała __NAMESPACE__, która zawiera nazwę aktualnej przestrzeni nazw. Jest ona bardzo podobna do innych specjalnych stałych w PHP, takich jak __FILE__ czy __CLASS__. Listing 5.3 pokazuje, jak korzystać z podrzędnych przestrzeni nazw. Listing 5.3. Wykorzystanie podrzędnych przestrzeni nazw get_typ()); bestia::gdziejestem(); ?>
Metoda gdziejestem została zadeklarowana jako statyczna, tak aby mogła być wykonana wyłącznie w kontekście klasy, a nie w kontekście obiektu. Składnia wywołania metody w kontekście klasy jest następująca: klasa::funkcja($arg). Takie wywołania nie są powiązane z żadną instancją obiektu i są nazywane wywołaniami w kontekście klasy. Dla klasy zwierze/dzikie/zwierze utworzono alias bestia, a jej nazwy zostały zaimportowane do lokalnej przestrzeni nazw. Operacje takie jak wywoływanie metod klas są także dozwolone dla zaimportowanych przestrzeni nazw. Jest też predefiniowana globalna przestrzeń nazw. Wszystkie normalne funkcje są częścią globalnej przestrzeni nazw. Wywołanie funkcji \phpversion() jest całkowicie równoznaczne z wywołaniem phpversion(), bez prefiksu \ — zgodnie z poniższym: php -r 'print \phpversion()."\n";' 5.3.3
Pomimo że tworzenie lokalnych wersji funkcji wbudowanych nie jest dobrym pomysłem, poprzedzenie wywołania funkcji lewym ukośnikiem da nam pewność, że wywoływana funkcja pochodzi z globalnej przestrzeni nazw i nie jest jej lokalną wariacją.
95
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
Przestrzenie nazw i autoładowanie Z poprzednich rozdziałów znasz funkcję __autoload, wykorzystywaną do ładowania klas do programu. Pomaga ona w automatyzacji dyrektywy require_once z listingu 5.3. Szkielet funkcji __autoload wygląda tak: function __autoload($klasa) { require_once("$klasa.php"); }
Kiedy wywoływana klasa zawiera przestrzenie nazw, do funkcji przekazywana jest pełna nazwa. Zmodyfikujmy listing 5.3 następująco: get_typ()); beast::gdziejestem(); ?>
Wykonany skrypt zwróci pełną ścieżkę: zwierze\dzikie\zwierze
W celu wykorzystania przestrzeni nazw z autoładowaniem powinniśmy utworzyć hierarchię katalogów, zamienić znaki \ na znaki / i dołączyć plik. Zmiana znaków może być wykonana za pomocą funkcji str_replace lub preg_replace. W prostych zastosowaniach, takich jak to, funkcja str_replace jest szybsza.
Przestrzenie nazw — podsumowanie Przestrzenie nazw są abstrakcyjnymi kontenerami przeznaczonymi do logicznego grupowania obiektów. Są dobrze znanym mechanizmem, powszechnie występującym w innych językach programowania, czasami nazywanym pakietami lub modułami. Skrypty stają się większe i bardziej skomplikowane każdego dnia, co powoduje, że tworzenie nowych identyfikatorów jest coraz trudniejsze. W tym rozdziale przedstawiliśmy dwie różne klasy, które miały tę samą nazwę. Przy takiej konwencji nazewniczej prawie pewne jest, że powstaną konflikty powodujące błędy. Dzięki przestrzeniom nazw mogliśmy zawsze odwoływać się do właściwej klasy, a także utworzyć dla klas aliasy o przyjaznych nazwach. Przestrzenie nazw są nowością, przynajmniej w PHP. Wykorzystuje je niewiele programów (dodatkowe biblioteki PHP), ale są one podstawową i mile widzianą częścią języka. Bez wątpienia uznasz tę nową funkcjonalność za bardzo przydatną.
Funkcje anonimowe Właściwie nie jest to nowa funkcjonalność — jest dostępna od wersji 4.0.1 — jednak jej składnia stała się elegantsza. We wcześniejszych wersjach PHP było możliwe utworzenie funkcji anonimowej poprzez zastosowanie funkcji wbudowanej create_function. Takie funkcje są z reguły bardzo krótkie i wykorzystuje się je jako odwołanie zwrotne w innych funkcjach. Poniżej znajduje się przykładowy skrypt sumujący wartości w tablicy przy użyciu funkcji wbudowanej array_reduce(). Funkcje array_map i array_reduce są implementacją algorytmu Google map-reduce algorithm. Funkcja array_reduce() wywoływana jest rekursywnie na tablicy aż do uzyskania pojedynczej wartości.
96
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
Składnia funkcji array_reduce jest bardzo prosta: array_reduce($tablica,funkcja_odwolania). Funkcja odwołania ma dwa argumenty — pierwszy zwracany jest przez poprzednią iterację, drugi jest aktualnym elementem tablicy. Listing 5.4 zawiera gotowy skrypt. Listing 5.4. Funkcja array_reduce
Funkcja anonimowa jest utworzona i przechowywana w zmiennej $sum. Oznaczona jako komentarz linia przedstawia stary sposób definiowana funkcji anonimowych. Nowy sposób jest o wiele elegantszy. Obie metody działają identycznie i zwrócą ten sam wynik. Funkcje anonimowe mogą także być zwrócone z innej funkcji jako jej wynik. Zasady widoczności w PHP powodują, że funkcja nie rozpoznaje zmiennych zewnętrznych, co oznacza, że zmienne z zewnętrznej funkcji nie będą mogły zostać zastosowane w funkcji wewnętrznej. Aby uzyskać dostęp do argumentu funkcji zawierającej wewnątrz funkcji zwracanej, musimy wykorzystać zmienną globalną. Całość wyglądałaby następująco: function func($a) { global $y; $y = $a; return function ($x) { global $y; return $y + $x; }; }
Zwrócona zostanie funkcja anonimowa zależna od zmiennej globalnej. Termin „closure” pochodzi z Perla, w którym obowiązują inne zasady widoczności, pozwalające na nieco inne wykorzystanie funkcji anonimowych. W PHP funkcje anonimowe są stosowane głównie do tworzenia krótkich funkcji odwołania zwrotnego oraz pozwalają nie marnować nazwy globalnej. Ten sposób tworzenia funkcji anonimowych jest elegantszy składniowo, ale nie stanowi istotnej innowacji.
Nowdoc Nowdoc pozwala na wstawianie tekstu do skryptu w nowy sposób. Wygląda to prawie identycznie jak w przypadku heredoc, z jedną bardzo ważną różnicą: nowdoc nie jest parsowany, co czyni go idealnym narzędziem do wstawiania kodu PHP lub nawet poleceń SQL. Baza danych Oracle posiada wewnętrzne tabele, których nazwy zaczynają się od V$. W wersjach PHP starszych niż 5.3 każdy znak $ w zapytaniach musiał być poprzedzony znakiem ucieczki \, zgodnie z poniższym: $FILE="select lower(db_name.value) || '_ora_' || v\$process.spid || nvl2(v\$process.traceid, '_' || v\$process.traceid, null ) || '.trc' from v\$parameter db_name cross join v\$process join v\$session
97
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
on v\$process.addr = v\$session.paddr where db_name.name = 'instance_name' and v\$session.sid=:SID and v\$session.serial#=:SERIAL";
Bez składni nowdoc zapytanie to musiało zostać zapisane dokładnie w ten sposób. Dzięki nowdoc możemy wprowadzać takie zapytania bez konieczności wykorzystania znaku \. Składnia heredoc wygląda następująco: $FILE= = <<
Nowa składnia nowdoc wygląda tak: $FILE = <<<'EOT' select lower(db_name.value) || '_ora_' || v$process.spid || nvl2(v$process.traceid, '_' || v$process.traceid, null ) || '.trc' from v$parameter db_name cross join v$process join v$session on v$process.addr = v$session.paddr where db_name.name = 'instance_name' and v$session.sid=:SID and v$session.serial#=:SERIAL; EOT;
Jedynymi różnicami są pojedyncze apostrofy obejmujące identyfikatory końca oraz brak znaku \. Cała reszta jest dokładnie taka sama. UWAGA. Zasady dotyczące znacznika „koniec tekstu” są takie same jak w przypadku heredoc; nie może być żadnych spacji przed średnikiem i po nim.
Aby zaobserwować różnice, przyjrzyjmy się fragmentowi kodu z listingu 5.5. Listing 5.5. Różnice pomiędzy heredoc i nowdoc
98
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
public $nazwa; function __construct($rodzaj,$nazwa) { $this->gatunek=$rondzaj; $this->nazwa=$nazwa; } function __toString() { return($this->gatunek.'.'.$this->nazwa); } } $zwierzak = new zwierze("pies","Fido"); $tekst = <<<'EOT' Moim ulubionym zwierzęciem jest {$zwierzak->gatunek}. Ma na imię {$zwierzak->nazwa}.\n To krótka nazwa: $zwierzak\n EOT; print "NOWDOC:\n$tekst\n"; $tekst = <<gatunek}. Ma na imię {$zwierzak->nazwa}.\n To krótka nazwa: $zwierzak\n EOT; print "HEREDOC:\n$tekst";
Ten sam tekst został najpierw zdefiniowany jako nowdoc, a następnie jako dobrze znany heredoc. Wynik działania skryptu rozjaśni wszystkie różnice pomiędzy działaniem obu sposobów definiowania tekstów: NOWDOC: Moim ulubionym zwierzęciem jest {$zwierzak->gatunek}. Ma na imię {$zwierzak->nazwa}.\n To krótka nazwa: $zwierzak\n HEREDOC: Moim ulubionym zwierzęciem jest pies. Ma na imię Fido. To krótka nazwa: pies.Fido
Pierwsza wersja nie interpretuje tekstu. Osadzone referencje do zmiennych, same zmienne, a nawet znaki specjalne wyświetlane są tak, jak zostały wpisane. Stara wersja interpretuje wszystko: referencje do zmiennych, zmienne i znaki specjalne. Nowdoc przeznaczony jest przede wszystkim do wstawiania kodu PHP (lub innego) do tekstu. Następujący skrypt byłby dużo trudniejszy do napisania przy wykorzystaniu heredoc:
Z nową składnią nowdoc wstawienie kodu SQL-a czy PHP stało się łatwiejsze. Nowdoc nie zastępuje składni heredoc, która nadal jest bardzo użyteczna w aplikacjach potrzebujących prostych szablonów i niewymagających pełnej funkcjonalności silnika Smarty. Smarty jest najpopularniejszym silnikiem szablonów dla PHP. Oferuje wiele możliwości, lecz jest bardziej skomplikowany niż format nowdoc. Nowdoc jest tylko pomocą, kiedy zachodzi konieczność wstawienia kodu PHP do tekstu.
99
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
Lokalne instrukcje goto PHP 5.3 wprowadza bardzo kontrowersyjną lokalną instrukcję goto (przymiotnik „lokalna” oznacza tutaj, że niemożliwe jest przeskoczenie poza aktualnie wykonywany zestaw poleceń lub do wewnątrz pętli). W niektórych językach, przede wszystkim C, możliwe jest wykonanie „nielokalnego goto” lub „długiego skoku”, jednak nie jest to możliwe w PHP. Ograniczenia lokalnej instrukcji goto są takie same jak w innych językach — nie wolno wskoczyć do pętli ani wyskoczyć poza aktualnie wykonywany podprogram. Instrukcja goto udostępniana jest jako ostateczność, nie powinna być stosowana regularnie. Jej składnia jest bardzo prosta. Listing 5.6 prezentuje pętlę while przepisaną tak, aby wykorzystać instrukcję goto. Listing 5.6. Przykład instrukcji goto "; if ($i>0) goto LAB; echo "Koniec pętli\n"; ?>
Po etykiecie następuje dwukropek. Ten mały skrypt wyświetli poniższy wynik: i=10 i=9 i=8 i=7 i=6 i=5 i=4 i=3 i=2 i=1 Koniec pętli
Nie podajemy przykładu rzeczywistego zastosowania instrukcji goto, ponieważ nigdy nie była nam potrzebna. Wydaje się, że będzie to najrzadziej wykorzystywana funkcja nowego PHP 5.3. Warto jednak wiedzieć o jej istnieniu, gdyby kiedyś zaszła potrzeba jej użycia. Powtórzmy: instrukcja goto nie jest mile widziana wśród programistów. Programowanie jednak, jak już wspomniano, nie jest religią i nie ma w nim kar za grzechy przeciwko stylowi programowania. Główne cele to przejrzystość i wydajność. Jeżeli goto pomaga w ich osiągnięciu, to jest do zaakceptowania.
Standardowa biblioteka PHP — SPL Standardowa biblioteka PHP (SPL) jest zbiorem klas podobnym do standardowej biblioteki szablonów (Standard Template Library — STL) w C++. Zawiera ona przydatne klasy dla standardowych struktur programistycznych, takich jak stos, sterta, listy podwójnie linkowane i kolejkowanie. Klasa SplFileObject została wspomniana wcześniej, w rozdziale 1. Dokumentacja SPL jest dostępna pod adresem http://us3.php.net/manual/en/book.spl.php. Pierwszą klasą, którą omówimy, będzie SplMaxHeap. Liczby wstawiane są do obiektu klasy SplMaxHeap w losowej kolejności. Kiedy są zwracane, ułożone są w porządku malejącym. Istnieje identyczna klasa, SplMinHeap, która sortuje liczby w porządku rosnącym. Listing 5.7 przedstawia skrypt. Listing 5.7. Skrypt SplMaxHeap
100
Klasy te mogą być dziedziczone i dzięki temu można zaimplementować je dla dat lub innych typów. Kiedy skrypt zostanie wykonany, wyświetli następujący wynik: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: wstawiam: Pobieram: 1 :970 2 :948 3 :905 4 :831 5 :635 6 :433 7 :399 8 :362 9 :293 10 :52 11 :16
948 16 433 293 831 399 905 362 635 52 970
Liczby wygenerowane przez funkcję rand zostały wstawione do zmiennej $hp w porządku losowym. Po pobraniu są posortowane w porządku malejącym. Klasę można wykorzystać w aktualnej formie, tak jak w poprzednim przykładzie, lub można ją rozszerzyć. W przypadku rozszerzenia klasy potomna klasa musi implementować metodę compare. Listing 5.8 przedstawia przykład. Listing 5.8. Rozszerzenie klasy 'Nowak','datazatrudnienia'=>'2009-04-18','placa'=>1000); $var2=array('nazwisko'=>'Kowalski','datazatrudnienia'=>'2008-09-20','placa'=>2000); $var3=array('nazwisko'=>'Wiśniewski','datazatrudnienia'=>'2010-01-10','placa'=>2000); $var4=array('nazwisko'=>'Wiśniewski','datazatrudnienia'=>'2007-12-15','placa'=>3000); $hp=new ExtHeap(); $hp->insert($var1);
101
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
$hp->insert($var2); $hp->insert($var3); $hp->insert($var4); foreach($hp as $emp) { printf("Nazwisko:%s Data zatrudnienia:%s\n",$emp['nazwisko'],$emp['datazatrudnienia']); } ?>
Przykład nie jest tak trywialny, jak mogłoby się wydawać na pierwszy rzut oka. Skrypt sortuje tablice według wartości daty. Nowa funkcja compare nie przyjmie jako argumentu niczego, co nie jest tablicą. Daty porównywane są po przekonwertowaniu ich do formatu Epoch. Nasze rozszerzenie klasy SplMaxHeap posortuje wpisy od najnowszego do najstarszego: Nazwisko:Wiśniewski Data zatrudnienia:2010-01-10 Nazwisko:Nowak Data zatrudnienia:2009-04-18 Nazwisko:Kowalski Data zatrudnienia:2008-09-20 Nazwisko:Wiśniewski Data zatrudnienia:2007-12-15
Poza klasami stosu, sterty i kolejki są także bardzo interesujące klasy do wykonywania operacji na plikach. Klasa SplFileObject została pokazana w rozdziale 1. i będzie powtórnie wykorzystana w rozdziałach dotyczących integracji z bazą danych. Są jednak bardziej interesujące klasy dotyczące plików, na przykład klasa SplFileInfo. Zwraca ona informacje o podanym pliku: getBasename()."\n"; print "Ostatnia zmiana:".strftime("%m/%d/%Y %T",$finfo->getCTime())."\n"; print "Właściciel:".$finfo->getOwner()."\n"; print "Rozmiar:".$finfo->getSize()."\n"; print "Katalog:".$finfo->isDir()? "No":"Yes"; print "\n"; ?>
Może uzyskać dane dotyczące daty utworzenia pliku, daty jego ostatniego użycia, jego nazwy, właściciela oraz wszystkie informacje dostarczane przez funkcję fstat w standardowej bibliotece języka C. Oto wynik działania skryptu: Nazwa:.bashrc Ostatnia zmiana:02/18/2011 09:17:24 Właściciel:500 Rozmiar:631 No
Klasa ta jest interesująca sama w sobie, jest jednak tutaj niezbędna, abyśmy mogli wyjaśnić sposób działania klasy FileSystemIterator, która także jest częścią SPL i działa podobnie jak polecenie find w systemie Unix — przesuwa się po drzewie katalogów i zwraca iterator według wyników. Listing 5.9 zawiera skrypt, który wyświetli nazwy wszystkich katalogów zawartych w katalogu /usr/local. Listing 5.9. Wyświetlenie nazw podkatalogów katalogu /usr/local isDir()) { print $plik->getFilename() . "\n"; } } ?>
102
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
Flagi określają, co zostanie zwrócone. Ustawienie flagi CURRENT_AS_FILEINFO oznacza, że chcemy, aby każdy obiekt iteratora był obiektem klasy SplFileInfo zawierającym informacje o pliku. Dostępna jest także flaga CURRENT_AS_PATHNAME, po której ustawieniu klasa FileSystemiterator zwróci ścieżki zamiast informacje o plikach. Oczywiście informacje o pliku zawierają ścieżki oraz inne dane, więc odwołanie do nich jest naturalne, jeżeli szukamy katalogów. Flaga SKIP_DOTS oznacza, że iterator ma ominąć katalogi . oraz ... W naszym systemie wynik działania skryptu jest następujący: var libexec sbin src tora bin include skype_static-2.1.0.81 man lib share etc
W innym systemie zwrócony wynik może wyglądać inaczej. Na przykład systemy Windows z reguły w ogóle nie mają katalogu /usr/local. Kolejnym użytecznym iteratorem jest Globiterator, który różni się nieco od klasy FileSystemiterator zaprezentowanej powyżej. Listing 5.10. Wykorzystanie klasy Globiterator
W tym przypadku interesują nas wyłącznie nazwy plików, do tego celu została wykorzystana flaga CURRENT_AS_PATHNAME. Oczywiście, flaga CURRENT_AS_FILEINFO także byłaby poprawna, natomiast flaga SKIP_DOTS
nie miałaby sensu.
SPL — podsumowanie Technicznie rzecz biorąc, SPL nie jest nowością w PHP 5.3; istniało także dla wersji 5.2, ale było odrębnym rozszerzeniem. Od wersji PHP 5.3 SPL jest integralną częścią języka i nie można jej wyłączyć lub usunąć. Jest dużym rozszerzeniem, które ciągle się rozwija — w jego skład wchodzi wiele użytecznych elementów. W tym rozdziale pokazaliśmy najużyteczniejsze z nich, jednak SPL oferuje o wiele więcej, niż możemy tu przedstawić. Poznanie SPL może zaoszczędzić wiele kłopotów i pracy podczas pisania skryptów PHP. Rozszerzenie to jest wyjątkowo praktyczne ze względu na wbudowaną kontrolę błędów oraz wyjątki.
Rozszerzenie phar W internecie bardzo popularnym językiem jest Java. Posiada archiwa *.jar pozwalające na spakowanie kilku plików w jednym archiwum i wykonywanie ich jak aplikacji. W wersji 5.3 PHP udostępniono rozszerzenie phar służące do tych samych celów. Daje ono możliwość tworzenia i modyfikacji archiwów zawierających skrypty PHP. Nazwa wywodzi się od angielskiej nazwy Php Archive. Na listingu 5.1 pokazaliśmy skrypt zawierający dwa dodatkowe pliki klas domestic.php oraz wild.php. Aby dystrybuować aplikację, musielibyśmy przesyłać trzy pliki. Gdyby było więcej klas, liczba plików byłaby 103
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
jeszcze większa. Celem jest dystrybuowanie tylko dwóch plików: skryptu wykonywalnego oraz archiwum phar zawierającego wszystkie niezbędne klasy. Innymi słowy, nowa wersja skryptu z listingu 5.1 wyglądałaby jak na listingu 5.11. Listing 5.11. Zmieniony listing 5.1 get_type()); $b=new \dzikie\zwierze(); printf("%s\n",$b->get_type()); ?>
Cała magia zawarta jest w dyrektywach include, które dołączają do skryptu archiwum i dodają referencje do plików. Jak utworzyć taki plik? Tak jak Java ma program o nazwie jar, tak w skład PHP 5.3 wchodzi program phar. Aby wyświetlić pomoc programu, należy wykonać polecenie phar help; dostępnych jest wiele opcji — wszystkie są dobrze udokumentowane. Archiwizator phar jest skryptem PHP wykorzystującym rozszerzenie .phar. W większości dystrybucji archiwizator ten nie posiada strony z podręcznikiem, więc phar help to najlepsze, co możemy uzyskać. Utwórzmy nasze pierwsze archiwum phar. Składnia jest bardzo prosta: phar pack -f animals.phar -c gz wild.php domestic.php
Argument pack instruuje program, aby utworzył nowe archiwum z nazwą podaną w parametrze -f oraz aby spakował do niego pliki wild.php i domestic.php. Aby można było wykonać tę operację w php.ini, parametr phar.readonly musi być ustawiony na wartość Off. Domyślnie parametr ustawiony jest na On, przez co archiwum nie będzie mogło być utworzone. Mechanizmem kompresji będzie gz. Dostępne metody to zip, gz (gzip) oraz bz2 (bzip2). Domyślnie kompresja nie jest wykorzystywana. Po przeprowadzeniu tej operacji możemy bez problemu wykonać program z listingu 5.2. Wynik jest następujący: pies tygrys
To jeszcze nie wszystko. Archiwizator PHP ma znacznie więcej możliwości. Jeżeli chcemy mieć pewność, że archiwum nie zostało zmodyfikowane, możemy je podpisać: phar sign -f animals.phar -h sha1
Powyższe polecenie podpisze plik animals.phar za pomocą popularnego algorytmu SHA1 i zapobiegnie jego modyfikacjom. Spróbujmy dodać pustą linię do pliku i ponownie uruchomić skrypt z listingu 5.11. Oto rezultat: PHP Warning: include(phar://animals.phar/wild.php): failed to open stream: phar ´"/tmp/animals.phar" has a broken signature in /tmp/listing5_11.php on line 3 PHP Warning: include(): Failed opening 'phar://animals.phar/wild.php' for inclusion ´(include_path='.:/usr/local/PEAR') in /tmp/listing5_11.php on line 3 PHP Warning: include(phar://animals.phar/domestic.php): failed to open stream: phar ´"/tmp/animals.phar" has a broken signature in /tmp/listing5_11.php on line 4 PHP Warning: include(): Failed opening 'phar://animals.phar/domestic.php' for inclusion ´(include_path='.:/usr/local/PEAR') in /tmp/listing5_11.php on line 4 PHP Fatal error: Class 'zwierze' not found in /tmp/listing5_11.php on line 5
Polecenia include nie zadziałały, cały skrypt nie został wykonany. To oznacza, że nasz skrypt jest prawidłowo zabezpieczony przed modyfikacjami; zmodyfikowane skrypty nie przejdą walidacji podpisu. Phar może także wypakowywać pliki z archiwum oraz dodawać i usuwać pliki. Wszystkie polecenia są bardzo proste. Oto przykład polecenia tworzącego listę zawartości archiwum:
104
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
phar list -f animals.phar |-phar:///home/mgogala/work/book/Rozdzial05/animals.phar/domestic.php \-phar:///home/mgogala/work/book/Rozdzial05/animals.phar/wild.php
Phar może nie tylko utworzyć listę plików wewnątrz archiwum, ale też je rozpakować lub usunąć, zupełnie jak analogiczne jar, ar oraz tar. Składnia jest bardzo podobna do tej umożliwiającej tworzenie list i wstawianie plików do archiwum. Polecenie phar delete -f animals.phar -e wild.php usunęłoby skrypt wild.php z archiwum, natomiast polecenie phar extract -f animals.phar -i wild.php wypakowałoby podany plik. Phar współpracuje też z wyrażeniami regularnymi. To polecenie spakowałoby całą zawartość katalogu do pliku o nazwie test.phar: phar pack -f test.phar -c zip *.php. Jako wynik wyświetli się lista nazw plików. Przy użyciu phar można także utworzyć plik wykonywalny. Aby to zrobić, należy utworzyć tak zwany „stub”. Stub jest skryptem, do którego przekazywana jest kontrola w momencie uruchomienia archiwum wykonywalnego. Format stuba jest specyficzny. Oto skrypt z listingu 5.1 zmodyfikowany tak, aby mógł zostać wykorzystany jako stub dla naszego archiwum: get_type()); $b=new \dzikie\zwierze(); printf("%s\n",$b->get_type()); __HALT_COMPILER(); ?>
Plik zostanie nazwany stub.php i będzie dodany do archiwum jako stub. Zwróć uwagę na funkcję __HALT_COMPILER() umieszczoną na końcu. Funkcja ta musiała być dodana, aby zakończyć działanie skryptu. Ponadto pomiędzy tą funkcją a znacznikiem zakończenia skryptu (?>) może być wstawiony maksymalnie jeden
znak spacji. Dodanie stuba jest proste. phar stub-set -f animals.phar -s stub.php
Zauważ, że plik dodany jako stub nie będzie widoczny jako część archiwum podczas tworzenia listy jego zawartości komendą phar list. Archiwum będzie można wykonać jak zwykły skrypt PHP: php zwierzeta.phar pies tygrys
W systemach Unix i pochodnych możliwe jest dodanie „bangu” na początku skryptu — dzięki temu skrypt będzie możliwy do wykonania z wiersza poleceń. Oto składnia: phar stub-set -f animals.phar -s stub.php -b '#!/usr/bin/env php'
W systemach uniksowych polecenie #!/usr/bin/env php określa się nazwą „bang”. Pozwala ono powłoce zlokalizować odpowiedni interpreter i wykonać skrypt. Archiwum animals.phar może zostać wykonane za pośrednictwem wiersza poleceń, oczywiście po przeprowadzeniu powyższych operacji: ./animals.phar pies tygrys
Stworzyliśmy pełną aplikację, zawierającą wszystkie klasy oraz stub potrzebny do wykonania skryptu. Program phar to tak naprawdę archiwum phar, które może zostać wykonane za pośrednictwem wiersza poleceń: phar list -f /opt/php/bin/phar |-phar:///opt/php/bin/phar.phar/clicommand.inc |-phar:///opt/php/bin/phar.phar/directorygraphiterator.inc |-phar:///opt/php/bin/phar.phar/directorytreeiterator.inc |-phar:///opt/php/bin/phar.phar/invertedregexiterator.inc |-phar:///opt/php/bin/phar.phar/phar.inc \-phar:///opt/php/bin/phar.phar/pharcommand.inc
105
ROZDZIAŁ 5. NOWOŚCI TECHNOLOGICZNE
Możliwe jest wypakowanie wszystkich elementów i ich przeanalizowanie. Phar jest oprogramowaniem open source, tak samo jak cały język PHP, a więc zachęcamy Cię do tego, abyś to zrobił. Istnieje także interfejs (API) pozwalający na tworzenie archiwów phar i manipulowanie nimi za pośrednictwem skryptu PHP. Phar jest prostym i zarazem bardzo ważnym programem. Zmieni metodę pakowania i dystrybuowania aplikacji PHP. Rozszerzenie to, dostępne w PHP 5.3 i późniejszych wersjach, nie wzbudziło takiego zainteresowania jak przestrzenie nazw lub nowdoc, będzie jednak miało znaczny wpływ na sposób rozpowszechniania aplikacji PHP. Zostały już zadeklarowane plany opublikowania aplikacji takich jak PhpMyAdmin i pgFouine. Phar nie wpływa na wydajność skryptów, ponieważ archiwum jest parsowane tylko raz. Nawet wtedy opóźnienie jest minimalne i, jak się wydaje, nie wpływa na czas wykonywania. Istnieje także bardzo ważny mechanizm buforowania, zwany APC, pozwalający na buforowanie zmiennych oraz całych plików. Buforowanie APC jest opcjonalnym modułem, który należy zainstalować osobno. Jest jednak często stosowany ze względu na znaczny wzrost wydajności, jaki można dzięki niemu osiągnąć. W szczególności funkcja apc_compile_file może zostać zastosowana do skompilowania skryptu PHP na kod binarny, który można przechować w buforze, a następnie wykorzystać w takiej postaci, co może znacznie poprawić wydajność aplikacji. Phar jest kompatybilny z najnowszą wersją APC, a pliki phar mogą być buforowane dzięki funkcji apc_compile_file. APC nie jest automatycznie instalowany z PHP, więc nie będzie tu omawiany. Jeżeli jesteś zainteresowany szczegółami, to więcej informacji znajdziesz pod adresem http://us3.php.net/manual/en/book.apc.php.
Podsumowanie W rozdziale tym omówiliśmy nowe funkcje PHP 5.3, takie jak przestrzenie nazw czy format nowdoc. Poznałeś SPL, które stało się integralną częścią PHP 5.3, oraz niepozorne, ale ważne rozszerzenie phar. Trudno wskazać, co w tym rozdziale było najważniejsze. Wszystkie omówione narzędzia są istotne i niezwykle przydatne. Przestrzenie nazw będą wykorzystywane coraz częściej i pozwolą na tworzenie coraz bardziej złożonych systemów, natomiast archiwa phar pozwolą na pakowanie całej aplikacji w jeden plik, dzięki czemu jej dystrybucja będzie znacznie łatwiejsza. SPL jest jeszcze w trakcie tworzenia; moduły opracowane do tej pory są niezwykle użyteczne. Tworzenie kodu w stylu znanym z wersji PHP 5.2 i wcześniejszych jest proste i nie wymaga żadnego wysiłku. Jednakże język PHP ewoluuje bardzo dynamicznie, więc ograniczenie się programisty do starego stylu może sprawić, że jego umiejętności staną się przestarzałe i w dużej mierze bezużyteczne. Nauka nowych rzeczy nie jest trudna — jest zabawna. Z całego serca ją polecamy.
106
ROZDZIAŁ 6
Tworzenie formularzy i zarządzanie nimi Formularze są często źródłem nowych danych dla aplikacji. Takie dane nie są ustrukturyzowane i często muszą być zmodyfikowane, sformatowane lub przetworzone w inny sposób przed ich zapisaniem. Dane mogą także pochodzić z potencjalnie niebezpiecznego źródła. W tym rozdziale pokażemy metody pobierania danych z formularzy, walidacji pól za pomocą JavaScriptu, przekazywania danych do PHP za pomocą Ajaksa oraz utrzymywania integralności w systemie przechowywania danych. Otrzymasz też kilka rad dotyczących operacji na obrazach, integrowania wielu języków i wykorzystywania wyrażeń regularnych.
Walidacja danych Istnieją dwa podstawowe obszary walidacji wykonywanej na formularzach. Jest ona przeprowadzana dla samego formularza po stronie klienta przy wykorzystaniu JavaScriptu oraz ma miejsce wtedy, gdy PHP odbierze dane po stronie serwera poprzez żądania GET lub POST. Rola walidacji w JavaScripcie jest dwojaka — walidacja zachodzi po stronie klienta i może zostać wykorzystana, aby przekazać mu (użytkownikowi strony) sugestie i ostrzeżenia dotyczące wprowadzonych danych oraz aby ułożyć dane według spójnego wzoru, oczekiwanego przez PHP. Walidacja PHP koncentruje się na zachowaniu integralności przekazanych danych i na działaniach powodujących, że dane będą spójne i kompatybilne z danymi już przechowywanymi. Zdefiniujemy formularz z dwoma elementami do zwalidowania przez JavaScript — do jednego z nich będzie wprowadzane obligatoryjne imię i nazwisko, a do drugiego numer telefonu z opcjonalnym numerem kierunkowym. W tym przykładzie nie będzie używany przycisk zatwierdzający; zatwierdzenie formularza będzie zawarte w funkcji JavaScriptu, która będzie aktywowana po wystąpieniu zdarzenia onblur. Omówimy to w dalszej części rozdziału. Na listingu 6.1 wykorzystujemy metodę GET, tak aby wysłane wartości pokazały się w pasku adresu przeglądarki. Dzięki temu będziemy mogli szybko przetestować różne dane i zwracane dla nich wyniki walidacji JavaScriptu. Listing 6.1. Przykład walidacji z zastosowaniem metody GET
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Kiedy testy zostaną zakończone, będzie można przełączyć metodę na POST. Krótszą formę adresu URL można uzyskać za pomocą metody POST, ponieważ dane wysłane z formularza nie są widoczne dla klienta (np. http://domena.pl/katalog/ lub http://domena.pl/katalog/skrypt.php). Gdy strona po zatwierdzeniu formularza może być dodana do zakładek, lepszym wyborem może być GET. Dzięki temu klient nie będzie musiał ponownie wypełniać formularza podczas kolejnej wizyty. Przy wykorzystaniu metody GET adres URL przeglądarki po zatwierdzeniu będzie podobny do http://domena.pl/?zmienna1=przy&zmienna2=klad. UWAGA. Możesz zastosować zarówno metodę POST, jak i GET, w zależności od wymagań projektu. Należy pamiętać, że żadna z nich nie jest bezpieczna, obie mogą być wykorzystane podczas ataku. Jednakże użycie POST może być uznane za bezpieczniejsze, ponieważ zwyczajni użytkownicy nie mogą uzyskiwać różnych rezultatów wskutek manipulowania zawartością paska adresu. Więcej informacji dotyczących bezpieczeństwa znajduje się w rozdziale 11.
Kiedy klient wypełnia pola tekstowe z listingu 6.1, funkcja walidująca JavaScriptu wywoływana jest w zdarzeniu onkeyup z referencją this. Dzięki temu JavaScript będzie miał dostęp do wszystkich podstawowych właściwości potrzebnych w procesie walidacji. Zdarzenie onblur spróbuje zatwierdzić formularz, wykorzystując Ajax, jeżeli oba pola przejdą walidację. W rozdziale 15. wyjaśnimy, jak wykonać odwołanie Ajaksa. Zdarzenie onfocus zaznacza
już wprowadzony tekst, tak aby klient nie musiał usuwać poprzednio wprowadzonych danych. Tekst zaznaczany jest za pomocą metody JavaScriptu select() przy wykorzystaniu właściwości aktualnego elementu (onfocus='this.select();'). Zdefiniujemy funkcję JavaScriptu, przyjmującą jeden parametr (listing 6.2). Walidacja będzie polegała na sprawdzeniu wyrażenia regularnego. Budowanie wyrażeń regularnych omówimy w dalszej części rozdziału. Na razie skoncentrujemy się na podstawowej strukturze. Listing 6.2. Walidacja w JavaScripcie function waliduj(a){ if(a.value.length>3) switch(a.name){ case 'nazwisko': if(a.value.match(/^[a-zA-Z\-]+ [a-zA-Z\-]+$/)){ /* ... poprawna walidacja imienia i nazwiska ... */ return true; }else{ /* ... brak walidacji imienia i nazwiska ... */ } break; case 'telefon': if(a.value.match((/^((\(|\[)?\d{3}?(\]|\))?(\s|-)?)?\d{3}(\s|-)?\d{4}$/)){ /* ... poprawna walidacja numeru telefonu ... */ return true; } else{ /* ... brak walidacji numeru telefonu ... */ } break; } return false; }//funkcja walidująca
Funkcja waliduj rozpoczyna sprawdzanie pola formularza, jeżeli długość wprowadzonego tekstu jest większa niż trzy znaki. Graniczna liczba znaków musi zostać zmieniona, jeżeli walidowane dane nie zawierają wystarczającej liczby znaków. Jeśli kod walidujący wykonuje odwołania Ajaksa, mogące wykonywać zapytania do bazy danych lub korzystać ze skomplikowanych algorytmów, ograniczenie długości tekstu pozwala zapobiegać niepotrzebnemu obciążeniu serwera. W kolejnym kroku wykorzystamy nazwę elementu formularza, aby ustalić, do którego z wyrażeń regularnych powinien pasować wprowadzony tekst. Dzięki zwracanej wartości true lub false funkcję waliduj możemy 108
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
UWAGA. Zakładamy, że dane wpisywane na listingu 6.2 muszą mieć długość większą niż trzy znaki. Jednak dane często zawierają teksty lub liczby składające się z maksymalnie trzech znaków. W tym przypadku polecenie if(a.value.length>3) mogłoby zostać przeniesione do wewnątrz wyrażenia case i być zmodyfikowanie odrębnie dla każdego z walidowanych pól.
wykorzystać także podczas wyszukiwania. Porównujemy wartość z pola tekstowego z wartością wyrażenia regularnego poprzez wywołanie funkcji match. Jeżeli dopasowanie zakończy się powodzeniem, zwrócony zostanie sprawdzany ciąg znaków. W przeciwnym wypadku zwracana jest wartość null. Na listingu 6.3 zdefiniujemy funkcję szukaj, która sprawdzi oba pola formularza. Następnie na listingu 6.4 zainicjujemy odwołanie Ajaksa, aby przekazać wartości elementów formularza do skryptu PHP. Listing 6.3. Definiowanie funkcji szukaj function szukaj(){ if(waliduj(document.getElementById('nazwisko')) &&waliduj(document.getElementById('telefon'))){ //Utworzenie i wykonanie odwołania Ajaksa } }//zapisanie funkcji
Dzięki tej funkcji wymagane parametry są przekazywane do funkcji waliduj i wewnątrz niej są sprawdzane. Samo przesłanie formularza odbywa się poprzez odwołanie Ajaksa. Jeżeli nazwisko i telefon poprawnie przejdą walidację, formularz zostanie przesłany jako URL za pośrednictwem Ajaksa. URL będzie miał postać zbliżoną do http://localhost/skrypt7_1.php?nazwisko=john+smith&telefon=(201) 443-3221. Możemy teraz rozpocząć budowę komponentu PHP służącego do walidacji. Ponieważ atrybuty przekazywane są poprzez URL, możemy przetestować różne warianty, ręcznie modyfikując adres URL z zastosowaniem znanych wyjątków i formatów. Na przykład walidację nazwiska po stronie PHP testujemy, wykorzystując następujące adresy URL: http://localhost/skrypt7_1.php?nazwisko=Shérri+smith&telefon=(201) 443-3221 http://localhost/skrypt7_1.php?nazwisko=john+o'neil&telefon=(201) 443-3221 http://localhost/skrypt7_1.php?nazwisko=john+(*#%_0&telefon=(201) 443-3221
Możemy także przetestować walidację numeru telefonu, wykorzystując taki zestaw adresów URL: http://localhost/skrypt7_1.php?nazwisko=john+smith&telefon=2014433221 http://localhost/skrypt7_1.php?nazwisko=john+smith&telefon=john+smith http://localhost/skrypt7_1.php?nazwisko=john+smith&telefon=201 443-3221 ext 21
Na listingu 6.4 możemy teraz przedstawić użyty do walidacji kod PHP. Listing 6.4. Walidacja PHP $val){ $formData[$key]=htmlentities($val,ENT_QUOTES,'UTF-8'); } if(isset($formData['nazwisko'])&&isset($formData['telefon'])){ $expressions=array('nazwisko'=>"/^[a-zA-Z\-]+ [a-zA-Z\-]+$/", 'telefon'=>"/^((\(|\[)?\d{3}?(\]|\))?(\s|-)?)?\d{3}(\s|-)?\d{4}$/" ); if(preg_match($expressions['nazwisko'],$formData['nazwisko'],$matches['nazwisko'])===1 && preg_match($expressions['telefon'],$formData['telefon'],$matches['telefon'])===1){ /* kod mający coś zrobić z nazwiskiem i telefonem */ } } ?>
109
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Funkcja preg_match przyjmuje wyrażenie regularne, ciąg znaków, który ma być sprawdzony, oraz tablicę wypełnianą wynikiem porównania. Istnieje kilka użytecznych rozszerzeń PHP, pomagających w walidacji danych, czyszczeniu danych oraz porównywaniu wyrażeń. Filtrowanie może być dobrym i spójnym sposobem na czyszczenie i walidowanie danych. Zawiera kilka popularnych funkcji służących walidacji danych wprowadzanych przez użytkownika. Listing 6.5 przedstawia filtry walidujące URL i e-mail z biblioteki Filter. Funkcję filter_var wykorzystuje się poprzez przekazanie do niej łańcucha znaków oraz filtra czyszczącego lub walidacji. Filtr czyszczący usuwa z tego łańcucha wszystkie niewspierane znaki, natomiast filtr walidujący sprawdza, czy łańcuch jest sformatowany poprawnie i czy przedstawia odpowiedni typ danych. Listing 6.5. Funkcja filter_var w PHP
Filtry czyszczące są przydatne podczas poprawiania danych pod względem spójności. Filtry te także wykorzystują funkcję filter_var. Listing 6.6 przedstawia użycie filtrów czyszczących dla adresów e-mail oraz URL. Listing 6.6. Poprawianie spójności danych przy zastosowaniu filtrów
110
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Biblioteka Perl Compatible Regular Expressions (PCRE) również zawiera kilka użytecznych funkcji, na przykład operujące na wyrażeniach regularnych find, replace, grep, match oraz match all, a także funkcje find i replace, wykorzystujące odwołania zwrotne. Na listingu 6.7 użyta została funkcja preg_match_all w celu znalezienia wszystkich ciągów znaków rozpoczynających się od dużej litery, po której następują małe litery. Zastosujemy funkcję var_export do wyświetlenia wyniku z tablicy $wyniki. Listing 6.7. Wykorzystanie funkcji preg_match_all array ( 0 => 'Samochód', 1 => 'Ford', 2 => 'Super', 3 => 'Myjnia', ), ) */ ?>
Na listingu 6.7 do funkcji preg_match_all przekazywane są trzy parametry: wyrażenie regularne, łańcuch znaków, w którym wyszukiwane są wyniki, i tablica, do której wyniki są zapisywane. Możemy także przekazać flagę w celu dostosowania tablicy wyników i wartość przesunięcia pozwalającego na rozpoczęcie wyszukiwania od określonego znaku. Na listingu 6.8 przedstawiono przykład. Listing 6.8. Dostosowanie tablicy $wyniki array ( 0 => 'Ford',
111
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Pozostałe funkcje PCRE działają podobnie i są przydatne podczas walidacji, ekstrakcji i modyfikacji danych.
112
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Wczytywanie plików i obrazów Dodawanie dokumentów na serwerze jest powszechną praktyką. Dokumenty z reguły znajdują się na zdalnym komputerze lub serwerze i muszą zostać przeniesione na serwer hostujący aplikację. Można tego dokonać, wykorzystując elementy formularza. Zanim zaczniemy dodawać pliki z zastosowaniem formularza, dobrym pomysłem jest sprawdzenie pliku konfiguracyjnego PHP. Zawiera on wiele ustawień, które bezpośrednio wpływają na sposób działania formularza — na to, co może zrobić, jak dużo i w jakim czasie. Znajomość tych ustawień będzie bardzo pomocna podczas rozwiązywania problemów dotyczących wczytywania plików przez formularz oraz z samym formularzem. Typowym problemem jest brak uprawnień do zapisu plików na serwerze odbierającym dane. Formularz z listingu 6.9 pozwoli na dodanie plików za pomocą przycisku przeglądania plików oraz za pomocą adresu URL z serwera. W formularzu został zdefiniowany jego nowy atrybut, enctype, który określa sposób kodowania danych w tym formularzu. Jest to konieczne, jeżeli przesyłane są dane binarne — w tym przypadku pliki. Listing 6.9. Definiowanie atrybutu enctype
Jeżeli wartość właściwości enctype nie zostanie zdefiniowana, to przyjmowana jest domyślna, application/x-www-formurlencoded, która obsłuży większość przypadków z wyjątkiem plików i danych innych niż ASCI. Kiedy użytkownik strony zatwierdzi formularz, dwie różne zmienne superglobalne zostaną wykorzystane do przechowywania danych: $_FILES dla wczytywanego pliku lokalnego oraz $_POST dla pola adresurl. Zmienna $_FILES będzie zawierała metadane dotyczące wczytywanego pliku. Metadane przechowywane w $_FILES pokazano w tabeli 6.1. Tabela 6.1. Metadane przechowywane w zmiennej $_FILES Zmienna (funkcja)
Opis
Przykład
$_FILES['pliklokalny']['name']
Pierwotna nazwa pliku na komputerze użytkownika
jeep.png
$_FILES['pliklokalny']['size']
Rozmiar wczytywanego pliku w bajtach
12334
$_FILES['pliklokalny']['tmp_name']
Nazwa tymczasowa wczytywanego pliku na serwerze
/tmp/phpclbig4
$_FILES['pliklokalny']['type']
Typ MIME pliku
image/png
$_FILES['pliklokalny']['error']
Kod błędu skojarzony z wczytywanym plikiem
is_uploaded_file(tmpname)
Zwraca wartość logiczną, jeżeli plik został wczytany za pośrednictwem HTTP POST
Zanim plik pliklokalny zostanie przeniesiony do docelowej lokalizacji, warto sprawdzić, czy pochodzi z żądania HTTP POST. Można tego dokonać przy wykorzystaniu funkcji is_uploaded_file(), która przyjmuje
113
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
jeden parametr — tymczasową nazwę pliku ['tmp_name']. Aby przenieść plik do katalogu po jego wczytaniu na serwer, należy zastosować funkcję move_uploaded_file(), przyjmującą dwa parametry: tymczasową nazwę pliku ['tmp_name'] i lokalizację docelową. Dla zdalnego adresu URL jest kilka metod, których możemy użyć w celu uzyskania pliku. Wygodnym sposobem na pobranie pliku za pośrednictwem HTTP, HTTPS i FTP jest aplikacja wget (aplikacja wiersza poleceń), wywołana za pomocą funkcji shell_exec. Funkcja ta wykonuje polecenie i zwraca jego wynik (jeżeli wystąpi). Inna metoda pobierania plików to np. wykorzystanie narzędzi związanych z gniazdami, jak fsocketopen lub curl. Plik może zostać pobrany przy zastosowaniu następującej składni: shell_exec('wget '. escapeshellcmd($_POST['adresurl']));
Zwróć uwagę na funkcję escapeshellcmd. Służy tu ona do przekształcenia znaków wykorzystywanych powszechnie podczas ataków, aby można było zapobiec wykonaniu na serwerze niepożądanych poleceń.
Konwersja obrazów i miniatury W pracy z aplikacjami internetowymi często wykorzystywane są pliki graficzne. Obrazy mogą być umieszczane w galerii zdjęć, pojawiać się jako zrzuty ekranowe czy w pokazie slajdów. W poprzednim podrozdziale wyjaśniliśmy, w jaki sposób dodawać dokumenty na serwer poprzez wczytywanie ich za pośrednictwem formularza lub za pomocą aplikacji wget w połączeniu z poleceniem shell_exec. Teraz, kiedy pliki już znalazły się na serwerze, możemy zacząć nimi manipulować, tak aby pasowały do struktur innych programów. PHP posiada bibliotekę GD — zawiera ona funkcje pozwalające na tworzenie obrazów i manipulowanie nimi. Omówimy tylko małą ich część — funkcje dotyczące zmiany rozmiarów i konwersji. Funkcja php_info() może zostać użyta do sprawdzenia wersji biblioteki GD zainstalowanej na serwerze. Podczas tworzenia miniatury obrazu wykonamy jego kopię PNG, zmniejszoną do szerokości 200 px oraz proporcjonalnej wysokości obliczonej na podstawie oryginalnych wymiarów. Aby uzyskać wymiary obrazu, wykorzystamy funkcję getimagesize(), która jako parametr przyjmuje nazwę obrazu, natomiast zwraca tablicę metadanych zawierającą wysokość, szerokość oraz typ MIME. Przykład przedstawiono na listingu 6.10. Listing 6.10. Wykorzystanie funkcji getimagesize()
114
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Tablica o nazwie $metaDane przechowuje typ MIME oraz wysokość i szerokość oryginalnego pliku. Te informacje zostaną wykorzystane, aby w GD otworzyć oryginalny obraz. Możemy teraz ustalić wartość zmiennej $nowaWysokosc dla miniaturki. Typ MIME sprawdzany jest wewnątrz polecenia switch, aby można było obsłużyć różne typy plików. Funkcja imagecreatefromjpg i podobne otwierają oryginalny obraz i zwracają uchwyt do zasobu. Miniatura o podanym rozmiarze tworzona jest za pomocą imagecreatetruecolor. Podczas tworzenia miniatury funkcja imagecopyresampled przyjmuje kilka parametrów: uchwyt do miniatury, uchwyt do oryginalnego pliku, wartości x i y dla punktów docelowych i źródłowych, nową szerokość i wysokość oraz oryginalną wysokość i szerokość. Miniatura tworzona jest przez funkcję imagepng i przekazanie do niej uchwytu miniatury oraz nowej nazwy pliku. Uchwyt jest usuwany za pośrednictwem funkcji imagedestroy, do której jest przekazany. W rezultacie otrzymujemy plik PNG w wybranym formacie. Na listingu 6.11 pokazane są wyniki działania funkcji getimagesize() dla oryginalnego pliku i miniatury. Listing 6.11. Wyniki działania funkcji getimagesize() dla oryginalnego pliku i miniatury //obraz.jpg array ( 0 => 1600, 1 => 1200, 2 => 2, 3 => 'width="1600" height="1200"', 'bits' => 8, 'channels' => 3, 'mime' => 'image/jpeg', ) //miniatura.png array ( 0 => 200, 1 => 150, 2 => 3, 3 => 'width="200" height="150"', 'bits' => 8, 'mime' => 'image/png', )
Miniatura pokazana na rysunku 6.1 została utworzona przez skrypt z listingu 6.10. Oryginalny obraz był w formacie JPEG i miał wymiary 1200 px na 900 px. Plik wynikowy jest w formacie PNG, a jego wymiary to 200 px na 150 px.
Wyrażenia regularne Wyrażenia regularne są wykorzystywane do opisywania wzorców w tekście, mogą być stosowane w JavaScripcie, MySQL i PHP. Zanim zaczniemy budować wyrażenia regularne (regex), powinniśmy najpierw zaopatrzyć się w edytor wyrażeń regularnych. Darmowy i prosty w użyciu edytor dla systemu Windows to Regex Tester dostępny pod adresem antix.co.uk. Istnieje wiele innych edytorów, niektóre z nich mają znacznie więcej opcji. Regex Tester jest jednak doskonałym narzędziem na początek.
115
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Rysunek 6.1. Miniatura utworzona przez skrypt z listingu 6.10 Tabela 6.2 prezentuje możliwe do zastosowania znaki i ich przykłady w wyrażeniach regularnych. Tabela 6.2. Dostępne znaki i ich przykłady Znak
Znaczenie
Przykład
[znaki]
jeden z podanych znaków
/ko[tp]/ odpowiada wyrazom kot i kop
[^znaki]
żaden z podanych znaków
/ko[tp]/ odpowiada wyrazowi koń, nie odpowiada wyrazom kot i kop
(znaki)
wszystkie podane znaki
/k(ot)/ odpowiada wyrazowi kot
{n}
dokładnie n razy
/\d{2}/ odpowiada liczbie 12, gdzie \d reprezentuje
liczbę {n,}
n lub więcej razy
/\d{2,}/ odpowiada liczbie 1234
{n,m}
od n do m razy
/\d{2,3}/ odpowiada liczbie 123
\
znak ucieczki
/Dr\./ odpowiada wyrażeniu Dr.
+
jeden lub więcej razy
/\d+/ odpowiada liczbie 1
*
zero lub więcej razy
/\d*/ odpowiada pustemu ciągowi oraz liczbie 12
?
zero lub jeden raz
/\d?/ odpowiada pustemu ciągowi i liczbie 1
.
dowolny znak poza znakiem nowej linii
/./ odpowiada znakowi a
|
lub
/a|b/ odpowiada znakom a lub b
^
rozpoczęcie linii lub danych wejściowych
/^a/ odpowiada rozpoczęciu linii
$
koniec linii lub danych wejściowych
/a$/ odpowiada końcowi linii
\w
znak alfanumeryczny
/a\w/ odpowiada wyrażeniu ab
\W
znak inny niż alfanumeryczny
/a\W/ odpowiada wyrażeniu a?
\s
biały znak
/a\sb/ odpowiada wyrażeniu 'a b'
\S
inny niż biały znak
/a\Sb/ odpowiada wyrażeniu a-b
\b
granica słowa
/\bKo/ odpowiada wyrażeniu Ko w słowie Kot
\B
brak granicy słowa
/\Bot/ odpowiada wyrażeniu ot w słowie Kot
\d
liczba
/A\d/ odpowiada wyrażeniu A4
\D
znak inny niż liczba
/A\D/ odpowiada wyrażeniu AA
W tabeli 6.2 widzimy najczęściej stosowane znaki oraz pasujące do nich wyrażenia. Na listingach 6.7 i 6.8 wykorzystane zostało wyrażenie [A-Z][a-z]+. Zgodnie z tabelą to wyrażenie możemy odczytać następująco: „Dopasuj jeden duży znak alfabetyczny ([A-Z]), po nim mały znak alfabetyczny ([a-z]) powtórzony jeden raz lub więcej razy (+)”. Łącznik wykorzystany między znakami A i Z interpretowany jest jako dowolny znak z zakresu
116
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
od A do Z włącznie. Kolejnym przykładem zastosowania łącznika jest wyrażenie [a-e], do którego dopasowane zostaną litery a, b, c, d i e, ale nie f czy g. Można także użyć liczb. Wyrażenie [1-3] pasuje do liczb 1, 2 i 3, ale nie pasuje do 4 lub 5. Wyrażenie [A-Z][a-z]{4} dopasuje wszystkie pięciowyrazowe słowa rozpoczynające się od dużej litery. W JavaScripcie możemy porównywać wyrażenia regularne za pomocą metod match, search, replace i split, zgodnie z tabelą 6.3. Można także użyć obiektu RegExp, który posiada metody compile, exec i test, zgodnie z tabelą 6.4. Tabela 6.3. Wykorzystanie wyrażeń regularnych za pomocą metod match, search, replace i split Metoda
Opis
Przykład
String.match(regex)
Sprawdzenie ciągu znaków z wyrażeniem i zwrócenie dopasowanych podciągów lub wartości null, jeśli wyrażenie nie zostanie dopasowane
var str="kod 90210"; var wyrazenie=/[0-9]{5}/; str.match(wyrazenie); //zwróci 90210
String.search(regex)
Sprawdzenie ciągu znaków z wyrażeniem i zwrócenie pozycji dopasowanego podciągu lub -1, jeśli wyrażenie nie zostanie dopasowane
var str="kod 90210"; var wyrazenie=/[0-9]{5}/; str.search(wyrazenie); //zwróci 4
String.replace(regex,tekst)
Sprawdzenie ciągu znaków z wyrażeniem i zamiana dopasowanych ciągów na podany tekst
var str="kod 90210"; var wyrazenie=/[0-9]{5}/; str.replace(wyrazenie,'-----'); //zwróci "kod -----"
String.split(regex)
Sprawdzenie ciągu znaków z wyrażeniem i podzielenie go zgodnie z wyrażeniem
var str="kod 90210 ustawiony"; var wyrazenie=/[0-9]{5}/; var czesci=str.split(wyrazenie); //zwróci tablicę ["kod","ustawiony"]
Tabela 6.4. Wykorzystanie wyrażeń regularnych za pomocą metod compile, exec i test Metoda
Opis
Przykład
Regexp.compile(regex,flaga)
Sprawdzenie tablicy z wyrażeniem
$takst=array('kod 90210','lub kod 90211'); $wyrazenie='/[0-9]{5}/'; $wyniki=preg_grep($wyrazenie,$tekst); //$wyniki zawiera: Array(0 => 'kod 90210', 1 => 'lub kod ´90211')
Regexp.exec(string)
Sprawdzenie ciągu znaków z wyrażeniem i zwrócenie wszystkich dopasowań
W PHP możemy porównywać ciągi znaków z wyrażeniami za pomocą funkcji PCRE, zgodnie z tabelą 6.5. Jak widać na przykładach, w PHP i JavaScripcie wyrażenia regularne mogą być bardzo użyteczne. Są doskonałym narzędziem do prostych i złożonych operacji na danych. Wyrażenia mogą także być stosowane
117
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
Tabela 6.5. Wykorzystanie funkcji PCRE Metoda
Opis
Przykład
preg_grep(regex, array)
Sprawdzenie tablicy z wyrażeniem
$tekst=array('kod 90210','lub kod 90211'); $wyrazenie='/[0-9]{5}/'; $wyniki=preg_grep($wyrazenie,$tekst); //$wyniki zawiera: Array(0 => 'kod 90210', 1 => 'lub kod 90211)
preg_match_all(regex, string, matches)
Sprawdzenie ciągu znaków z wyrażeniem i zwrócenie wszystkich dopasowań
Sprawdzenie ciągu znaków z wyrażeniem i podzielenie go zgodnie z wyrażeniem
w MySQL do wykonywania złożonych wyszukań. Można np. utworzyć zapytanie "select * from kontakty where opis regexp '[0-9]{5}'", które zwróci wszystkie kontakty posiadające w kolumnie „Opis” poprawny pięcioznakowy kod (lub jakąkolwiek pięciocyfrową liczbę).
Integracja języków To zawsze niespodzianka, kiedy przeglądasz dane, które powinny być wyświetlone w pewien sposób, a zamiast tego pojawiają się dziwne znaki zagnieżdżone w tekście. W większości przypadków jest to spowodowane problemami z kodowaniem. Niezależnie od tego, czy dane pochodzą z formularza, pliku CSV, czy innego dokumentu, skrypt wprowadzający dane napotkał znak, którego się nie spodziewał, spoza zakresu pewnego zestawu znaków, najprawdopodobniej ISO-8859-1 lub UTF-8. UWAGA. Zestaw ISO-8859-1 (Latin-1) zawiera ośmiobitowe znaki graficzne ASCII i zarówno standardowe, jak i rozszerzone znaki ASCII (0 – 255). UTF-8 jest kodowaniem wielobajtowym dla znaków Unicode, zawiera znaki z ISO-8859-1 oraz znaki diakrytyczne.
118
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
W przypadku napotkania problemów związanych z kodowaniem jest kilka rzeczy, które powinieneś zaobserwować i zidentyfikować. Ważne jest źródło danych — jeżeli dane pochodzą ze strony internetowej, należy się upewnić, że został ustawiony odpowiedni Content-Type. Można to sprawdzić w sekcji HEAD dokumentu XHTML. Gdyby konieczne było kodowanie UTF-8, wpis wyglądałby następująco:
Baza danych może być kolejnym źródłem problemów z kodowaniem. Podczas tworzenia bazy danych MySQL ustawiany jest zestaw znaków. Może być też zmieniony dla tabel i wierszy. Znak z zestawu UTF-8 może zostać błędnie zinterpretowany, jeżeli baza danych spodziewa się innego kodowania. Dla kodowania UTF-8 zestaw znaków to utf8_general_ci (niezależny od wielkości liter) lub utf8_unicode_ci. Możemy także wymusić, aby MySQL oczekiwał znaków UTF-8 podczas wykonywania zapytania 'set names utf8' po połączeniu się z bazą danych. Kod PHP również może niepoprawnie identyfikować kodowanie tekstu. Jest wiele funkcji i bibliotek, które mogą pomóc w radzeniu sobie z problemami z kodowaniem — istnieją także flagi oznaczające konkretne kodowania. Funkcja utf8_encode() zakoduje tekst ISO-8859-1 jako UTF-8. Kolejną przydatną funkcją przy zmianie kodowania tekstu jest mb_convert_encoding() — przyjmuje ona tekst, kodowanie docelowe i kodowanie źródłowe. Funkcje mp_* pochodzą z biblioteki Multibyte String Function zawierającej wiele różnych funkcji do operacji na znakach wielobajtowych. Te funkcje to między innymi mb_substr (pobranie części ciągu), mb_strlen (pobranie długości ciągu) i mb_eregi_replace (zamiana ciągu za pomocą wyrażeń regularnych). Niektóre z funkcji PHP także mogą przyjmować flagi kodowania. Używając funkcji htmlentities(), możemy przekazać flagę, aby określić kodowanie UTF-8 — htmlentities($str,ENT_QUOTES,'UTF-8'). Oprócz funkcji MB istnieją jeszcze moduł iconv oraz wbudowane w PHP wsparcie dla kodowania. Funkcje iconv, a w szczególności funkcja iconv(), konwertują tekst na podane kodowanie. Wykonując polecenie iconv("UTF8","ISO-8859-1//IGNORE//TRANSLIT",$str), konwertujemy tekst UTF-8 na jego ekwiwalent w kodowaniu ISO-8859-1. Flagi //IGNORE i //TRANSLIT pozwalają określić sposób postępowania ze znakami, których przekonwertowanie nie jest możliwe. Flaga IGNORE usunie (bez komunikatu) nieznany znak i znaki następujące po nim, natomiast flaga TRANSLIT spróbuje ustalić poprawny znak.
Podsumowanie Podczas projektowania i tworzenia formularzy jest kilka ważnych kwestii, o których należy pamiętać, np. walidacja, spójność danych, manipulacje plikami (także obrazami). W tym rozdziale pokazaliśmy wiele metod pozwalających na radzenie sobie z tymi problemami i odpowiednie przykłady. Dane możesz zwalidować po stronie klienta, przy wykorzystaniu JavaScriptu, oraz po stronie serwera — dzięki PHP. Oba podejścia są komplementarne. Walidacja po stronie klienta skupia się raczej na sprawdzeniu, czy dane wprowadzone przez użytkownika są akceptowalne, natomiast walidacja po stronie serwera powinna dbać o zapewnienie spójności przechowywanych danych. Żaden poważny twórca aplikacji webowych nie może obejść się bez znajomości wyrażeń regularnych, dlatego przedstawiliśmy krótkie wprowadzenie do zagadnienia. Wyrażenia mogą być wykorzystywane w wielu przypadkach do znajdowania, sprawdzania, zastępowania i dzielenia łańcuchów znaków. Przedstawiliśmy proste przykłady prezentujące poszczególne czynności w JavaScripcie i PHP. Na końcu rozdziału pokazaliśmy w skrócie, dlaczego konieczne jest dbanie o to, aby dane były wprowadzane w czytelnym formacie. W następnym rozdziale przyjrzymy się integracji PHP z bazami danych innymi niż powszechnie znane bazy relacyjne, np. SQLite3, MongoDB i CouchDB.
119
ROZDZIAŁ 6. TWORZENIE FORMULARZY I ZARZĄDZANIE NIMI
120
ROZDZIAŁ 7
Integracja z bazami danych. Część I W tym rozdziale będziemy pracować głównie z bazami NoSQL. Najpopularniejsze z nich to MongoDB, CouchDB, Google Big Table i Cassandra, są jednak także inne. Jak wskazuje nazwa, bazy NoSQL nie są klasycznymi bazami SQL i nie implementują właściwości ACID. ACID (ang. atomicity, consistency, isolation, durability) oznacza atomowość, spójność, izolację i trwałość, które są standardowymi właściwościami systemów relacyjnych baz danych. Systemy NoSQL nie mają warstwy zarządzania transakcjami, zatwierdzania transakcji ani mechanizmu ich cofania. Nie mają też struktury, co oznacza, że nie mają wdrożonego wzorca schemat-tabela-kolumna. Zamiast tabel mają kolekcje, które różnią się od tych pierwszych, ponieważ przechowują różnorodne wiersze lub dokumenty — jak nazywa się je w bazach NoSQL. Różnica między dokumentami i wierszami polega na tym, że wiersze mają ustaloną strukturę wynikającą ze struktury relacyjnej, a dokumenty nie. Ponadto bazy NoSQL nie przechowują wierszy w tradycyjnym ich rozumieniu — przechowują dokumenty. Dokumenty są obiektami zapisanymi w notacji JSON (JavaScript Object Notation). Oto przykład dokumentu JSON: var= { "klucz1":"wartosc1", "klucz2": { "klucz3":"wartosc3" }, "klucz4":["a1","a2","a3"...], … }
Jest to jeden z wielu formatów utworzonych w celu skrócenia nadmiarowych zapisów XML. Bazy NoSQL w większości wykorzystują JavaScript jako wewnętrzny język bazodanowy do manipulacji dokumentami JSON. Takie bazy są tworzone z myślą o dwóch właściwościach: • wysoka wydajność i skalowalność, • małe wymagania administracyjne. Zazwyczaj wyszukiwanie w obrębie jednej kolekcji jest bardzo szybkie, nie ma jednak możliwości wykonywania złączeń. Złączenia muszą być wykonywane już wewnątrz aplikacji. Wysoka wydajność osiągana jest za pomocą opatentowanego przez Google algorytmu mapreduce („dziel i rządź”), dzięki któremu bazy NoSQL są wysoce skalowalne i można ich używać na luźno sparowanych klastrach. Algorytm Google pozwala na efektywne dzielenie zadań pomiędzy wiele maszyn, nie wymaga przy tym niczego poza połączeniem sieciowym pomiędzy współpracującymi serwerami. Ten typ baz danych jest nowy. Ich użytkowanie zapoczątkowano w roku 2009 i nie opracowano jeszcze standardów dotyczących języka dostępu do danych. Języki dają do dyspozycji przeważnie następujące polecenia zaimplementowane jako odwołania do ich API (Application Programming Interface): insert (wstaw), find (znajdź), findOne (znajdź jeden), update (modyfikuj) i delete (usuń). Dokładna składnia i opcje każdego z poleceń są różne w różnych systemach. Ponadto generatory aplikacji, takiej jak Cake czy Symphony, nie zostały dobrze
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
przetestowane z większością z tych baz danych, co powoduje, że tworzenie opartych na nich aplikacji jest trudniejsze. Wróćmy na chwilę do wymagań ACID; są następujące: • Każda transakcja kończy się sukcesem lub niepowodzeniem tylko jako całość. Jeżeli transakcja zakończy się niepowodzeniem, musi być przywrócony stan bazy sprzed transakcji — tak jakby transakcja nigdy nie miała miejsca (atomowość). • Każda transakcja ma dostęp wyłącznie do danych zatwierdzonych przed jej rozpoczęciem (spójność). • Użytkownicy nie widzą zmian wykonanych przez innych użytkowników przed ich zatwierdzeniem (izolacja). • Zatwierdzone zmiany są trwałe. Dane nie mogą być utracone, nawet jeżeli baza danych przestanie działać (trwałość). Wymagania ACID są przestrzegane przez wszystkie poważniejsze systemy baz danych relacyjnych i zostały opracowane w oparciu o systemy bankowe. Transakcja w systemie relacyjnym (RDBMS) zamodelowana jest zgodnie z jej odpowiednikiem w prawdziwym świecie finansów. Wszystkie powyższe wymagania są spełniane podczas płacenia czekiem. Jeżeli dostępne środki są wystarczające, transakcja zmodyfikuje kwoty na kontach bankowych płacącego i otrzymującego zapłatę; jeżeli środki nie są wystarczające, żadne z kont nie będzie zmodyfikowane. Każda z transakcji będzie „widziała” stan konta w chwili jej rozpoczęcia. Transakcje innych użytkowników nie mają na siebie wpływu, a po dokonaniu transakcji zostanie zapisana informacja o niej. Brak spełnienia wymagań ACID przez bazy typu NoSQL powoduje, że nie nadają się one dla systemów bankowych oraz innych procesów biznesowych mających podobne wymagania. Brak struktury natomiast powoduje trudności w użytkowaniu baz NoSQL z warstwami dostępu do danych, takimi jak Hibernate, co z kolei przekłada się na wydłużenie czasu tworzenia oprogramowania. Bazy NoSQL najlepiej nadają się na potrzeby dużych hurtowni danych ze względu na swoją szybkość i skalowalność. Jak już wcześniej wspomniano, ten typ baz jest nowością, więc można spodziewać się rozmaitych przygód podczas diagnozowania błędów.
Wprowadzenie do MongoDB MongoDB jest najpopularniejszą bazą danych typu NoSQL, głównie dzięki prostej instalacji, dużej wydajności i liczbie dostępnych opcji. Instalacja interfejsu PHP do bazy MongoDB jest bardzo prosta, zwłaszcza w systemach Unix i Linux. Wystarczy uruchomić polecenie pecl install mongo. Rezultat wygląda następująco: pecl install mongo downloading mongo-1.1.3.tgz ... Starting to download mongo-1.1.3.tgz (68,561 bytes) ................done: 68,561 bytes 18 source files, building running: phpize Configuring for: PHP Api Version: 20041225 Zend Module Api No: 20060613 Zend Extension Api No: 220060519 building in /var/tmp/pear-build-root/mongo-1.1.3 …............................. (mnóstwo informacji kompilatora) Build process completed successfully Installing '/usr/lib/php5/20060613+lfs/mongo.so' install ok: channel://pecl.php.net/mongo-1.1.3 configuration option "php_ini" is not set to php.ini location You should add "extension=mongo.so" to php.ini
Instalacja jest kompletna. W systemach Windows instalacja jest jeszcze łatwiejsza. Należy pobrać gotową wersję ze strony http://www.mongodb.org/. Wszystko, co trzeba zrobić, to umieścić ją w odpowiednim miejscu i zaktualizować plik php.ini.
122
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Po wykonaniu tych czynności mamy do dyspozycji kilka klas. MongoDB nie przestrzega standardów SQL, więc dostępne typy danych są nieco inne niż w przypadku baz relacyjnych. Każdy typ danych w MongoDB jest zdefiniowany jako klasa PHP. Informacje dotyczące klas MongoDB można znaleźć na stronach PHP, pod adresem http://us3.php.net/manual/en/book.mongo.php. Oprócz klas typów istnieją jeszcze klasy opisujące połączenia, kolekcje, kursory i wyjątki. Kolekcje są z grubsza podobne do tabel dostępnych w RDBMS. Kolekcja jest nazwaną kolekcją dokumentów, które nie muszą mieć takiej samej struktury, może zostać zaindeksowana lub podzielona na partycje. Kolekcje przechowywane są w fizycznych strukturach nazywanych „bazami”, które są zaimplementowane jako kolekcje plików bazodanowych. Jeżeli baza lub kolekcja nie istnieją w momencie wstawiania dokumentu, są tworzone automatycznie. Oto jak wygląda wynik działania polecenia mongo dla pustej instalacji MongoDB : mongo MongoDB shell version: 1.6.5 connecting to: test > show dbs admin local >
Polecenie show dbs pokaże nam dostępne bazy danych. Książka dotyczy języka PHP, a nie bazy MongoDB, więc nie będziemy się zagłębiać w szczegóły interfejsu wiersza poleceń. W internecie można znaleźć wiele kursów dotyczących MongoDB — prawdopodobnie najlepszy i najbardziej kompletny jest ten na oficjalnej stronie MongoDB. Spójrzmy teraz na pierwszy skrypt PHP, który utworzy bazę test i kolekcję prac. Kolekcja zostanie zasilona czternastoma wierszami. Opisuje ona pracowników małej firmy (listing 7.1). Listing 7.1. Skrypt PHP, który utworzy bazę test i kolekcję prac1 7369, "pNazwisko" => "KOWALSKI", "stanowisko" => "SPRZEDAWCA", "men" => 7902, ´"datazatrudnienia" => "17-DEC-80", "pensja" => 800, "dzialId" => 20), array("pId"=>7499, "pNazwisko" => "NOWAK", "stanowisko" => "SPRZEDAWCA", "men" => 7698, ´"datazatrudnienia" => "20-FEB-81", "pensja" => 1600, "prowizja" => 300,"dzialId"=>30), array("pId"=>7521, "pNazwisko"=>"BONIEK", "stanowisko"=>"SPRZEDAWCA","men"=>7698, ´"datazatrudnienia"=>"22-FEB-81", "pensja"=>1250,"prowizja"=>500, "dzialId" => 30), array("pId"=>7566, "pNazwisko" => "DOBROWOLSKI", "stanowisko" => "MENEDZER", "men" => 7839, ´"datazatrudnienia" => "02-APR-81", "pensja" => 2975, "dzialId" => 20), array("pId"=>7654, "pNazwisko" => "MATUSZCZYK", "stanowisko" => "SPRZEDAWCA", "men" => 7698, ´"datazatrudnienia" => "28-SEP-81", "pensja" => 1250, "prowizja" => 1400,"dzialId"=>30), array("pId"=>7698, "pNazwisko"=>"CIBORSKI", "stanowisko"=>"MENEDZER", "men"=>7839, ´"datazatrudnienia"=>"01-MAY-81", "pensja"=>2850,"dzialId"=>30), array("pId"=>7782, "pNazwisko"=>"KOS", "stanowisko"=>"MENEDZER", "men"=>7839, ´"datazatrudnienia"=>"09-JUN-81", "pensja"=>2450,"dzialId"=>10), array("pId"=>7788, "pNazwisko"=>"KACZMAREK", "stanowisko"=>"ANALITYK", "men"=>7566, ´"datazatrudnienia"=>"19-APR-87", "pensja"=>3000,"dzialId"=>20), array("pId"=>7839, "pNazwisko"=>"KROL", "stanowisko"=>"PREZES", "datazatrudnienia" => ´"17-NOV-81", "pensja" => 5000, "dzialId" => 10), array("pId"=>7844, "pNazwisko" => "JANKIEWICZ", "stanowisko" => "SPRZEDAWCA", "men" => 7698, ´"datazatrudnienia" => "08-SEP-81", "pensja" => 1500, "prowizja" => 0,"dzialId"=>30), array("pId"=>7876, "pNazwisko"=>"DORUCH", "stanowisko"=>"SPRZEDAWCA", "men"=>7788, ´"datazatrudnienia"=>"23-MAY-87", "pensja"=>1100,"dzialId"=>20), 1
W przykładzie zastosowano daty w formacie angielskim. Jest to niezbędne do poprawnego działania funkcji strtotime wykorzystywanej w dalszej części rozdziału — przyp. tłum.
Struktura kodu jest bardzo prosta. Są w nim zdefiniowane nazwa hosta i port, na którym będzie nawiązane połączenie (localhost:27017), nazwa bazy danych (test) oraz nazwa kolekcji (prac). UWAGA. Nie zostały podane login i hasło, ale możliwe jest ich zdefiniowanie. Zaraz po instalacji baza dostępna jest dla wszystkich, którzy nawiążą z nią połączenie. Możliwe jest jej zabezpieczenie, tak aby żądała loginu i hasła.
Tablica $prac definiuje wszystkich pracowników firmy; przechowuje zagnieżdżone tablice, ponieważ dokumenty MongoDB reprezentowane są w PHP jako tablice asocjacyjne. Zauważ, że atrybuty tablic nie są jednakowe. Niektóre tablice mają atrybut prowizja, podczas gdy inne nie. Ponadto pracownik KROL nie ma atrybutu men. Wartości NULL ani żaden inny rodzaj zapełniania pustych atrybutów nie są potrzebne. Kolekcje MongoDB mogą przechowywać dokumenty heterogeniczne. Baza danych oraz kolekcja będą utworzone w momencie pierwszego wstawienia do nich dokumentu. Najlepszym miejscem, aby zaobserwować, co się dzieje, jest plik logu MongoDB. Jego lokalizacja zależy od instalacji. W systemach Linux przeważnie zlokalizowany jest w podkatalogu log głównego katalogu MongoDB. Oto, co pojawi się w logu, kiedy zostanie wykonany powyższy skrypt: Tue Tue Tue Tue Tue Tue Tue Tue Tue Tue
[initandlisten] connection accepted from 127.0.0.1:29427 #3 allocating new datafile /data/db/test.ns, filling with zeroes... done allocating datafile /data/db/ test.ns, size: 16MB, took 0 secs allocating new datafile /data/db/ test.0, filling with zeroes... done allocating datafile /data/db/ test.0, size: 64MB, took 0 secs allocating new datafile /data/db/ test.1, filling with zeroes... done allocating datafile /data/db/ test.1, size: 128MB, took 0 secs [conn3] building new index on { _id: 1 } for test.prac [conn3] done for 0 records 0.001secs [conn3] end connection 127.0.0.1:29427
Jak widać, nasza instancja MongoDB ma teraz nową bazę. Do jej założenia nie były potrzebne żadne specjalne uprawnienia. Wiersz poleceń MongoDB pokaże teraz inne informacje: > show dbs local (empty) test 0.078125GB > use test switched to db test > show collections
124
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
prac system.indexes >
Baza danych test jest już widoczna po wywołaniu polecenia show dbs, a polecenie show collections wyświetli kolekcje prac. Zobaczmy, co jeszcze można osiągnąć za pośrednictwem wiersza poleceń: > db.prac.ensureIndex({pId:1},{unique:true}); > db.prac.ensureIndex({pNazwisko:1}); > db.prac.count();
Te trzy polecenia założą indeks unikalny na atrybucie pId — dzięki czemu nie będzie możliwe, aby dwa wiersze miały taką samą wartość tego atrybutu — indeks zwykły na polu pNazwisko oraz policzą, ile dokumentów znajduje się w kolekcji. W kolekcji znajduje się czternaście dokumentów, nie czternaście wierszy. W przypadku baz NoSQL mówimy zawsze o dokumentach, nie o wierszach. > db.prac.find({pNazwisko:"KROL"}); { "_id" : ObjectId("4ec3ec2b35832cb410000016"), "pId" : 7839, "pNazwisko" : "KROL", "stanowisko" : ´"PRE", "datazatrudnienia" : "17-NOV-81", "pensja" : 5000, "dzialId" : 10 } >
Powyższe polecenie przeszukało kolekcję w poszukiwaniu dokumentu mającego wartość KROL dla atrybutu pNazwisko — MongoDB zwróciło dokument z takim atrybutem. Zwróć uwagę na atrybut _id w wyniku. Nie był on ujęty w tablicy $PRAC. Jest to identyfikator przypisany obiektowi przez MongoDB; identyfikator ten będzie unikalny w obrębie całej instancji, nie tylko w obrębie bazy. Może on być wykorzystany do wyszukania konkretnego dokumentu: > db.prac.find({"_id":ObjectId("4ec3ec2b35832cb41000001b")}); { "_id" : ObjectId("4ed4dbf97aca6ab005000008"), "pId" : 7839, "pNazwisko" : "KROL", "stanowisko" : ´"PREZES", "datazatrudnienia" : "17-NOV-81", "pensja" : 5000,"dzialId" : 10 } >
Kolekcja ma teraz unikalny indeks. Gdybyśmy spróbowali jeszcze raz uruchomić skrypt z listingu 7.1, wynik byłby następujący: Exception: E11000 duplicate key error index: test.prac.$pId_1 dup key: { : 7369 }
Gdyby podczas wstawiania dokumentu nie został ustawiony argument safe, wyjątek by nie wystąpił. Nie jest to jednak dobry pomysł, jeżeli wstawiamy dokumenty do już istniejącej kolekcji, dla której jest ustawiony indeks unikalny. Dodatkowo oznaczenie wstawiania jako safe spowoduje, że każde wstawienie będzie oczekiwało do momentu, kiedy poprzednie zostanie fizycznie zapisane na dysku. Innymi słowy, nasz skrypt spowodowałby wywołanie przynajmniej jednej operacji wejścia-wyjścia dla każdego dokumentu, co może być niedopuszczalne z punktu widzenia wydajności, jeżeli wstawiane są duże ilości danych. MongoDB często jest wykorzystywane dla hurtowni danych, do których wstawiane są nawet dziesiątki milionów dokumentów. W takim przypadku użycie safe może nie być dobrym pomysłem. Często stosowanym sposobem jest wstawienie tylko ostatniego dokumentu przy użyciu safe, takie podejście znacząco poprawia wydajność. Argument ten może także zostać wykorzystany do określenia liczby serwerów subskrybentów, które muszą otrzymać informację, zanim będzie uznana za zapisaną; replikacja i instalacja na klastrach jednak nie będą omawiane w tej książce.
Zapytania w MongoDB Utwórzmy teraz kilka zapytań. Listing 7.2 przedstawia pierwszy i zarazem najprostszy przykład. Jak już wcześniej wspomniano, MongoDB jest bazą NoSQL, w związku z tym składnia zapytań będzie nieznana dla osób, które nie miały wcześniej styczności z takimi bazami. Listing 7.2. Prosty przykład zapytania w MongoDB selectDB($nazwaBazy); $kolekcja=$polaczenie->selectCollection($nazwaBazy,$nazwaKolekcji); $kursor = $kolekcja->find(array("dzialId"=>20)); $kursor->sort(array("pensja"=>1)); foreach($kursor as $c) { foreach($c as $klucz => $wartosc) { if ($klucz != "_id") { print "$wartosc\t"; } } print "\n"; } }
Powyższy przykład pokazuje działanie obiektu cursor zwróconego przez metodę find. Jest on obiektem iteracyjnym (implementuje interfejs Iterator), reprezentującym wyniki zapytania, które mogą zostać użyte w pętli foreach podobnie jak tablice. Elementy tej pseudotablicy są dokumentami zwróconymi przez zapytanie. Każdy dokument jest tablicą asocjacyjną, wykorzystywaną przez PHP do ich reprezentowania. Po wykonaniu skryptu otrzymamy wynik: 7369 7876 7566 7788 7902
KOWALSKI DORUCH DOBROWOLSKI KACZMAREK KOWALEWSKI
SPRZEDAWCA SPRZEDAWCA MENEDZER ANALITYK ANALITYK
7902 7788 7839 7566 7566
17-DEC-80 23-MAY-87 02-APR-81 19-APR-87 03-DEC-81
800 1100 2975 3000 3000
20 20 20 20 20
W wyniku zapytania zwróceni zostali pracownicy z działu nr 20 — był to warunek zapytania. Dokumenty zostały następnie posortowane według wysokości pensji (atrybut pensja). Zapytanie nie jest wykonywane aż do momentu wywołania pętli foreach. Aby uzyskać wszystkie dokumenty, należałoby skorzystać z polecenia find() bez podawania argumentów. Było to bardzo proste zapytanie, pobierające wszystkie dokumenty mające wartość atrybutu dzialId równą 20. MongoDB potrafi o wiele więcej. Zapytania mogą opuszczać podaną liczbę dokumentów oraz ograniczać liczbę zwracanych wyników. Osoby zaznajomione z bazami danych open source znajdą podobieństwo między słowami kluczowymi limit i offset w bazach MySQL i PostgreSQL. Przykład składni takiego zapytania byłby następujący: $kursor = $kolekcja->find()->skip(3)->limit(5);
Gdybyśmy powyższe zapytanie wstawili do skryptu z listingu 7.2, zamiast linii wyszczególniającej kryteria wyszukiwania otrzymalibyśmy taki wynik: 7521 7654 7934 7844 7499
Pierwsze trzy dokumenty zostały pominięte — wyświetliło się tylko pięć dokumentów. Jak do tej pory, widzieliśmy tylko prosty warunek równości. Kolejne zapytanie zwróci tylko te dokumenty, których wartość atrybutu pensja jest większa niż 2900: $kursor = $kolekcja->find(array("pensja"=> array('$gt'=>2900)));
Zauważ, że wyrażenie $gt w zapytaniu MongoDB posiada operatory $lt, $gt, $lte, $gte i $ne, któr e oznaczają odpowiednio: „mniejszy niż”, „większy niż”, „mniejszy lub równy”, „większy lub równy”, „różny”. Składnia dla operatorów jest prosta — tablica asocjacyjna z argumentem wstawiana jest w miejsce wartości, zupełnie jak w przykładzie powyżej. Dokumenty w kursorze mogą być policzone przy wykorzystaniu funkcji count(): printf("Zwróconych zostało %d dokumentów.\n",$kursor->count());
Należy zauważyć, że opcje limit i skip nie zmienią wyniku funkcji count(). Innymi słowy, w linii $cursor = $coll->find()->skip(3)->limit(5) liczba wierszy nadal będzie wynosiła 14. MongoDB potrafi także wykonywać zapytania typu in. Następujące zapytanie zwróci wszystkie dokumenty mające wartość dzialId równą 10 lub 20: $kursor = $kolekcja->find(array("dzialId"=> array('$in'=>array(10,20)))); Oczywiście, składnia dla operatora $nin (odpowiednik not in) jest identyczna. Możliwe jest także wykonywanie zapytań typu exists. Następny przykład wybierze tylko te dokumenty, które mają ustawiony atrybut prowizja.
Odwrotność tego zapisu, znajdująca się poniżej, zwróci wyłącznie dokumenty bez tego atrybutu: $kursor = $kolekcja->find(array("prowizja"=> array('$exists'=>false)));
W zapytaniach MongoDB można stosować wyrażenia regularne. Zapytanie z listingu 7.3 zwróci wyłącznie pracowników zatrudnionych w grudniu (December). Listing 7.3. Wykorzystanie wyrażeń regularnych w zapytaniach MongoDB selectDB($nazwaBazy); $kolekcja=$polaczenie->selectCollection($nazwaBazy,$nazwaKolekcji); $kursor = $kolekcja->find(array("datazatrudnienia"=>new MongoRegex("/\d{2}-dec-\d{2}/i"))); $kursor->sort(array("dzialId"=>1,"pensja"=>1)); $kursor->sort(array("pensja"=>1)); foreach($kursor as $k) { foreach($k as $klucz => $wartosc) { if ($klucz != "_id") { print "$wartosc\t"; } } print "\n"; } printf("Zwróconych zostało %d dokumentów.\n",$kursor->count()); } catch(MongoException $e) { print "Wyjątek:\n"; die($e->getMessage()."\n"); } ?>
Wyrażenie /\d{2}-dec-\d{2}/i ma dokładnie taką samą składnię jak wyrażenia preg w PHP. To konkretne wyrażenie można odczytać następująco: dwie liczby oznaczające dzień miesiąca (\d{2}), następnie ciąg znaków -dec-, a po nim dwie liczby symbolizujące rok. Symbol /i na końcu wyrażenia oznacza, że wyrażenie ma nie sprawdzać wielkości liter — dopasuje ono zarówno dec, jak i DEC. Wykonanie skryptu zwróci następujące wyniki: 7369 KOWALSKI SPRZEDAWCA 7900 MIREK SPRZEDAWCA 7902 KOWALEWSKI ANALITYK Zwróconych zostało 3 dokumentów.
7902 7698 7566
17-DEC-80 03-DEC-81 03-DEC-81
800 950 3000
20 30 20
Możliwe jest przeprowadzenie operacji odwrotnej, aby dopasować wszystko, co nie pasuje do wyrażenia regularnego. Prezentuje to poniższy fragment kodu: $kursor = $kolekcja->find(array("datazatrudnienia"=> array('$not' => new MongoRegex("/\d{2}-dec-\d{2}/i"))));
Wykorzystujemy typ MongoRegex, aby baza miała informację, że podany ciąg jest wyrażeniem regularnym. Klasy typów danych były już wcześniej wspomniane. Typ MongoRegex jest jednym z nich. Klasa MongoDate zostanie zademonstrowana, kiedy przejdziemy do aktualizacji dokumentów. MongoDB ma jeszcze operator $where, pozwalający zastosować składnię JavaScriptu podczas wyszukiwania: $kursor = $kolekcja->find(array('$where'=> 'this.dzialId >= 10 & this.dzialId<=20'));
128
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Słowo kluczowe this w powyższym przykładzie jest z grubsza analogiczne do $this w PHP. Wskazuje ono na aktualną instancję klasy. JavaScript i PHP są zorientowane obiektowo; ich składnia jest podobna. Do tej pory koncentrowaliśmy się na sposobach lokalizowania interesujących nas dokumentów. Możemy także zdefiniować, które argumenty (nazywane czasami polami) zostaną zwrócone w wyniku zapytania. W skrypcie zaprezentowanym na listingu 7.4 możemy nareszcie zrezygnować z uciążliwego sprawdzania, czy pole nie jest polem id. W tym skrypcie nie ma już potrzeby sprawdzania tego. Listing 7.4. Określanie, które atrybuty zostaną zwrócone w wyniku zapytania selectDB($nazwaBazy); $kolekcja=$polaczenie->selectCollection($nazwaBazy,$nazwaKolekcji); $kursor = $kolekcja->find(array('$where'=> 'this.dzialId >= 10 & this.dzialId<=20')); $kursor->sort(array("dzialId"=>1,"pensja"=>1)); $kursor->fields(array("pNazwisko"=>true, "stanowisko"=>true, "dzialId"=>true, "datazatrudnienia"=>true, "pensja"=>true, "_id"=>false)); foreach($kursor as $k) { foreach($k as $klucz => $wartosc) { print "$wartosc\t"; } print "\n"; } printf("Zwróconych zostało %d dokumentów.\n",$kursor->count()); } catch(MongoException $e) { print "Wyjątek:\n"; die($e->getMessage()."\n"); } ?>
W aktualnej wersji MongoDB nie jest możliwe mieszanie pól, które mają być umieszczone w wyniku, i tych, które mają być wykluczone, z wyjątkiem pola id. Jeżeli obiekt id nie zostanie jawnie wykluczony, zawsze pojawi się w wyniku. Dzięki temu nieelegancki zapis if ($key != "_id") nie jest już potrzebny. Oto wynik działania skryptu: KOREK SPRZEDAWCA 23-JAN-82 KOS MENEDZER 09-JUN-81 KROL PREZES 17-NOV-81 KOWALSKI SPRZEDAWCA 17-DEC-80 DORUCH SPRZEDAWCA 23-MAY-87 DOBROWOLSKI MENEDZER 02-APR-81 KACZMAREK ANALITYK 19-APR-87 KOWALEWSKI ANALITYK 03-DEC-81 Zwróconych zostało 8 dokumentów.
1300 2450 5000 800 1100 2975 3000 3000
10 10 10 20 20 20 20 20
129
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Modyfikowanie dokumentów w MongoDB W tej części rozdziału pokażemy, w jaki sposób modyfikować dokumenty w MongoDB. Składnia jest banalnie prosta, więc wspomnimy także o niektórych kwestiach projektowych dotyczących hurtowni danych. Nasza mała kolekcja dobrze nam służyła, ma jednak kilka niedociągnięć. Po pierwsze, atrybut datazatrudnienia przechowywany jest jako tekst, przez co sortowanie dokumentów według daty jest prawie niemożliwe. Po drugie, tworzenie złączeń w MongoDB nie jest możliwe, dlatego musimy załączyć informacje dotyczące działu do naszej kolekcji. Identyfikator działu jest o wiele mniej czytelny niż jego nazwa i lokalizacja. MongoDB nie jest bazą relacyjną, musimy więc „zdenormalizować” nasze dane. W świecie relacyjnym nasz model wyglądałby tak jak na rysunku 7.1.
Rysunek 7.1. Projekt kolekcji dla MongoDB Te dwie tabele powinny być w gruncie rzeczy łatwo rozpoznawalne dla każdego, kto kiedykolwiek uczestniczył w kursie Oracle. Ponieważ MongoDB nie daje możliwości tworzenia złączeń, najlepsze, co możemy zrobić, to wstawienie wszystkich informacji z obu tabel rozpisanych na rysunku 7.1 do jednej kolekcji. Takie działanie to denormalizacja — jest ono bardzo powszechne w hurtowniach danych budowanych w oparciu o różne silniki bazodanowe, nie tylko w MongoDB. Dobrą wiadomością jest to, że MongoDB nie wymaga żadnych skomplikowanych modyfikacji tabel. Jedyne, co musimy zrobić, to zmodyfikować już istniejące dokumenty. Skrypt 7.5 pokazuje sposób, w jaki możemy to zrobić. Listing 7.5. Skrypt aktualizujący dokumenty selectDB($nazwaBazy); $kolekcja=$polaczenie->selectCollection($nazwaBazy,$nazwaKolekcji); $kursor = $kolekcja->find(); foreach($kursor as $k) { switch($k["dzialId"]) { case 10: $k["dNazwa"]="KSIEGOWOSC"; $k["lokalizacja"]="WARSZAWA"; break; case 20: $k["dNazwa"]="ANALIZY"; $k["lokalizacja"]="OPOLE"; break;
Trzeba przede wszystkim zauważyć, że metoda update() należy do klasy collection, a nie cursor. Klasa cursor wykorzystywana była tylko do poruszania się po kolekcji wyników oraz do przygotowania danych do aktualizacji. Metoda update przyjmuje następujące parametry: kryteria służące do zlokalizowania dokumentów, które mają być zmodyfikowane, dokument, który będzie wpisany w ich miejsce, oraz tablice opcji. Metoda update także wspiera opcję safe, podobnie jak metoda insert. Gdyby został wykonany skrypt z listingu 7.2, w wyniku otrzymalibyśmy nieczytelne, duże liczby zamiast atrybutu datazatrudnienia. MongoDB przechowuje
Można zauważyć, że pole datazatrudnienia ma wszystkie cechy poprawnej daty. Musimy ją tylko poprawnie sformatować i nasz skrypt będzie idealny. Opis klasy MongoDate na stronie www.php.net pokazuje, że klasa ma dwie publiczne właściwości: sec, reprezentujący liczbę sekund od rozpoczęcia epoki, i usec, reprezentujący liczbę milisekund od rozpoczęcia epoki. Możemy teraz wykorzystać wbudowaną metodę strftime, aby poprawnie sformatować datę: foreach($c as $key => $val) { if ($val instanceof MongoDate) { printf("%s\t",strftime("%m/%d/%Y",$val->sec)); } else { print "$val\t"; } }
Po tej modyfikacji skrypt z listingu 7.4 zwróci czytelny wynik: KOREK KOS KROL KOWALSKI DORUCH
Teraz, kiedy atrybut datazatrudnienia przechowywany jest jako poprawna data, możemy według niego sortować wyniki. Dodatkowo kolekcja prac przechowuje teraz informacje o dziale, w którym zatrudniony jest pracownik, co jest użyteczniejsze niż sam numer. Właśnie wykonaliśmy pierwszy krok w kierunku budowy poprawnej hurtowni danych.
Agregacje w MongoDB Hurtownie danych wykorzystywane są do różnego rodzaju wyznaczania trendów i agregacji. Przejrzeliśmy metody pobierania danych, jednak do tej pory nic nie reprezentowało funkcjonalności group by, sum ani żadnych innych funkcji grupujących dostępnych w bazach relacyjnych. Ciągle porównujemy MongoDB do baz relacyjnych, ponieważ jest on debiutantem na tym polu; jest bazą danych, której celem jest łatwiejsze tworzenie hurtowni danych. Bazy relacyjne były używane jako hurtownie danych na długo przed pojawieniem się MongoDB, porównywanie ich jest więc usprawiedliwione. Jednym z zadań, które musiałaby wykonać relacyjna baza danych, jest obliczenie sumy pensji dla każdego z departamentów. MongoDB nie jest bazą relacyjną, więc tradycyjne select dzialId,sum(pensja) from prac group by dzialId nie jest odpowiedzią. Aby osiągnąć ten cel, MongoDB wykorzystuje framework mapreduce. Framework najpierw rozdziela zadanie pomiędzy „pracowników” (faza „map”, czyli „dziel”), a następnie łączy wyniki działania poszczególnych pracowników w ostateczny wynik (faza „reduce”, czyli „rządź”). MongoDB przekazuje funkcje JavaScriptu do procesów „pracowników”. To podejście daje większe możliwości niż funkcje grupujące takie jak SUM lub COUNT. Jego wadą jest konieczność znajomości JavaScriptu w celu skorzystania z możliwości frameworka mapreduce. Język JavaScript nie jest przedmiotem opisu w tej książce, więc pokazane zostaną tylko najprostsze przykłady emulujące działanie funkcji SUM, COUNT i AVG. Jest jeszcze jedno bardzo ważne ograniczenie MongoDB. Jak do tej pory, wszystkie dostępne silniki JavaScriptu są jednowątkowe, co oznacza, że aby skorzystać z wątków równoległych, konieczne jest skonfigurowanie mechanizmu sharding, który jest odpowiednikiem partycjonowania w bazie danych rozbitej na wiele węzłów w klastrze. To ograniczenie prawdopodobnie zostanie wyeliminowane w nowych wersjach. Kolejny skrypt zwróci sumy pensji zawarte w parametrze pensja, w kolekcji prac, wraz z liczbą pracowników w dziale i średnią pensją. Skrypt wykorzystuje metodę group, która należy do klasy collection. Na listingu 7.6 przedstawiono skrypt. Listing 7.6. Skrypt zwracający sumę pensji, liczbę pracowników w dziale i średnią pensję selectDB($nazwaBazy); $kolekcja = $polaczenie->selectCollection($nazwaBazy, $nazwaKolekcji); $klucze = array("dzialId" => 1); $wartosciStartowe = array('suma' => 0, 'ile' => 0); $rzadz = new MongoCode('function(obj,prev) { prev.suma += obj.pensja; prev.ile++; }'); $zakoncz= new MongoCode('function(obj) { obj.śr = obj.suma/obj.ile; }'); $grupowanie = $kolekcja->group($klucze, $wartosciStartowe, $rzadz, array('finalize'=>$zakoncz)); foreach ($grupowanie['retval'] as $grp) { foreach ($grp as $klucz => $wartosc) {
Algorytm mapreduce jest algorytmem rekurencyjnym. Funkcja reduce przyjmuje dwa argumenty: aktualnie przetwarzany obiekt i poprzednią wartość obiektu z właściwościami zdefiniowanymi w zmiennej initial. MongoDB iteruje po wynikach i rekurencyjnie wylicza sumę i liczbę elementów. Kiedy proces się zakończy, wywołana zostanie funkcja finalize. Argumentem funkcji jest wynikowy obiekt, zawierający właściwości dzialId, count i sum. Funkcja finalize doda właściwość avg. Wynik działania skryptu wygląda następująco: dzialId => 20 dzialId => 30 dzialId => 10
Wynik będzie przechowywany w zmiennej $group_by, która jest tablicą asocjacyjną zawierającą nie tylko wynik operacji, ale także informacje o liczbie grup, liczbę przetworzonych w procesie wyliczania dokumentów oraz końcowy status operacji. Strukturę zmiennej wynikowej możemy zobaczyć, stosując funkcję print_r — najczęściej używaną funkcję w procesie debugowania. Funkcja print_r zwraca strukturę zmiennej na urządzenie wyjściowe. W przypadku skryptu z listingu 7.6 wynik będzie następujący: Array ( [retval] => Array ( [0] => Array ( [dzialId] => 20 [suma] => 10875 [ile] => 5 [śr] => 2175 ) [1] => Array ( [dzialId] => 30 [suma] => 9400 [ile] => 6 [śr] => 1566.6666666667 ) [2] => Array ( [dzialId] => 10 [suma] => 8750 [ile] => 3 [śr] => 2916.6666666667 ) ) [count] => 14 [keys] => 3 [ok] => 1 )
133
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Obiekt retval zawiera zwrócone wartości, obiekt count będzie zawierał liczbę przetworzonych dokumentów, natomiast obiekt keys — liczbę grup w zestawie danych. Obiekt ok jest końcowym statusem operacji; gdyby coś poszło nie tak, miałby wartość 0. W skrypcie zastosowaliśmy klasę MongoDate z przykładu aktualizowania danych i klasę MongoCode, podobną do klasy MongoRegex wykorzystanej w części dotyczącej zapytań z użyciem wyrażeń regularnych. JavaScript jest wszechstronnym językiem obiektowym, który może być wykorzystany do tworzenia agregatów o wiele bardziej złożonych niż suma czy średnia. Framework mapreduce dostępny jest pod adresem https://github.com/infynyxx/MongoDB-MapReduce-PHP. Dalsze omawianie frameworka i agregatów JavaScriptu wymagałoby odwoływania się do wiadomości dotyczących JavaScriptu i w związku z tym wykracza poza zakres tej książki.
Podsumowanie MongoDB MongoDB to relatywnie nowa baza danych i najpopularniejsza baza NoSQL. Jest świetnym narzędziem do budowy hurtowni danych, przede wszystkim ze względu na łatwość obsługi klastrów. Jest bazą danych open source, co czyni ją idealną do budowy wysoko wydajnych hurtowni danych. Jest także dobrze udokumentowana, łatwa w instalacji, integracji z PHP i w testowaniu. Jej nowe wersje ukazują się praktycznie każdego dnia. Dzisiaj transakcyjne bazy danych nadal królują — z wielu powodów. Jednym z nich jest dostępność języka manipulowania danymi — SQL — podczas gdy bazy NoSQL nie zostały jeszcze ustandaryzowane. Kolejną bazą, którą omówimy, będzie CouchDB, projekt fundacji Apache, podobny w swej naturze do MongoDB.
Wprowadzenie do CouchDB CouchDB jest projektem open source prowadzonym przez fundację Apache. Jest bazą NoSQL z kontrolą spójności wielu wersji (MVCC). MVCC to mechanizm pozwalający na przechowywanie w bazie wielu wersji tego samego dokumentu. Instalacja CouchDB jest bardzo prosta; istnieją pakiety instalacyjne dla wszystkich większych systemów operacyjnych. Dostępny jest instalator dla systemu Windows, są też pakiety dla różnych dystrybucji Linuksa oraz Uniksa. Instalacja we wszystkich systemach jest bezproblemowa, chociaż CouchDB jest przede wszystkim bazą linuksową. Zarówno MongoDB, jak i CouchDB są pozbawione schematów, ale CouchDB jest w tym bardziej konsekwentny. CouchDB nie ma żadnych encji takich jak kolekcje. Cała baza jest jedną, amorficzną kolekcją dokumentów. Aby ułatwić organizację bazy, CouchDB wykorzystuje definiowane przez użytkownika widoki, tworzone jako funkcje JavaScriptu używające frameworka mapreduce firmy Google do organizowania dokumentów. Identycznie jak w przypadku MongoDB, dokumenty są obiektami JSON. Sterownik MongoDB zajmuje się konwersją tablic asocjacyjnych PHP z i do obiektów JSON; CouchDB tego nie robi. Komunikuje się ze światem zewnętrznym poprzez protokół HTTP — pobiera i zwraca obiekty JSON. Aby ułatwić komunikację z CouchDB, warto zainstalować rozszerzenie PHP JSON, wykorzystując narzędzie PECL. Rozszerzenie udostępnia funkcje json_encode i json_decode, które pozwalają konwertować tablice asocjacyjne PHP z i do obiektów JSON. Dzięki takiej architekturze biblioteki PHP dla CouchDB nie wymagają linkowania tak jak rozszerzenia PHP dla MongoDB. Najpopularniejszą biblioteką dla CouchDB jest PHP-on-Couch, którą można pobrać pod adresem https://github.com/dready92/PHP-on-Couch. Biblioteka nie wymaga instalacji. Można ją zapisać w dowolnym miejscu i dołączyć do skryptu za pomocą poleceń include lub require. Takie uproszczenia są możliwe właśnie dzięki temu, że CouchDB komunikuje się za pośrednictwem protokołu HTTP. Dla systemów Linux są dostępne narzędzia wiersza poleceń do komunikacji z serwerami HTTP. Najpopularniejszym z nich jest curl — bardzo użyteczny podczas pracy z CouchDB. Pierwsze polecenie, wyświetlające ekran powitalny i sprawdzające czy CouchDB jest aktywny, wygląda następująco: curl http://localhost:5984 {"couchdb":"Welcome","version":"1.0.1"}
134
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Aplikacja curl skontaktowała się z serwerem HTTP pod adresem localhost (IP 127.0.0.1, port 5984); serwer zwrócił obiekt JSON. Sparsujmy powyższy obiekt przy wykorzystaniu tego skryptu:
Wynik będzie następujący: Array ( [couchdb] => Welcome [version] => 1.0.1 )
Funkcja json_decode przekonwertowała obiekt JSON zwrócony przez bazę do tablicy asocjacyjnej PHP.
Wykorzystanie interfejsu Futon CouchDB może przyjmować polecenia HTTP, możliwe jest więc utworzenie bazy danych za pomocą polecenia curl –X PUT http://localhost:5984/dbname. O wiele wygodniejsze jest jednak wykorzystanie interfejsu administracyjnego dla CouchDB — Futonu. Dostęp do niego możesz uzyskać przy użyciu swojej ulubionej przeglądarki, pod adresem http://localhost:5984/_utils. Jeżeli serwer nie jest zainstalowany lokalnie, należy wpisać odpowiedni adres i numer portu. Numer portu można skonfigurować. W przypadku Opery wynik będzie taki jak na rysunku 7.2.
Rysunek 7.2. Futon pomoże Ci podczas tworzenia baz danych i kolekcji
135
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Tworzenie baz jest banalnie proste. W lewym górnym rogu znajduje się przycisk Create Database (utwórz bazę). Kliknij, wpisz test jako nazwę bazy i prześlij go do bazy. Voilà! Baza o nazwie test została utworzona (rysunek 7.3).
Rysunek 7.3. Baza danych o nazwie test Futon może być pomocny podczas tworzenia widoków. Widoki są utworzonymi przez użytkownika funkcjami JavaScriptu implementującymi protokół mapreduce firmy Google. Kiedy widok jest wykorzystywany po raz pierwszy, jest wyliczany dla każdego z dokumentów, a rezultaty przechowywane są w indeksie w formie b-drzewa. Ma to miejsce tylko za pierwszym razem, kiedy widok jest tworzony. Później funkcja wywoływana jest tylko dla dodawanych i modyfikowanych dokumentów. Aby utworzyć widok, musimy najpierw utworzyć kilka dokumentów. Czas na pierwszy skrypt wykorzystujący CouchDB, generujący taką samą strukturę prac jak w przypadku MongoDB (listing 7.7). Listing 7.7. Skrypt PHP wykorzystujący CouchDB 7369, "pNazwisko"=>"KOWALSKI", "stanowisko" => "SPRZEDAWCA", "men" => 7902, ´"datazatrudnienia" => "17-DEC-80", "pensja" => 800, "dzialId" => 20,"_id" => "7369"), array("pId"=>7499, "pNazwisko"=>"NOWAK", "stanowisko" => "SPRZEDAWCA", "men" => 7698,
Klasa couchClient, wykonująca połączenie, oraz klasa couchDocument zostały zdefiniowane w plikach pochodzących z katalogu PHP-on-Couch, załączonych na początku skryptu. Nazwa folderu jest dowolna, ponieważ biblioteka nie jest instalowana. W tym przypadku katalog nazwano PHP-on-Couch i umieszczono go w miejscu wskazanym w parametrze include_path. Parametr include_path jest parametrem dla interpretera PHP — z reguły wyszczególniony jest w pliku php.ini. Z wyjątkiem załączonych plików i procedury ładowania danych skrypt jest prawie identyczny z tym dotyczącym MongoDB (listing 7.1). Główną różnicą jest duplikacja wartości parametru pId w parametrze _id typu string. CouchDB pozwala nam na przypisywanie naszych własnych wartości jako identyfikatorów dokumentów. Identyfikator musi być unikalny i nie może być typu liczbowego — musi być typu string. Dlatego oryginalny parametr pId nie został po prostu zastąpiony parametrem _id. Jeżeli spojrzymy na interfejs Futon, zobaczymy nasze nowo dodane dokumenty (rysunek 7.4). CouchDB komunikuje się poprzez protokół HTTP, co oznacza, że każdy z rekordów będzie widoczny w przeglądarce. Możemy to zaobserwować, po prostu klikając którykolwiek z dokumentów (rysunek 7.5).
137
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Rysunek 7.4. Nowo wstawione dokumenty widoczne w interfejsie Futon Warto także wspomnieć o polu zawierającym numer wersji, oznaczonym jako _rev. Pokazywana jest tylko ostatnia wersja, jednak możliwe jest zobaczenie wszystkich wersji. Jak już wcześniej wspomniano, CouchDB zawiera system zarządzania wersjami i jest w pełni kompatybilny z założeniami ACID. Powiedziano również, że CouchDB nie ma mechanizmu zapytań ad hoc. To oznacza, że aby pobrać dokument, należy odpytać bazę na podstawie pola id. Listing 7.8 przedstawia skrypt pobierający i aktualizujący jeden dokument. Listing 7.8. Skrypt pobierający i aktualizujący jeden dokument pensja=1500; $dokument->record(); } catch(Exception $e) { printf("Kod wyjątku:%d\n",$e->getCode()); printf("%s\n",$e->getMessage()); exit(-1); } ?>
138
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Rysunek 7.5. Każdy dokument jest widoczny w przeglądarce Ten skrypt pobierze dokument o identyfikatorze 7844, ustawi atrybut pensja na wartość 1500 i zapisze ten dokument z powrotem w bazie. Klasa nie jest idealna do zapytań o dokument — wykorzystuje metodę getInstance klasy statycznej, wywoływaną w jej kontekście. Oznacza to, że funkcja nie jest wywoływana jako część obiektu; nie ma kontekstu obiektu, w którym mogłaby zostać wywołana. Dokument używa funkcji _get i _set dokumentu do ustawienia jego właściwości. Jeżeli sprawdzisz dokument w interfejsie Futon, zobaczysz, że jego wersja uległa zmianie. Niestety, nie ma możliwości odpytywania bazy według innych kluczy. Aby odpytać bazę CouchDB, należy utworzyć widok. Widoki są tworzone jako funkcje mapreduce w JavaScripcie. Podczas tworzenia widoku funkcja jest wyliczana dla każdego z dokumentów, a rezultaty są przechowywane w indeksie w formie b-drzewa. Dla każdego dodawanego lub modyfikowanego dokumentu indeks jest zmieniany. Widoki tworzymy przy wykorzystaniu interfejsu Futon. W prawym górnym rogu interfejsu jest lista rozwijana o nazwie View, wybrana wartość to All documents (wszystkie dokumenty). Jeżeli zmienimy wybraną wartość na Temporary view… (widok tymczasowy), pojawi się formularz tworzenia widoku. Zagłębianie się w meandry tworzenia i implementacji widoków wykracza poza zakres tej książki. Szczegóły zostały dobrze opisane w świetnej książce Joego Lennona pod tytułem Beginning CouchDB. Wprowadziliśmy w formularzu następujący skrypt, aby utworzyć widok o nazwie dzialId30, przechowywany w dokumencie o nazwie pensja. Widoki także są dokumentami i są przechowywane w specjalnej bazie o nazwie _design. Nasz widok wygląda następująco: function(dokument) { if (dokument.dzialId==30) { emit(dokument._id, { pId:dokument.pId, pNazwisko: dokument.pNazwisko, stanowisko: dokument.stanowisko,
139
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
men: dokument.men, pensja: dokument.pensja}); } }
Widok pobierze tylko dokumenty mające wartość parametru dzialId równą 30. Funkcja zwraca dwa obiekty: klucz i przypisany do niego dokument JSON. Jeżeli klucz będzie miał wartość NULL, baza przypisze go automatycznie. Funkcja będzie wykonana dla każdego dokumentu w bazie i jeżeli parametr dzialId będzie miał wartość 30, parametry pId, pNazwisko, stanowisko, men i pensja zostaną zwrócone w formie obiektu JSON. Widok zostanie zapisany w dokumencie z id="pensja", o nazwie dzialId30. Teraz, kiedy mamy strukturę bazy, którą możemy odpytać, skrypt jest trywialny — pokazano go na listingu 7.9. Listing 7.9. Skrypt wykorzystujący widok asArray()->getView('pensja','dzialId30'); foreach ($dzialId30['rows'] as $wiersz) { foreach ($wiersz['value'] as $klucz => $wartosc) { printf("%s = %s\t",$klucz,$wartosc); } print "\n"; } } catch(Exception $e) { printf("Kod wyjątku:%d\n",$e->getCode()); printf("%s\n",$e->getMessage()); exit(-1); } ?>
Skrypt wywołuje metodę getView klasy couchClient, aby odpytać bazę danych. Wynik zwracany jest jako tablica. Dostępnych jest wiele opcji, które mogą być wykorzystane — ograniczenie liczby wyników, ograniczenie dozwolonych kluczy, sortowanie itp. Dokumentacja jest raczej skąpa, więc najlepszym sposobem na poznanie funkcjonalności jest analiza kodu klasy. Po uruchomieniu skryptu otrzymamy wynik: pId pId pId pId pId pId
Podsumowanie CouchDB CouchDB jest potężną bazą, chociaż brak możliwości wykonywania zapytań ad hoc poważnie go ogranicza. Niemniej jednak jest to baza bardzo popularna i dobrze udokumentowana. Interfejsy PHP są łatwe w użyciu, lecz nie są konieczne — CouchDB można okiełznać poprzez zastosowanie protokołu HTTP i narzędzi wiersza poleceń, takich jak curl. Wykorzystanie pakietów PEAR HTTP_Request lub HTTP_Request2 i rozszerzenia JSON jest wystarczające do komunikacji z CouchDB.
140
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Kolejna baza danych, którą omówimy, należy do kategorii baz SQL. Nie jest pełnoprawnym systemem RDBMS, implementuje jednak znaczną część standardu SQL 92.
Wprowadzenie do SQLite SQLite jest bazą danych typu SQL, cała jej zawartość mieści się w jednym pliku. Baza ta przeznaczona jest dla systemów wbudowanych. Wykorzystywana jest przez przeglądarkę Firefox, klienta poczty Thunderbird oraz wiele innych aplikacji działających na różnych urządzeniach, od telefonów komórkowych po systemy typu mainframe. SQLite jest bazą relacyjną, co oznacza, że implementuje język SQL; jest ponadto systemem typu open source (http://sqlite.org/). Bazy relacyjne w przeciwieństwie do baz NoSQL mają raczej rygorystyczny model danych. Jest on kolekcją połączonych ze sobą obiektów, głównie tabel i widoków. Podstawową jednostką modelu relacyjnego jest tabela. Tabele modelowane są zgodnie z tradycyjnym układem — mają ustaloną strukturę z kolumnami, zwykle nazywaną atrybutami i wierszami. Każdy wiersz może zawierać jedynie takie kolumny, które zostały zdefiniowane dla tabeli, i żadnych innych — w przeciwieństwie do baz NoSQL, które pozbawione są struktury. Jeżeli w wierszu nie ma wartości dla kolumny, wartość dla tej kolumny jest ustawiana na NULL, tj. sztuczną wartość posiadającą pewne nietypowe właściwości. NULL jest czarną dziurą teorii relacyjnej. Nic nigdy nie jest równe NULL. Aby sprawdzić, czy wartość jest równa NULL, należy skorzystać z operatora IS [NOT] NULL. Ponadto wartość NULL modyfikuje logikę w systemach RDBMS. Logiczne porównanie z wartością NULL zawsze zwraca NULL, które jest trzecią wartością mogącą być wynikiem operacji logicznych, poza wartościami prawda i fałsz. Bazy relacyjne nie wykorzystują logiki binarnej — wykorzystują logikę trynarną, z trzema dozwolonymi wynikami ewaluacji wyrażeń. NULL nie jest jednak tak naprawdę wartością; jest brakiem wartości. NULL jest także jednym z typów danych. SQLite3 wspiera następujące typy: • • • • •
NULL Integer Real Text Blob
Inne bazy relacyjne wspierają też różnorodne typy dotyczące daty i czasu, takie jak DATE, TIME, INTERVAL czy TIMESTAMP, jednak SQLite jest bazą wbudowaną, a dostępne typy danych ograniczone są do wymienionych powyżej. Nieduża objętość była jednym z podstawowych założeń projektowych, wsparcie dla rozbudowanych typów daty i czasu znacznie by ją zwiększyły i właśnie dlatego zostały pominięte w ostatecznej wersji. Następny rozdział opisuje pełnoprawną relacyjną bazę danych o nazwie MySQL, która posiada rozbudowane wsparcie dla typów dotyczących dat i czasu. Ostatnia część tego rozdziału traktuje o SQLite i jego integracji z PHP. Są jeszcze dwa typy encji, o których należy wspomnieć w kontekście baz relacyjnych: widoki i więzy integralności. Widoki są przygotowanymi wcześniej w formie pakietu zapytaniami, przechowywanymi w bazie w celu późniejszego wykonania. Widoki są w gruncie rzeczy nazwanymi zapytaniami. Więzy integralności, jak sama nazwa wskazuje, są zasadami i wytycznymi, które muszą spełniać nasze dane. SQLite pozwala na deklarację więzów integralności, jednak ich nie wspiera, z wyjątkiem kluczy głównych, które są najważniejsze z punktu widzenia integralności danych. Klucz główny w tabeli jednoznacznie identyfikuje jej wiersze. Każdy wiersz musi posiadać unikalną wartość klucza głównego. Jest to analogiczne do numeru konta bankowego — każdy klient banku musi mieć nadany numer konta i różni klienci mają różne numery. Oznacza to, że klucz główny nie może przyjmować wartości NULL. Klucze główne są bardzo ważne w teorii baz relacyjnych. Puryści twierdzą, że taki klucz powinna posiadać każda tabela. Jaki jest sens dysponowania tabelą, w której nie można jednoznacznie zidentyfikować wiersza? W jaki sposób rozpoznać wtedy, czy wiersze się różnią? Są jeszcze więzy unikalne, zapewniające, że wyspecyfikowana wartość będzie unikalna. To oznacza, że wartość klucza unikalnego może przyjmować NULL — inaczej niż w przypadku klucza głównego. Są także więzy typu NOT NULL, wymuszające, aby kolumna zawsze miała wartość przy wstawianiu wiersza do tabeli.
141
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
Więzy sprawdzające są zdefiniowanymi przez użytkownika ograniczeniami wartości dla kolumny. Przykładem byłoby ograniczenie wartości kolumny jedynie do liczb dodatnich — wartości ujemne byłyby wtedy niedozwolone. Ostatnim i najbardziej złożonym typem więzów są klucze obce. Aby przybliżyć ich istotę, odwołamy się do rysunku dotyczącego aktualizacji danych w MongoDB (rysunek 7.6).
Rysunek 7.6. Projekt kolekcji dla MongoDB Mamy dwie tabele — jedna z nich opisuje pracowników, druga działy. Wymaganie, aby identyfikatory działów w tabeli pracowników były dostępne w tabeli zawierającej działy, nazywane jest kluczem obcym. Każda z wartości kolumny dzialId w tabeli prac musi być dostępna w kolumnie z kluczem głównym lub kluczem unikalnym w drugiej tabeli — w tym przypadku w dzial. Te typy encji nie są wyjątkowe w SQLite; są opisane w specyfikacji standardu SQL. Najnowsza wersja tego standardu została opublikowana w roku 2008. SQL jest żywym językiem, który niepodzielnie rządzi w świecie baz danych — implementowany jest przez większość systemów baz danych aktualnie dostępnych na rynku, także w systemach komercyjnych, takich jak Oracle, Microsoft SQL Server, IBM DB2 czy Sybase oraz w systemach typu open source, np. MySQL, PostgreSQL, SQLite czy Firebird. Bazy NoSQL są nowością i nadal szukają swojego miejsca na rynku. Chociaż ta książka jest o PHP, a nie o bazach danych i standardzie SQL, wyjaśnimy w zarysie, w jaki sposób korzystać z baz relacyjnych w PHP. Nie zakładamy, że Czytelnik zna bazy relacyjne, jednak taka wiedza na pewno ułatwi zrozumienie tego i następnego rozdziału. Wiemy, jakich obiektów możemy się spodziewać w relacyjnej bazie danych, musimy zatem powiedzieć coś na temat sposobu manipulowania tymi obiektami — jak dane są pobierane z bazy i jak są aktualizowane. Bazy relacyjne zostały zamodelowane w oparciu o podstawową teorię zbiorów. Głównym obiektem wszystkich zapytań SQL jest podzbiór. Zapytania tworzone jako polecenie SELECT pozwalają użytkownikowi na uzyskanie podzbioru co najmniej jednej tabeli. Ważne jest, abyśmy wyniki zapytań SELECT postrzegali jako podzbiory, a nie jako pojedyncze wiersze i rekordy — jak czasami są nazywane. Oprócz zapytań typu SELECT istnieją jeszcze zapytania INSERT, DELETE i UPDATE, które także operują na podzbiorach. Omówienie baz relacyjnych nie byłoby kompletne, gdybyśmy nie wspomnieli o indeksach. Indeksy nie są obiektami logicznymi jak tabele czy widoki; są czysto fizycznymi strukturami, tworzonymi przez administratora w celu przyspieszenia wykonywania zapytań. Indeksy są z reguły tworzone automatycznie w celu zaimplementowania kluczy głównych i unikalnych, jednakże nie w SQLite. W SQLite indeksy unikalne muszą być tworzone ręcznie, aby ograniczenie było przestrzegane. SQL nie jest językiem proceduralnym. Zapytania SQL definiują podzbiór, na którym będą operować, a nie sposób, w jaki ten podzbiór będą ekstrahować. Każdy system relacyjny zawiera część zwaną optymalizatorem zapytań, który determinuje metodę dostępu do obiektów potrzebnych w trakcie działania zapytania SQL. W szczególności optymalizator decyduje, które indeksy będą użyte, aby wykonać zapytanie i pobrać potrzebny podzbiór danych, oraz która metoda zostanie wykorzystana do łączenia tabel, jeżeli jest to konieczne. Oprócz optymalizatora każdy system relacyjny posiada (SQLite nie jest tu wyjątkiem) słownik danych. Słownik danych zwany jest metadanymi — danymi o danych. Opisuje on wszystkie obiekty w bazie danych i odgrywa istotną rolę w funkcjonowaniu bazy danych. SQLite jest bazą wbudowaną, opracowaną z myślą o jej niewielkich rozmiarach, więc rola słownika danych powierzona została jednemu plikowi — sqlite_master. Tabela zawiera następujące kolumny: 142
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
• name (nazwa obiektu) • type (typ obiektu) • tbl_name (nazwa tabeli, ważna dla indeksów) • rootpage (początek obiektu w pliku bazodanowym) • sql (zapytanie tworzące dany obiekt) Większość innych baz relacyjnych ma znacznie obszerniejszy słownik danych, który składa się z setek tabel. Słownik danych najlepiej zademonstrować za pośrednictwem wiersza poleceń, za pomocą programu sqlite3. Słownik jest wywoływany zwykle z nazwą bazy danych podaną jako argument: sqlite3 test.sqlite. Jeżeli baza test.sqlite nie istnieje, zostanie utworzona. Wynik będzie następujący: sqlite3 test.sqlite SQLite version 3.3.6 Enter ".help" for instructions sqlite>
Program ten ma dobrze napisaną pomoc zawierającą całkiem sporo przydatnych funkcji. Można go wykorzystać do wywoływania zapytań SQL i sprawdzania wyników bez tworzenia skryptów. SQLite jest bazą wbudowaną, co oznacza, że baza ta powinna być wykorzystywana jako komponent programów, a nie jako samodzielna baza danych zarządzana za pośrednictwem programów CLI, takich jak sqlite3. Przyjrzyjmy się zatem interfejsowi PHP dla bazy SQLite. Każdy interfejs programistyczny dla dowolnej bazy relacyjnej ma przynajmniej następujące komponenty: • Mechanizmy połączeniowe. Dla SQLite są bardzo proste — w przeciwieństwie do innych systemów, które z reguły posiadają własne protokoły i różne metody autentykacji. • Mechanizmy uruchamiania zapytań SQL. Mogą być relatywnie złożone, zależnie od opcji. Razem z mechanizmami uruchamiania SQL każdy interfejs programistyczny ma z reguły metody do przypisywania wyników do zmiennych. Przykłady przedstawimy dalej, kiedy proces przypisywania do zmiennych zostanie szczegółowo wyjaśniony. W tej kategorii mieszczą się także mechanizmy przygotowawcze, które przekształcają zapytanie SQL z formy czytelnej na obiekt zwany „uchwytem do zapytania”. • Mechanizmy opisujące zestaw wynikowy. Bazy relacyjne zwracają zestawy wynikowe, które zawierają różne kolumny, nazwy i typy danych. Zawsze dostępne jest wywołanie „opisujące”, które zwróci opis zestawu wynikowego do programu wywołującego. • Mechanizm zwracający zestaw wynikowy do programu wywołującego. Różne bazy mają różne opcje przyspieszania pobierania danych, więc ten mechanizm także nie jest tak do końca prosty. Jeżeli interfejs ma klasy, z reguły dostępne są klasy połączeniowa, klasa reprezentująca zapytanie oraz klasa zestawu wynikowego. Ze względów historycznych klasa zestawu wynikowego bywa nazywana klasą kursora — kursor ten nie jest tożsamy z kursorem udostępnianym przez język SQL. Komponenty te są oczywiście dostępne w interfejsie PHP dla SQLite, więc bez zbędnego przedłużania zobaczmy pierwszy skrypt korzystający z SQLite (listing 7.10). Skrypt utworzy strukturę bazy składającą się ze wspomnianych wcześniej tabel prac i dzial oraz jednego klucza obcego i indeksu. Listing 7.10. Przykład SQLite
143
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
CREATE TABLE prac ( pId integer NOT NULL, pNazwisko text , stanowisko text , men integer, datazatrudnienia text, pensja real, prowizja real, dzialId integer, CONSTRAINT prac_pkey PRIMARY KEY (pId), CONSTRAINT fk_dzialId FOREIGN KEY (dzialId) REFERENCES dzial (dzialId) ON DELETE CASCADE ); CREATE UNIQUE INDEX pk_prac on prac(pId); CREATE INDEX prac_dzialId on prac(dzialId); CREATE UNIQUE INDEX pk_dzial on dzial(dzialId); EOT; try { $db = new SQLite3("test.sqlite"); @$db->exec($DDL); if ($db->lastErrorCode() != 0) { throw new Exception($db->lastErrorMsg()."\n"); } print "Struktura bazy utworzona pomyślnie.\n"; } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage()); } ?>
Powyższy skrypt składa się w dużej części z poleceń SQL zawartych w zmiennej $DDL. Aktywną częścią jest blok try, w którym wykonywane jest zapytanie poprzez przekazanie go do metody exec, wykorzystywanej do uruchamiania zapytań. Ta metoda zwraca instancję klasy resultset lub cursor, z której można uzyskać informacje dotyczące liczby zwróconych kolumn, ich nazw i typów oraz same dane. Polecenia zawarte w zmiennej $DDL tworzą tabele i indeksy, nie zwracają żadnych kolumn. Skąd w takim razie wiemy, czy polecenie zakończyło się powodzeniem? Niestety, klasa SQLite3 nie zwraca wyjątków; wyjątki muszą być zgłaszane przez programistę. SQLite udostępnia natomiast metody pozwalające uzyskać ostatni kod błędu oraz jego opis, które można wykorzystać przy zgłaszaniu wyjątku. Kod operacji zakończonej powodzeniem ma wartość 0, każda inna wartość oznacza błąd. Uruchomiony i wykonany skrypt utworzy bazę test.sqlite (jeżeli taka nie istnieje) oraz strukturę zgodną z zapytaniem. Zauważ także, że wszystkie zapytania zostały zapisane w jednej zmiennej: dwa polecenia create table i trzy polecenia create index zostały wykonane razem. Ponadto indeksy unikalne zadbają, aby dwa jednakowe rekordy nie były wstawione do tabeli pomimo faktu, że SQLite nie wspiera więzów integralności. Nadal jednak możliwe będzie wstawienie wartości NULL w kolumnach z kluczem głównym. Tabele są utworzone i musimy załadować do nich dane. Wstawiane dane zostały umieszczone w dwóch plikach CSV, więc jedyne, czego potrzebujemy, to ogólny skrypt do ładowania plików CSV do tabeli. Skrypt będzie przyjmował dwa argumenty wiersza poleceń: nazwę tabeli i nazwę pliku. Taki skrypt jest doskonałym narzędziem umożliwiającym demonstrowanie wielu koncepcji z różnych systemów zarządzania relacyjnymi bazami danych (RDBMS). Pliki CSV wyglądają następująco: Prac.csv 7369,KOWALSKI,SPRZEDAWCA,7902,17-DEC-80,800,,20 7499,NOWAK,SPRZEDAWCA,7698,20-FEB-81,1600,300,30 7521,BONIEK,SPRZEDAWCA,7698,22-FEB-81,1250,500,30 7566,DOBROWOLSKI,MENEDZER,7839,02-APR-81,2975,,20
Wynik wykonania powyższego skryptu wygląda następująco: ./skrypt7.11.php prac Prac.csv Polecenie insert: insert into prac values(:1,:2,:3,:4,:5,:6,:7,:8) Do tabeli prac wstawiono 14 wierszy. ./skrypt7.11.php dzial Dzial.csv Polecenie insert: insert into dzial values(:1,:2,:3) Do tabeli dzial wstawiono 4 wierszy.
Ten skrypt jest naprawdę użyteczny, ponieważ bardzo łatwo możemy zmodyfikować go na potrzeby innych baz danych. Jak w przypadku skryptu z listingu 7.10, najważniejsza jest część zawarta w bloku try. Pierwszą rzeczą wartą zauważenia jest zapytanie na początku bloku try: $res = $db->query("select * from $nazwaTabeli");
Metoda query wykonuje zapytanie przekazane do niej jako parametr w postaci ciągu znaków i zwraca instancję klasy statement. Klasa statement jest wykorzystywana do uzyskania informacji o nazwach i typach kolumn zwracanych przez zapytanie oraz do uzyskania samych danych. Zauważ jednak, że żadne wiersze nie zostały zwrócone z bazy; rezultat zapytania został jedynie sprawdzony, aby ustalić liczbę zwróconych kolumn. Zostało to wykonane poprzez wywołanie metody numColumns klasy statement: $liczbaKolumn = $res->numColumns();
Jeżeli liczba kolumn w tabeli jest znana, polecenie insert może być utworzone, a zestaw wynikowy został zamknięty za pomocą metody finalize. Zamykanie kursorów jest dobrą praktyką, kiedy nie są już potrzebne. Zapobiega to wyciekom pamięci oraz pomyłkom. Wróćmy do tworzenia polecenia insert. Są dwie możliwe strategie: • Odrębne polecenia insert mogą być utworzone dla każdego wstawianego wiersza. Zmusza to bazę danych do parsowania każdego zapytania osobno, a następnie wykonania go. Parsowanie każdego zapytania oznacza przekazanie go do optymalizatora zapytań. Może to być kosztowna operacja, szczególnie w bardziej złożonych bazach danych, uwzględniających statystyki obiektów jako część optymalizacji zapytań. Często, chociaż nie zawsze, jest to podejście łatwiejsze do oprogramowania, szczególnie jeżeli program operuje na znanym zestawie tabel i kolumn. • Możemy utworzyć zapytanie zawierające miejsca do wstawienia wartości, sparsować je raz i wykonać wiele razy, przypisując nowe wartości w przygotowanych miejscach przy każdym wywołaniu polecenia. Rozwiązanie to wymaga wykorzystania mechanizmów interfejsu służących do przypisywania danych, a co za tym idzie, jest bardziej skomplikowane niż wykonywanie zapytań utworzonych za pomocą funkcji do manipulowania na ciągach znaków, jednak prawie zawsze skutkuje znacznie szybszym kodem. Ten skrypt wypisuje utworzone polecenie insert na standardowym urządzeniu wyjścia, dzięki czemu możemy zobaczyć rezultat naszej pracy. W przypadku tabeli prac wygląda on następująco: insert into prac values(:1,:2,:3,:4,:5,:6,:7,:8)
Encje :1,:2,:3...:8 są nazywane polami zastępczymi. Dowolny ciąg znaków alfanumerycznych poprzedzony znakiem dwukropka jest dozwolonym znakiem zastępczym. Kiedy baza danych przygotowuje zapytanie zawierające znaki zastępcze, parsuje je na formę wewnętrzną, jednak nie może wykonać polecenia, dopóki nie zostaną podane wartości, które mają być wstawione w wyznaczone miejsca. Ta część jest wykonywana przez
146
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
wywołanie metod bindValue lub bindParam, które przypisują zmienne do odpowiednich pól. W naszym skrypcie została wykorzystana metoda bindValue, ponieważ główna pętla przy każdej iteracji zwraca nową zmienną $wiersz, więc przypisywanie jej jako parametru nie miałoby sensu. Zmienną można było zadeklarować na początku skryptu jako zmienną globalną, jednak używanie zmiennych globalnych nie jest uznawane za dobrą praktykę programistyczną. Zmienne globalne czynią program nieczytelnym i mogą prowadzić do kolizji i błędów. Metoda prepare zwraca instancję klasy statement, która także posiada metodę execute. Kiedy wszystkie pola zastępcze mają już przypisaną wartość, polecenie może być wykonane wiele razy bez konieczności powtórnego parsowania go; wystarczy tylko dostarczać nowy zestaw danych przy każdym wykonaniu. Wywołanie przypisujące wygląda następująco: $res->bindValue(":$i", $wiersz[$i - 1]);
Głównym powodem, dla którego wybraliśmy dla pól formę :$i, było umożliwienie bindowania w pętli. W ten sposób dochodzimy do końca skryptu. Jest jeszcze jedna rzecz, którą należy zauważyć — dziwny warunek if w głównej pętli, sprawdzający przy wykorzystaniu funkcji implode i strlen, czy wiersz jest pusty. Obiekty SplFileObject w niektórych wersjach PHP 5.3 zwracały pustą linię na końcu pliku, bez tego sprawdzenia wstawienie dla niej także byłoby wykonane, ponieważ SQLite nie wspiera więzów integralności. Inne bazy odrzuciłyby wiersz zawierający puste pole dla klucza głównego, ale też wycofałyby całą transakcję, czyli usunięte zostałyby wszystkie poprzednio wstawione wiersze — nie jest to zachowanie, o które nam chodziło. Dodanie tego polecenia if jest niską ceną za sprawdzanie błędów w klasie i brak konieczności pisania rzeczy w stylu: $fp = fopen($nazwaPliku, "r"); if (!$fp) { die("Nie mogę otworzyć pliku $nazwaPliku do odczytu!\n"); }
Zrobili to już autorzy biblioteki SPL. Jest jeszcze jedna rzecz, którą trzeba przeanalizować, zanim zakończymy ten rozdział. Do tej pory tworzyliśmy tabele i ładowaliśmy do nich dane, jednak jeszcze nic z nich nie pobieraliśmy. Zapytanie, które zostanie wykonane, jest standardowym złączeniem: select p.pNazwisko,p.stanowisko,d.dNazwa,d.lokalizacja from prac p join dzial d on(d.dzialId=p.dzialId);
Takie zapytanie nazywane jest złączeniem, ponieważ łączy dane z dwóch tabel (lub większej ich liczby), aby zaprezentować je jako wiersze. Składnia ta jest nazywana złączeniem ANSI i jest dobrze przenośna między bazami danych. Dokładnie to zapytanie mogłoby być wykonane dla dowolnej bazy relacyjnej bez konieczności zmiany nawet jednego znaku. Wykonanie skryptów pokazanych na listingach 7.10 i 7.11 utworzy strukturę bazy i wypełni ją danymi, możemy więc — zanim jeszcze zaczniemy pisać skrypt — przetestować zapytanie za pomocą wcześniej wspomnianego narzędzia sqlite3. Zwykłe wyświetlenie danych byłoby banalne, więc skrypt wypisze także nagłówki kolumn i odpowiednio ustawi format danych (listing 7.12). Listing 7.12. Skrypt pobierający dane z bazy SQLite query($zapytanie); if ($db->lastErrorCode() != 0) { throw new Exception($db->lastErrorMsg()); } // pobranie liczby kolumn $liczbaKolumn = $res->numColumns(); // Dla każdej kolumny zdefiniuj format w zależności od jej typu.
147
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
foreach (range(0, $liczbaKolumn - 1) as $i) { $nazwyKolumn[$i] = $res->columnName($i); switch ($res->columnType($i)) { case SQLITE3_TEXT: $formaty[$i] = "% 12s"; break; case SQLITE3_INTEGER: $formaty[$i] = "% 12d"; break; case SQLITE3_NULL: $formaty[$i] = "% 12s"; break; default: $formaty[$i] = "%12s"; } } // wyświetlenie nazw kolumn po konwersji na kapitaliki foreach ($nazwyKolumn as $c) { printf("%12s", strtoupper($c)); } // wypisanie podkreślenia nagłówka printf("\n% '-48s\n", "-"); // wypisanie danych wiersza while ($wiersz = $res->fetchArray(SQLITE3_NUM)) { foreach (range(0, $liczbaKolumn - 1) as $i) { printf($formaty[$i], $wiersz[$i]); } print "\n"; } } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Wynik zapytania wygląda następująco: ./listing7_12.php PNAZWISKO STANOWISKO DNAZWA LOKALIZACJA -----------------------------------------------KOWALSKI SPRZEDAWCA ANALIZY OPOLE NOWAK SPRZEDAWCA SPRZEDAZE SZCZECIN BONIEK SPRZEDAWCA SPRZEDAZE SZCZECIN DOBROWOLSKI MENEDZER ANALIZY OPOLE MATUSZCZYK SPRZEDAWCA SPRZEDAZE SZCZECIN CIBORSKI MENEDZER SPRZEDAZE SZCZECIN KOS MENEDZER KSIEGOWOSC WARSZAWA KACZMAREK ANALITYK ANALIZY OPOLE KROL PREZES KSIEGOWOSC WARSZAWA JANKIEWICZ SPRZEDAWCA SPRZEDAZE SZCZECIN DORUCH SPRZEDAWCA ANALIZY OPOLE MIREK SPRZEDAWCA SPRZEDAZE SZCZECIN KOWALEWSKI ANALITYK ANALIZY OPOLE KOREK SPRZEDAWCA KSIEGOWOSC WARSZAWA
Skrypt ten zawiera standardowe elementy, ale także kilka nowych wywołań. Nowe metody to columnName, columnType i fetchArray. Metoda columnName jest bardzo prosta. Przyjmuje jako argument numery kolumn (od zera) i zwraca nazwę kolumny. Metoda columnType jest bardzo podobna do columnName, zwraca predefiniowane 148
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
stałe o nazwach: SQLITE3_INTEGER, SQLITE3_FLOAT, SQLITE3_TEXT, SQLITE3_BLOB oraz SQLITE3_NULL. Przedstawiane przez nie typy są zgodne z ich nazwami. Inne bazy danych pokazują także takie informacje, jak wielkość kolumny czy skala (dla kolumn zmiennoprzecinkowych), jednak SQLite jest bazą wbudowaną i nie udostępnia takich informacji. Ostatnia metoda to fetchArray, która zwraca dane z bazy danych wiersz po wierszu, przedstawiając wiersze jako zwykłe tablice, tablice asocjacyjne lub oba rodzaje tablic, zależnie od ustawionej flagi trybu, która może przyjmować jedną z trzech wartości: SQLITE3_NUM, SQLITE3_ASSOC lub SQLITE3_BOTH.
Podsumowanie SQLite Interfejs PHP dla SQLite jest analogiczny do normalnych wywołań, jakie możemy wykonywać w dowolnym interfejsie dla innych baz danych, takich jak MySQL czy PostgreSQL — obie zostaną opisane w następnym rozdziale. SQLite zyskało duże powodzenie wraz ze wzrostem popularności komputerów bezprzewodowych. Nie jest pełnoprawnym systemem RDBMS, posiadającym wsparcie wersjonowania, blokowania na niskim poziomie, własny protokół sieciowy czy możliwość uczestniczenia w dwufazowym rozproszonym zatwierdzaniu zmian; nie ma nawet wsparcia dla prostych więzów integralności. Ma jednak znajomo wyglądający interfejs SQL oraz rozszerzenia dla różnych języków programowania, dzięki czemu łatwo jest nauczyć się z niego korzystać osobom, które wcześniej pracowały z innymi systemami relacyjnymi. Jest idealną bazą do przechowywania takich rzeczy, jak: zakładki w przeglądarce Firefox, listy kontaktowe, numery telefonów czy lista utworów odtwarzanych na telefonie komórkowym. PHP i Apache są także dostępne dla wielu platform, w tym mobilnych, włączając w to iPhone’a, co czyni parę PHP/SQLite idealną do tworzenia oprogramowania na urządzenia mobilne. Takie zestawienie ma kilka ciekawych możliwości, które wykraczają poza zakres tej książki. Możliwe jest rozszerzenie funkcjonalności SQLite i zarejestrowanie funkcji PHP, tak aby działały wewnątrz serwera SQL lub nawet jako funkcje agregujące. Oba produkty szybko się rozwijają i ich możliwości będą z pewnością coraz większe.
Podsumowanie Ten rozdział został poświęcony integracji PHP z bazami danych, które nie są standardowymi bazami, jak MySLQ czy PostgreSQL. Wszystkie te bazy są nowe. Na przykład opisany w tym rozdziale SQLite jest dostępny tylko dla PHP 5.3 i późniejszych wersji. MongoDB i CouchDB także są nowymi technologiami. Na razie w świecie baz danych dominują bazy relacyjne, które są zamodelowane z myślą o finansach. W kolejnym rozdziale przedstawimy pełnoprawny systemem relacyjny MySQL oraz dwie warstwy abstrakcji: PDO i ADOdb. Na końcu rozdziału poświęcimy trochę uwagi narzędziu Sphinx, które jest bardzo popularnym programem wyszukiwania pełnotekstowego.
149
ROZDZIAŁ 7. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ I
150
ROZDZIAŁ 8
Integracja z bazami danych. Część II W tym rozdziale pokażemy, w jaki sposób pracować z pełnoprawną relacyjną bazą danych, jaką jest MySQL. Następnie zapoznasz się z dwoma warstwami abstrakcji — PDO i ADOdb. Na końcu rozdziału wyjaśnimy, jak korzystać z systemu wyszukiwania pełnotekstowego Sphinx. W ramach bazy MySQL jest kilka rozszerzeń PHP. Najczęściej stosowanym jest rozszerzenie MySQL. Jest ono dość stare i nie wspiera obiektowości — istnieje od czasów PHP4 i MySQL4. Brakuje mu kilku ważnych opcji, takich jak przypisywanie zmiennych. Dostępne jest nowsze rozszerzenie — MySQLi — które także będzie omówione w tym rozdziale. Należy nadmienić, że stare, proceduralne rozwiązanie w dalszym ciągu jest używane najczęściej.
Wprowadzenie do rozszerzenia MySQLi Rozszerzenie MySQLi jest pod wieloma względami podobne do rozszerzenia SQLite3 omawianego w poprzednim rozdziale. Jest zorientowane obiektowo w przeciwieństwie do starego rozszerzenia MySQL — podobnie jak w przypadku SQLite3 nie zwraca wyjątków. Rozszerzenia są jednakowe — z wyjątkiem bazy danych, która w tym przypadku jest o wiele potężniejsza niż SQLite i wspiera pełny zestaw możliwości zawartych w standardzie ANSI. Na potrzeby tego rozdziału MySQL w wersji 5.1 został uruchomiony na maszynie lokalnej: mysql -u test --password=test test Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with –A Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 50 Server version: 5.1.37-1ubuntu5.5 (Ubuntu) Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> select version(); +-------------------+ | version() | +-------------------+ | 5.1.37-1ubuntu5.5 | +-------------------+ 1 row in set (0.00 sec)
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Nazwa użytkownika to test, hasło test, nazwa bazy to także test. Struktura bazy będzie taka sama jak w przykładzie SQLite3: utworzone zostaną te same tabele prac i dzial. Wykorzystamy również te same dwa skrypty — jeden do załadowania danych do bazy, drugi do uruchamiania zapytań. Przepisanie skryptów pozwala na dokonanie porównania i bardzo jasno tłumaczy przeznaczenie poszczególnych metod rozszerzenia MySQLi. Oto opisy tabel w stylu MySQL: mysql> describe prac; +------------------+-------------+------+-----+-------------------+-------+ | Field | Type | Null | Key | Default | Extra | +------------------+-------------+------+-----+-------------------+-------+ | pId | int(4) | NO | PRI | NULL | | | pNazwisko | varchar(10) | YES | | NULL | | | stanowisko | varchar(9) | YES | | NULL | | | men | int(4) | YES | | NULL | | | datazatrudnienia | timestamp | NO | | CURRENT_TIMESTAMP | | | pensja | double | YES | | NULL | | | prowizja | double | YES | | NULL | | | dzialId | int(4) | YES | | NULL | | +------------------+-------------+------+-----+-------------------+-------+ 8 rows in set (0.05 sec) mysql> describe dzial; +-------------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+-------------+------+-----+---------+-------+ | dzialId | int(4) | NO | PRI | NULL | | | dNazwa | varchar(14) | YES | | NULL | | | lokalizacja | varchar(13) | YES | | NULL | | +-------------+-------------+------+-----+---------+-------+ 3 rows in set (0.00 sec)
Tabele są puste i także w tym przypadku dane zostaną załadowane z dwóch plików CSV. CSV (ang. comma-separated values — wartości oddzielone przecinkami) jest standardowym formatem tabelarycznym w plikach tekstowych rozpoznawanych przez bazę danych MySQL i arkusze kalkulacyjne, takie jak Microsoft Excel. Większość baz danych ma specjalne mechanizmy pozwalające na łatwiejsze ładowanie danych z plików CSV. Dotyczy to również MySQL-a, który udostępnia polecenie LOAD DATA zademonstrowane w poniższym przykładzie. Skrypt ten jest jednak nadal dobrą ilustracją. Oto opis składni polecenia LOAD DATA: mysql> help load data Name: 'LOAD DATA' Description: Syntax: LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name' [REPLACE | IGNORE] INTO TABLE tbl_name [CHARACTER SET charset_name] [{FIELDS | COLUMNS} [TERMINATED BY 'string'] [[OPTIONALLY] ENCLOSED BY 'char'] [ESCAPED BY 'char'] ] [LINES [STARTING BY 'string'] [TERMINATED BY 'string'] ] [IGNORE number LINES] [(col_name_or_user_var,...)] [SET col_name = expr,...]
152
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Pliki, z których będziemy ładować dane, to prac.csv i dzial.csv. Plik prac różni się nieco od wersji z rozdziału 7., dlatego że SQLite w przeciwieństwie do MySQL nie wspiera typów dat. MySQL wspiera pełen zakres typów dat i działań arytmetycznych na datach wyspecyfikowanych w standardzie ANSI. Aby wstawić dane dla typu TIMESTAMP, musimy użyć poprawnego formatu daty, czyli YYYY-MM-DD HH24:MI:SS. YYYY oznacza czterocyfrowy rok, MM numer miesiąca, DD dzień miesiąca, HH24 to godzina, a MI i SS oznaczają odpowiednio minuty i sekundy. Oto zawartość plików: prac.csv 7369,KOWALSKI,SPRZEDAWCA,7902,"1980-12-17 00:00:00",800,,20 7499,NOWAK,SPRZEDAWCA,7698,"1981-02-20 00:00:00",1600,300,30 7521,BONIEK,SPRZEDAWCA,7698,"1981-02-22 00:00:00",1250,500,30 7566,DOBROWOLSKI,MENEDZER,7839,"1981-04-02 00:00:00",2975,,20 7654,MATUSZCZYK,SPRZEDAWCA,7698,"1981-09-28 00:00:00",1250,1400,30 7698,CIBORSKI,MENEDZER,7839,"1981-05-01 00:00:00",2850,,30 7782,KOS,MENEDZER,7839,"1981-06-09 00:00:00",2450,,10 7788,KACZMAREK,ANALITYK,7566,"1987-04-19 00:00:00",3000,,20 7839,KROL,PREZES,,"1981-11-17 00:00:00",5000,,10 7844,JANKIEWICZ,SPRZEDAWCA,7698,"1981-09-08 00:00:00",1500,0,30 7876,DORUCH,SPRZEDAWCA,7788,"1987-05-23 00:00:00",1100,,20 7900,MIREK,SPRZEDAWCA,7698,"1981-12-03 00:00:00",950,,30 7902,KOWALEWSKI,ANALITYK,7566,"1981-12-03 00:00:00",3000,,20 7934,KOREK,SPRZEDAWCA,7782,"1982-01-23 00:00:00",1300,,10
Plik dzial jest identyczny z tym z rozdziału 7.: dzial.csv 10,KSIEGOWOSC,WARSZAWA 20,ANALIZY,OPOLE 30,SPRZEDAZE,SZCZECIN 40,OBSLUGA,LUBLIN
Skrypt tworzący tabele nie zawiera żadnych interesujących elementów. Wszystkie wywołania wykorzystywane do uruchomienia poleceń CREATE TABLE są zastosowane także w skryptach dotyczących wstawiania i pobierania danych. Listing 8.1 przedstawia skrypt ładujący dane z plików CSV do odpowiadających im tabel MySQL. Listing 8.1. Załadowanie danych z plików CSV do odpowiadających im tabel MySQL \n"); } $nazwaTabeli = $argv[1]; $nazwaPliku = $argv[2]; $liczbaWierszy = 0; function utworz_polecenie_insert($tabela, $liczbaKolumn) { $polecenie = "insert into $tabela values("; foreach (range(1, $liczbaKolumn) as $i) { $polecenie.= "?,"; } $polecenie = preg_replace("/,$/", ')', $polecenie); return ($polecenie); } try { $db = new mysqli("localhost", "test", "test", "test"); $db->autocommit(FALSE); $res = $db->prepare("select * from $nazwaTabeli"); if ($db->errno != 0) { throw new Exception($db->error);
153
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
} $liczbaKolumn = $res->field_count; $res->free_result(); $ins = utworz_polecenie_insert($nazwaTabeli, $liczbaKolumn); $fmt = str_repeat("s", $liczbaKolumn); $res = $db->prepare($ins); if ($db->errno != 0) { throw new Exception($db->error); } $fp = new SplFileObject($nazwaPliku, "r"); while ($wiersz = $fp->fgetcsv()) { if (strlen(implode('', $wiersz)) == 0) continue; array_unshift($wiersz, $fmt); foreach(range(1,$liczbaKolumn) as $i) { $wiersz[$i]=&$wiersz[$i]; } call_user_func_array(array(&$res, "bind_param"), &$wiersz); $res->execute(); if ($res->errno != 0) { print_r($wiersz); throw new Exception($res->error); } $liczbaWierszy++; } $db->commit(); if ($db->errno != 0) { throw new Exception($db->error); } print "Do tabeli $nazwaTabeli wstawiono $liczbaWierszy wierszy.\n"; } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
W tym skrypcie znajduje się kilka interesujących elementów. Łączenie z bazą nie jest jednym z nich. Argumenty tworzenia nowego połączenia są następujące: nazwa hosta, nazwa użytkownika, hasło oraz baza, z którą chcemy się połączyć. To polecenie wyłączy automatyczne zatwierdzanie zmian: $db->autocommit(FALSE);
MySQL jest w pełni transakcyjną bazą danych, która wspiera wymagania ACID omówione w rozdziale 7. Polecenie COMMIT jest poleceniem ANSI SQL zatwierdzającym i utrwalającym zmiany wykonane w aktualnej transakcji. Przeciwieństwem jest komenda ROLLBACK, która anuluje efekty aktualnej transakcji. W trybie automatycznego zatwierdzania baza wykona polecenie COMMIT po każdym poleceniu SQL, na przykład po poleceniu INSERT. Polecenie COMMIT jest bardzo kosztowne, ponieważ zanim będą mogły być wykonane dalsze czynności, sesja musi czekać, aż informacje zostaną fizycznie zapisane na dysku — zgodnie z wymaganiami ACID. Polecenie to nie tylko jest kosztowne i czasochłonne — jego automatyczne wykonywanie może prowadzić do sytuacji, w której tylko część danych będzie zapisana w bazie, co z reguły jest niepożądane. Dobrzy programiści będą chcieli wyeliminować problem i ponownie uruchomić ładowanie. UWAGA. Wyłączenie automatycznego zatwierdzania transakcji jest częstą praktyką podczas pracy z relacyjnymi bazami danych. Postępuje się tak, kiedy skrypt zawiera polecenia INSERT, UPDATE lub DELETE. Automatyczne zatwierdzanie jest bardzo kosztowną operacją.
154
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Tak jak w przypadku SQLite, wykorzystane zostało polecenie select * from tabela w celu zdeterminowania liczby kolumn w tabeli. Pierwszym wywołaniem jest prepare: $res = $db->prepare("select * from $nazwaTabeli");
To polecenie sparsuje polecenie SQL i zamieni je na obiekt klasy MYSQLI_STMT — czyli sparsowane polecenie. Jedną z właściwości klasy jest liczba pól: $liczbaKolumn = $res->field_count;
Kiedy znamy już liczbę kolumn, możliwe jest zamknięcie wyniku za pomocą polecenia free_result oraz zbudowanie zapytania INSERT. Funkcja, która została do tego wykorzystana, jest bardzo podobna do tej z listingu 7.11, ale nie jest identyczna. Różnica jest taka, że polecenie teraz wygląda tak: insert into dzial values(?,?,?)
Zamiast pól zastępczych :1, :2 i :3 pojawiły się znaki zapytania. Powodem tego jest brak wsparcia dla nazwanych przypisań w MySQLi — możliwe są jedynie przypisania pozycyjne. Wszystkie przypisania muszą być wykonane jednocześnie poprzez przypisanie tablicy elementów do sparsowanego polecenia. Metoda przypisania ma następujący format: $res->bind_param("fmt",$var1,$var2,$var3,...,$varN);
Pierwszym parametrem jest ciąg formatu, który składa się z jednego znaku dla każdego przypisywanego elementu. Znaki te mówią o typie zmiennej przypisywanej na pozycji zgodnej z jej pozycją w tablicy. To oznacza, że $var1 zostanie przypisana do pierwszej pozycji w zapytaniu, oznaczonej pierwszym znakiem zapytania, $var2 do drugiej itd. Znaki formatu to i dla liczb całkowitych, d dla typu double, s dla ciągów znaków, b dla typu blob. Bloby to zbiory danych binarnych, np. obrazy. Stanęliśmy przed problemem: musimy przypisać zmienne do polecenia za pomocą jednego polecenia PHP, nie wiemy jednak, ile zmiennych musimy przypisać. Łańcuch formatu jest prosty — składa się po prostu z samych łańcuchów znaków. Jedną z zalet słabo typowanych języków programowania, takich jak PHP, jest to, że z reguły typy danych nie stanowią problemu; prawie wszystko można przekonwertować na ciąg znaków. Na potrzeby metody bind_param musimy skorzystać ze sztuczki. Na szczęście język PHP jest pod tym względem bardzo usłużny. Istnieje funkcja PHP o nazwie call_user_func_array, która wywołuje funkcję nazwaną w pierwszym parametrze z tablicą z drugiego parametru jako tablicą argumentów. Gdybyśmy mieli funkcję F(), przyjmującą trzy argumenty ($a1, $a2 i $a3), to wyrażenie F($a1,$a2,$a3) byłoby całkowicie równoważne z wyrażeniem call_user_func_array("F",array($a1,$a2,$a3)). Gdyby funkcja F() była metodą obiektu $obj, pierwszy argument musiałby przyjąć formę array($obj,"F") zamiast samego "F". To rozwiązałoby problem w każdej wersji PHP, aż do wersji 5.3. Niestety, w wersji PHP 5.3 MySQLi oczekuje referencji do przypisywanych zmiennych i nie akceptuje wartości. To jest powodem utworzenia w kodzie następującego zapisu: array_unshift($wiersz, $fmt); foreach(range(1,$liczbaKolumn) as $i) { $wiersz[$i]=&$wiersz[$i]; }
Upewniamy się, że każda z przypisywanych zmiennych zawiera referencję do wartości. Nie odnosi się to do łańcucha formatu. Zakres pętli rozpoczyna się od 1, po wykonaniu unshift format znajduje się na początku tablicy. PHP indeksowanie tablic zawsze rozpoczyna od 0, nasz zakres natomiast rozpocznie się od 1, co oznacza, że pomijamy format, pozostawiając go jako wartość. Po przygotowaniu tablicy argumentów „magiczne” przypisywanie wartości wygląda następująco: call_user_func_array(array(&$res, "bind_param"), &$wiersz);
Następnie wykonywane jest sparsowane polecenie $res. Operacja jest powtarzana dla każdego wiersza zwróconego z pliku CSV z obiektu SplFileObject. Po odczytaniu wszystkich wierszy pętla kończy swoje działanie i wywoływane jest zatwierdzenie operacji. Jest to logiczne miejsce dla polecenia commit. Oczywiście, jak zostało powiedziane na początku tego rozdziału, MySQLi nie zwraca wyjątków; programista jest odpowiedzialny za sprawdzenie błędów po każdym krytycznym kroku. MySQLi udostępnia świetne narzędzia wspomagające ten proces. Każdy obiekt z biblioteki MySQLi posiada właściwości errno oraz error. Właściwość errno zawiera 155
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
kod błędu, natomiast error — jego tekstowy opis. W bibliotece MySQLi znajdują się trzy różne klasy: MYSQLi — opisująca połączenie z bazą danych, MYSQLi_STMT — opisująca sparsowane polecenia, MYSQLi_RESULT — opisująca zestaw wynikowy zwrócony przez bazę do skryptu PHP. Listing 8.1 wykorzystywał klasy połączenia i polecenia. Aby zobaczyć klasę wyniku, musimy pobrać dane (listing 8.2). Listing 8.2. Tworzenie raportu identycznego z raportem z listingu 7.12 query($zapytanie); print "\n"; if ($db->errno != 0) { throw new Exception($db->error); } // pobranie liczby kolumn $liczbaKolumn = $res->field_count; // pobranie nazw kolumn while ($info = $res->fetch_field()) { $nazwyKolumn[] = strtoupper($info->name); } // wypisanie nazw kolumn foreach ($nazwyKolumn as $c) { printf("%-12s", $c); } // wypisanie podkreślenia nagłówka printf("\n%s\n", str_repeat("-", 12 * $liczbaKolumn)); // wypisanie danych wiersza while ($wiersz = $res->fetch_row()) { foreach (range(0, $liczbaKolumn - 1) as $i) { printf("%-12s", $wiersz[$i]); } print "\n"; } } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Skrypt wyświetli raport identyczny z tym ze skryptu z listingu 7.12. Także struktura skryptu jest taka sama jak na listingu 7.12. Zwróć uwagę, że nie ma konieczności wyłączania automatycznego zatwierdzania, ponieważ nie ma tutaj żadnych transakcji. Metoda query, należąca do klasy połączenia, zwraca obiekt klasy MYSQLI_RESULT, w tym przypadku nazwany $res; jedna z jego właściwości przechowuje liczbę kolumn: $liczbaKolumn = $res->field_count;
156
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Dla każdej kolumny istnieje opis — obiekt pomocniczej klasy stdClass. Obiekt pobierany jest poprzez wywołanie metody fetch_field obiektu $res. Oto fragment kodu: while ($info = $res->fetch_field()) { $nazwyKolumn[] = strtoupper($info->name); }
Skrypt wykorzystuje jedynie właściwość name, ale cały opis, zawarty w obiekcie $info, wygląda tak: stdClass Object ( [name] => pId [orgname] => pId [table] => prac [orgtable] => prac [def] => [max_length] => 4 [length] => 4 [charsetnr] => 63 [flags] => 53251 [type] => 3 [decimals] => 0 )
Właściwość name odnosi się do nazwy kolumny. Właściwości orgname i orgtable są ważne podczas pracy z widokami. Standard SQL opisuje obiekty nazywane widokami, które w rzeczywistości są nazwanymi zapytaniami. Zapytania mogą zmieniać nazwy kolumn; właściwość name będzie zawierała nową nazwę, podczas gdy oryginalna nazwa i tabela będą zapisane we właściwościach orgname i orgtable. Najważniejszą kolumną, poza kolumnami z nazwą i długością, jest kolumna zawierająca typ. Niestety, znaczenie liczb odnoszących się do typów nie zostało opisane w dokumentacji MySQLi. Z doświadczenia wiemy, że 3 oznacza liczbę całkowitą, 5 oznacza typ double, 253 to łańcuch znaków, a 7 to typ timestamp. Tak jak w przypadku innych baz danych, wywołujemy metodę fetch, aby pobrać dane z bazy. W tym przypadku metoda to fetch_row. Pętla pobierająca dane jest taka sama jak ta z listingu 7.12: while ($wiersz = $res->fetch_row()) { foreach (range(0, $liczbaKolumn - 1) as $i) { printf("%-12s", $wiersz[$i]); } print "\n"; }
Wynik skryptu wygląda tak samo jak wynik skryptu z listingu 7.12: PNAZWISKO STANOWISKO DNAZWA LOKALIZACJA -----------------------------------------------KOWALSKI SPRZEDAWCA ANALIZY OPOLE NOWAK SPRZEDAWCA SPRZEDAZE SZCZECIN BONIEK SPRZEDAWCA SPRZEDAZE SZCZECIN DOBROWOLSK MENEDZER ANALIZY OPOLE MATUSZCZYK SPRZEDAWCA SPRZEDAZE SZCZECIN CIBORSKI MENEDZER SPRZEDAZE SZCZECIN KOS MENEDZER KSIEGOWOSC WARSZAWA KACZMAREK ANALITYK ANALIZY OPOLE KROL PREZES KSIEGOWOSC WARSZAWA JANKIEWICZ SPRZEDAWCA SPRZEDAZE SZCZECIN DORUCH SPRZEDAWCA ANALIZY OPOLE MIREK SPRZEDAWCA SPRZEDAZE SZCZECIN KOWALEWSKI ANALITYK ANALIZY OPOLE KOREK SPRZEDAWCA KSIEGOWOSC WARSZAWA
157
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Podsumowanie rozszerzenia MySQLi Rozszerzenie MySQLi jest nowocześniejsze i ma o wiele większe możliwości niż oryginalne rozszerzenie MySQL. Brakuje mu jednak kilku ważnych funkcji, takich jak nazwane przypisania czy obsługa wyjątków. Wiele firm hostingowych zezwala wyłącznie na stosowanie oryginalnego rozszerzenia MySQL, które jest wypierane przez większe, lepsze i szybsze rozszerzenie MySQLi. Na szczęście nie jest to jedyna możliwość wyboru. Istnieje także rodzina rozszerzeń PDO, która pozwala rozwiązywać problemy dotyczące nazwanych przypisań i wyjątków. Rozszerzenia te omawiamy poniżej.
Wprowadzenie do PDO Nazwa PDO jest skrótem od określenia „PHP data objects” (obiekty danych PHP). PDO jest próbą unifikacji rozszerzeń dla wszystkich baz danych w jednym interfejsie programistycznym (API), co uprościłoby proces programowania i zmniejszyło zasób wiedzy potrzebnej do tworzenia oprogramowania współpracującego z bazami danych. Projekt okazał się sukcesem w odniesieniu do niektórych baz danych. Podczas gdy wszystko sprowadzono do wspólnego mianownika, niektóre specjalne właściwości zostały utracone — na przykład polecenie copy w PostgreSQL czy interfejs tablicowy i łączenie sesji w Oracle. Te funkcje zaprojektowano w celu poprawienia szybkości przetwarzania danych, ale nie są one dostępne za pośrednictwem PDO. Ponadto producenci systemów bazodanowych wspierają raczej rozszerzenia przeznaczone dla ich baz danych, zaniedbując PDO. PDO ma dwie warstwy. Pierwsza to główny interfejs PDO, druga to sterownik dla konkretnej bazy danych, który w połączeniu z interfejsem wykonuje właściwą komunikację z bazą. PDO jest domyślnie włączone, jednak sterowniki baz danych muszą być instalowane indywidualnie. Jedną z baz danych, dla których PDO jest lepszy niż ich własne rozszerzenia, jest właśnie MySQL. Zobaczmy, jak wyglądałby skrypt ładujący dane z CSV, utworzony przy wykorzystaniu PDO (listing 8.3). Listing 8.3. Ładowanie danych z plików CSV przy wykorzystaniu PDO \n"); } $nazwaTabeli = $argv[1]; $nazwaPliku = $argv[2]; $liczbaWierszy = 0; function utworz_polecenie_insert($tabela, $liczbaKolumn) { $polecenie = "insert into $tabela values("; foreach (range(1, $liczbaKolumn) as $i) { $polecenie.= "?,"; } $polecenie = preg_replace("/,$/", ')', $polecenie); return ($polecenie); } try { $db = new PDO('mysql:host=localhost;dbname=test, 'test', 'test'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $res = $db->prepare("select * from $nazwaTabeli"); $res->execute(); $liczbaKolumn = $res->columnCount(); $ins = utworz_polecenie_insert($nazwaTabeli, $liczbaKolumn); $res = $db->prepare($ins); $fp = new SplFileObject($nazwaPliku, "r"); $db->beginTransaction(); while ($wiersz = $fp->fgetcsv()) { if (strlen(implode('', $wiersz)) == 0) continue; $res->execute($wiersz);
158
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Jak do tej pory, jest to najkrótsza wersja, mimo to zapewnia pełną funkcjonalność. Większość pominiętego kodu to kod obsługujący błędy. Tego typu kod został całkowicie usunięty ze skryptu. Powodem jest wywołanie setAttribute następujące bezpośrednio po połączeniu z bazą danych. W tym wywołaniu ustalamy, że w przypadku napotkania błędu PDO zwróci wyjątek klasy PDOException. Obiekt wyjątku zawiera kod błędu oraz jego opis słowny, które można wykorzystać do obsługi błędów. To uczyniło kod obsługi błędów niepotrzebnym — został więc usunięty ze skryptu. Kod, który przypisuje zmienne do pól zastępczych czy poleceń, także został pominięty. PDO potrafi przypisać zmienne podczas wykonania zapytania, podobnie jak kolejny interfejs, który omówimy — ADOdb. Metoda execute przyjmuje jako argument tablicę zmiennych i przypisuje je do polecenia bezpośrednio przed jego wykonaniem. Można to porównać do komplikacji z funkcją call_user_func_array, gdy było konieczne przypisanie zmiennych na listingu 8.1. PDO obsługuje nazwane przypisania przy wykorzystaniu metody bindValue, nie jest ona jednak często potrzebna. W skrypcie brak jest podejścia „wspólnego mianownika” — PDO nie ma możliwości wyłączenia automatycznego zatwierdzania transakcji dla sesji. Możemy jawnie rozpocząć transakcję, co oczywiście wyłączy automatyczne zatwierdzanie na czas trwania transakcji, ale nie dla całej sesji. Ponadto PDO musi na początku wykonać przygotowane zapytanie, aby mogło zostać opisane. Natywny sterownik MySQLi nie musi wykonywać zapytania, aby je opisać. Mogliśmy uruchomić metodę field_count dla zapytania, które zostało przygotowane, ale nie było uruchomione. Wykonywanie zapytań o długim czasie wykonywania może spowodować duże opóźnienie. Jeżeli ładowana tabela zawiera setki milionów rekordów, wykonanie początkowego zapytania select * from $nazwaTabeli może trwać godzinami. Powodem tego są wymagania ACID. Gwarantują one użytkownikowi, że zobaczy wyłącznie zmiany zatwierdzone przed rozpoczęciem jego zapytania. Baza danych musi zrekonstruować wiersze zmodyfikowane po rozpoczęciu zapytania i zaprezentować użytkownikowi wersję wierszy sprzed rozpoczęcia jego zapytania. Może to być czasochłonny proces, jeżeli tabela, na której jest przeprowadzany, jest duża i często modyfikowana. Alternatywą byłoby zablokowanie tabeli i uniemożliwienie innym użytkownikom wykonywania modyfikacji. Nie trzeba dodawać, że to rozwiązanie nie zda egzaminu, jeżeli jednym z wymagań biznesowych jest współbieżność operacji. Aby to osiągnąć, trzeba by się odwołać do sztuczek nieprzenośnych między systemami. Jedno z możliwych rozwiązań to utworzenie następującego zapytania: select * from $nazwaTabeli limit 1. To zapytanie zwróciłoby tylko jeden wiersz i dzięki temu zostałoby wykonane o wiele szybciej — niezależnie od rozmiaru tabeli. Niestety, zapytanie to nie zadziała dla bazy Oracle, która nie wspiera polecenia LIMIT, a zamiast tego stosuje polecenie ROWNUM. Nie ma uniwersalnego rozwiązania tego problemu. Jest to ryzyko używania PDO. Na szczęście większość użytkowników wykorzystuje tylko jeden system bazodanowy lub dwa takie systemy (np. tylko MySQL i PostgreSQL), więc z reguły nie jest to duży problem. Zobaczmy teraz drugi skrypt — mały raport tworzony za pomocą zapytania SQL (listing 8.4). Listing 8.4. Raport wykonany za pomocą zapytania SQL setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
159
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
$res = $db->prepare($zapytanie); $res->execute(); // pobranie liczby kolumn $liczbaKolumn = $res->columnCount(); // Dla każdej kolumny zdefiniuj format w zależności od jej typu. foreach (range(0, $liczbaKolumn - 1) as $i) { $info = $res->getColumnMeta($i); $nazwyKolumn[] = $info['name']; } // wyświetlenie nazw kolumn po konwersji na kapitaliki foreach ($nazwyKolumn as $c) { printf("%-12s", strtoupper($c)); } // wypisanie podkreślenia nagłówka printf("\n%s\n", str_repeat("-", 12 * $liczbaKolumn)); // wypisanie danych wiersza while ($wiersz = $res->fetch(PDO::FETCH_NUM)) { foreach ($wiersz as $r) { printf("%-12s", $r); } print "\n"; } } catch(PDOException $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Skrypt jest zupełnie standardowy — nie ma tu wiele do oglądania. Interesujące jest to, że metoda getColumnMeta, wykorzystana do opisania kursora, nadal w dokumentacji oznaczona jest jako eksperymentalna
i ma dopisek „używaj na własne ryzyko”. Ta metoda jest absolutnie niezbędna; jej brak znacznie ograniczyłby funkcjonalność całego PDO. Metoda ta nie działa dla wszystkich baz danych. Nie działa np. dla bazy Oracle. Opis kolumn zwrócony przez metodę wygląda następująco: Array ( [native_type] => VAR_STRING [flags] => Array ( ) [table] => d [name] => lokalizacja [len] => 13 [precision] => 0 [pdo_type] => 2 )
Nazwa tabeli to d, ponieważ metoda pobrała alias tabeli z zapytania. Wykonane zapytanie jest następujące: $zapytanie = "select p.pId,p.stanowisko,d.dNazwa,d.lokalizacja from prac p join dzial d on(d.dzialId=e.dzialId)";
160
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
W zapytaniu tym wykorzystaliśmy alias p dla tabeli prac oraz alias d dla tabeli dzial, aby skrócić warunek połączenia z dzial.dzialId=prac.dzialId do równie zrozumiałej (przez bazę danych) formy d.dzialId=p.dzialId. Metoda getColumnMeta zwróciła alias zamiast faktycznej nazwy tabeli. To niekoniecznie jest błąd, ale powoduje, że pole table jest mniej użyteczne. Ponadto metoda fetch zawiera opcję PDO::FETCH_NUM, podobną do tej z listingu 7.12, dotyczącego SQLite. Tak jak wtedy, metoda może zwrócić tablicę indeksowaną liczbami, nazwami kolumn lub jako obiekt z właściwościami takimi jak nazwy kolumn. Domyślne ustawienie to FETCH_BOTH, co spowoduje zwrócenie tablicy asocjacyjnej i indeksowanej liczbami. Skoro wspominamy o SQLite — także dla SQLite dostępny jest sterownik PDO. Co więcej, metoda getColumnMeta działa idealnie i zwraca nazwę tabeli, a nie jej alias, jak w przypadku MySQL. Oba skrypty PDO działałyby bez problemu, gdybyśmy zmienili linię, w której następuje połączenie na $db = new PDO('sqlite:test.sqlite'). Oczywiście polecenia rozpoczynające i kończące transakcje nie byłyby potrzebne, ale nie spowodowałyby też żadnych problemów.
Podsumowanie PDO PDO jest całkiem dobre jak na bibliotekę, która nadal jest rozwijana. W PHP6 PDO będzie jedynym rozszerzeniem baz danych, jednak nie zdarzy się to szybko. Rozszerzenie to jest zupełnie wystarczające dla podstawowych działań na bazie, jednak nadal nie potrafi korzystać z unikalnych cech baz, tworzonych głównie z myślą o wydajności. W związku z tym, że PDO nie jest w pełni funkcjonalne, należy je traktować raczej jako oprogramowanie beta.
Wprowadzenie do ADOdb Ostatnim opisywanym w tej książce rozszerzeniem dotyczącym baz danych jest ADOdb. Jest to rozszerzenie opracowane przez Johna Lima, dostępne bezpłatnie na licencji BSD. Większość dystrybucji Linuksa udostępnia je w formie pakietu, dla pozostałych można je pobrać pod adresem: http://adodb.sourceforge.net Instalacja sprowadza się do rozpakowania zawartości do katalogu. Katalog, w którym znajdą się rozpakowane elementy, powinien zostać wpisany do parametru include_path w pliku php.ini. UWAGA. Katalog, w którym znajdą się rozpakowane elementy, musi zostać wpisany do parametru include_path w pliku php.ini.
ADOdb zamodelowano zgodnie z frameworkiem firmy Microsoft — ADO. Wspiera wyjątki, iterowanie po kursorach bazy danych oraz pozycyjne i nazwane przypisywanie zmiennych. Wspiera także wiele baz danych, podobnie jak oryginalne ADO, między innymi: SQL, PostgreSQL, SQLite, Firebird, Oracle SQL Server, DB2 i Sybase. Wykorzystuje oryginalne rozszerzenia dla poszczególnych baz danych, wpisane w interpreter PHP. Jeżeli MySQL jest wspierany przez interpreter PHP, możliwe jest użycie ADOdb. Innymi słowy, ADOdb jest tylko strukturą klas nałożoną na oryginalny sterownik; ustawia opcje sterownika w zależności od własnych ustawień, jednak oryginalny sterownik nie jest dostarczany przez twórcę ADOdb. ADOdb dostępne jest w dwóch wariantach: pierwszym, starszym, który obsługuje zarówno PHP4 i PHP5, oraz nowszym, wspierającym wyłącznie PHP5. Przykłady w tej książce przetestowano przy wykorzystaniu nowszej wersji. Obie wersje wyglądają dokładnie tak samo. Jeżeli potrzebna jest wersja wspierająca PHP4, trzeba ją pobrać osobno. Oczywiście PHP4 nie obsługuje wyjątków, więc ta część nie będzie dostępna. ADOdb zawiera dwie klasy główne: klasę połączenia oraz klasę dla wyniku — klasę zestawu lub zestawu wynikowego — jak jest ona nazywana w dokumentacji ADOdb. Aby lepiej to wyjaśnić, zobaczmy pierwszy z dwóch skryptów — skrypt ładujący dane z plików CSV (listing 8.5).
161
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Poniższe dwie linie załadują do naszego skryptu wszystkie podstawowe klasy. Jest jeszcze kilka innych klas, które będą omówione później. require_once ('adodb5/adodb.inc.php'); require_once ('adodb5/adodb-exceptions.inc.php');
Dokładna lokalizacja dołączanych plików jest zależna od instalacji. Dystrybucja ADOdb może być wypakowana w dowolnym miejscu i będzie działała poprawnie, o ile ścieżka do tego miejsca będzie odpowiednio ustawiona w parametrze include_path. Nowe połączenie jest tworzone poprzez wywołanie funkcji NewADOConnection. Nie jest to klasyczny konstruktor PHP, lecz funkcja zwracająca instancję klasy. Kiedy obiekt połączenia zostanie utworzony, możliwe będzie nawiązanie połączenia poprzez wywołanie metody Connect. ADOdb udostępnia także metodę pozwalającą na wyłączenie automatycznego zatwierdzania zmian. W tym skrypcie jest to niepotrzebne, ponieważ on sam kontroluje transakcje — wyłączenie go nie spowoduje żadnych problemów i jest uznawane za dobrą praktykę programistyczną.
162
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Zauważ, że metoda wywołująca polecenia należy do klasy połączenia, a nie do klasy zestawu wynikowego. ADOdb musi wywołać zapytanie, aby możliwe było opisanie zestawu — określenie liczby kolumn, ich nazw, typów i długości. Liczba kolumn jest sprawdzana za pomocą polecenia FieldCount. Przypisywanie zmiennych nie jest konieczne. Możliwe jest przekazanie tablicy zmiennych do metody wykonującej polecenie, dokładnie tak samo jak w przypadku PDO. Warto po raz kolejny zauważyć, że metoda wykonująca polecenie jest w klasie połączenia, a nie w klasie wyniku. Metoda wywołująca jest potężna i wspiera wywołania tablicowe. Wywołanie tablicowe nie jest często stosowane w przypadku MySQL, ale jest wykorzystywane podczas pracy z Oracle czy PostgreSQL, które zostały specjalnie zoptymalizowane pod kątem takich wywołań. Co to oznacza? Gdybyśmy mieli wywołać następujące polecenie: $polecenie="insert into tab values(?,?)", a tablica wierszy wyglądałaby tak: $zestaw_wierszy = array( array($a1,$a2), array($b1,$b2), array($c1,$c2));
następujące polecenie wstawiłoby wszystkie trzy wiersze — jednym zapytaniem: $db->Execute($polecenie,$zestaw_wierszy);
Co zyskujemy, wstawiając cały zestaw danych w ten sposób? Przede wszystkim ogranicza to ruch sieciowy, który ciągle jest najsłabszym punktem aplikacji. Jeżeli mamy do wstawienia 100 wierszy, to wstawiając wiersz po wierszu, musimy wykonać 100 odwołań do bazy. Jeżeli rekordy wstawiane są w pakietach po 20, wystarczy pięć odwołań. Ponadto komunikacja między procesami jest zmniejszona, co skutkuje mniejszym obciążeniem bazy danych. Bindowanie zestawów jest domyślnie wyłączone i musi zostać aktywowane poprzez ustawienie atrybutu bulkInsert przy połączeniu: $db->bulkBind=true. Nie ma to większego sensu przy MySQL czy SQLite, ale może być bardzo przydatne podczas pracy z innymi bazami danych. Wszystko inne jest całkowicie standardowe — poza metodą CompleteTrans, która jest sprytna i „wie”, kiedy cofnąć transakcję, jeżeli wystąpi jakikolwiek błąd. Są także klasyczne metody zatwierdzania i cofania transakcji, ale podczas ich stosowania trzeba dodatkowo sprawdzać, czy pojawiają się błędy. Jest to działanie nadmiarowe, ponieważ ADOdb w przypadku błędu zwróci wyjątek, a transakcja zakończy się niepowodzeniem, zanim dojdzie do punktu zatwierdzenia. Ponadto mieliśmy problemy z metodą CompleteTrans podczas pracy z bazą PostgreSQL 9.0 — metoda wykonywała cofnięcie transakcji w momentach, w których powinna ją zatwierdzić. W rezultacie zamiast na nią postawiliśmy na funkcję CommitTrans. Zobaczmy teraz nasz raport. SQL jest już nam dobrze znany; interesuje nas tylko opisywanie kolumn i pobieranie wierszy (listing 8.6). Listing 8.6. Tworzenie raportu przy wykorzystaniu ADOdb Connect("localhost", "test", "test", "test"); $res = $db->Execute($zapytanie); // pobranie liczby kolumn $liczbaKolumn = $res->FieldCount(); // pobranie nazw kolumn foreach (range(0, $liczbaKolumn - 1) as $i) { $info = $res->FetchField($i);
163
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
$nazwyKolumn[] = $info->name; } // wyświetlenie nazw kolumn po konwersji na kapitaliki foreach ($nazwyKolumn as $c) { printf("%-12s", strtoupper($c)); } // wypisanie podkreślenia nagłówka printf("\n%s\n", str_repeat("-", 12 * $liczbaKolumn)); // wypisanie danych wiersza while ($wiersz = $res->FetchRow()) { foreach ($wiersz as $r) { printf("%-12s", $r); } print "\n"; } } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Na samym początku skryptu znajduje się linia ustawiająca zmienną $ADODB_FETCH_MODE na wartość ADODB_FETCH_NUM. Jest to inna wersja mechanizmu, który widzieliśmy już wcześniej. Zamiast przekazywania formy, w jakiej chcemy otrzymać dane, jako parametru, jak w przypadku PDO, w ADOdb ustawiamy specjalną zmienną globalną, która potem jest uwzględniana przez metodę FetchRow. Tak jak w przypadku PDO, ADOdb może zwrócić tablicę asocjacyjną, tablicę indeksowaną liczbami lub obie (domyślnie zwróci obie). Metodą opisującą kolumny jest FetchField. Przyjmuje ona jako argument numer kolumny i zwraca obiekt posiadający następujące właściwości: nazwa, typ i maksymalna długość. Oto przykład zwracanego obiektu: ADOFieldObject Object ( [name] => pNazwisko [max_length] => -1 [type] => varchar )
Jak widać na powyższym przykładzie, pole max_lenght nie jest zbyt dokładne, nie można na nim polegać. Na szczęście wiemy już, że PHP jest słabo typowanym językiem. ADOdb jest dużą biblioteką. Ma nawet własny mechanizm składowania wyników, który nie jest tak wydajny jak pakiet „memcached”, ale jest banalnie prosty w użyciu. Składowanie oparte jest na systemie plików. Rezultaty są zapisywane w plikach, a przy następnym wykonaniu zapytania rezultat jest po prostu pobierany z pliku. Jeżeli serwer PHP znajduje się na innej maszynie niż serwer baz danych, wykorzystanie tego mechanizmu może naprawdę zaoszczędzić czas i zasoby. Ponadto przechowywanie wyników zapytań działa dla wielu użytkowników — jeżeli uruchamiają oni podobne aplikacje, wyniki zostaną przechowane i przyspieszenie będzie znaczne. Aby zdefiniować przechowywanie wyników, należy tylko określić katalog, w którym mają być przechowywane, przez ustawienie zmiennej globalnej: $ADODB_CACHE_DIR="/tmp/adodb_cache";
Katalog przechowujący wyniki szybko się powiększa, więc powinien być umieszczony w miejscu, które jest regularnie czyszczone przez system. Katalog /tmp jest całkowicie czyszczony przy każdym uruchomieniu systemu, o ile system jest tak skonfigurowany. Przechowywane wyniki są wykorzystywane za pośrednictwem polecenia CacheExecute:
164
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
$res = $db->CacheExecute(900,$zapytanie);
Pierwszy parametr definiuje długość życia przechowywanego wyniku w sekundach. Jeżeli plik jest starszy, niż wynosi podana liczba sekund, nie będzie wykorzystany. Drugim argumentem jest zapytanie, które ma zostać wykonane. Powstanie plik, który będzie wyglądał następująco: ls -R /tmp/adodb_cache/ /tmp/adodb_cache/: 03 /tmp/adodb_cache/03: adodb_03b6f957459e47bab0b90eb74ffaea68.cache
Podkatalog 03 zależy od wyniku funkcji mieszającej wykonanej na zapytaniu. Następnie wykonywana jest inna funkcja, tworząca nazwę pliku. Jeżeli zapytanie w nazwie pliku jest takie samo jak zapytanie w skrypcie, wynik zostanie pobrany z pliku, a nie z bazy. Przypisywanie zmiennych jest zabronione. Przechowywane mogą być jedynie wyniki zapytań bez pól zastępczych. To zrozumiałe, ponieważ wynik zapytania zależy od przypisywanych wartości, a te przypisywane są podczas wykonania zapytania, co czyni zapamiętanie wyniku niemożliwym. Na często modyfikowanych bazach, dla których wymagane jest, aby dane były dokładne i aktualne, mechanizmy przechowujące wyniki nie mogą być stosowane. Są jednak bardzo przydatne dla relatywnie niezmiennych danych, które są często pobierane z bazy. Data na przykład raczej nie zmieni się w ciągu 24 godzin, co czyni bieżącą datę idealnym elementem do przechowania.
Podsumowanie ADOdb ADOdb udostępnia wiele innych metod i trików, jednak tematyka ta wykracza poza zakres tej książki. Opisaliśmy te najczęściej używane. Biblioteka ta jest bardzo obszerna — najobszerniejsza ze wszystkich bibliotek, które do tej pory przedstawiliśmy. Wykorzystuje się ją w wielu systemach open source oraz w systemach bazodanowych; jest dobrze opisana i dobrze wspierana.
Wyszukiwanie pełnotekstowe przy wykorzystaniu Sphinksa Przeszukiwanie tekstów z reguły uważa się za zagadnienie niezwiązane z integracją aplikacji z bazami danych, jednak każda z wiodących baz posiada system wyszukiwania pełnotekstowego. Sphinx jest domyślnym systemem wyszukiwania pełnotekstowego dla bazy MySQL. W tej książce pokażemy jednakże, w jaki sposób wykorzystać Sphinksa z bazą PostgreSQL, ponieważ tę bazę mamy pod ręką. Czym więc jest wyszukiwanie pełnotekstowe i do czego jest potrzebne? Większość nowoczesnych baz danych radzi sobie całkiem dobrze z wyrażeniami regularnymi — można by więc pomyśleć, że wyszukiwanie pełnotekstowe nie jest potrzebne. Niestety, wyszukiwania za pomocą wyrażeń regularnych przeważnie nie pozwalają korzystać z indeksów, są więc za mało wydajne, aby mogły być praktyczne. Dlatego powstała technika pozwalająca na tworzenie specjalnych indeksów tekstowych, pomagających w wyszukiwaniu pełnotekstowym. Indeksy tekstowe i towarzyszące im oprogramowanie mają następujące zastosowania: • Wyszukiwanie słów — wyszukiwanie rekordów zawierających dany wyraz, np. „kurczak” lub „sałatka”. • Wyszukiwanie fraz — przydatne dla użytkowników szukających fraz, np. „sałatka z kurczaka”, którzy niekoniecznie chcą otrzymać wyniki typu „skrzydełka kurczaka i sałatka z tuńczyka”, jakie zostałyby zwrócone podczas wyszukiwania według słów. • Wyszukiwanie bliskości — znane także jako „operacje odległości”, pozwalające wyszukać wiersze, w których pole tekstowe zawiera np. słowa „witaj” i „świecie” w odległości nie większej niż trzy słowa. • Wyszukiwanie kworum — podawana jest lista słów, a wiersze pasujące to te, dla których znaleziona zostanie określona liczba słów z listy.
165
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
• Wyszukiwanie z użyciem operatorów logicznych — można łączyć wyszukiwania słów przez wykorzystanie operatorów AND, OR i NOT. Wszystkie współczesne bazy danych posiadają takie możliwości. Oczywiście jest wiele systemów wyszukiwania pełnotekstowego, zarówno typu open source, jak i komercyjnych. Silniki wyszukiwania pełnotekstowego typu open source to: Sphinx, Lucene, Xapian i Tsearch2. Każdy z nich ma swoje wady i zalety. Są także produkty komercyjne, takie jak Oracle*Text lub IDOL firmy Autonomy Corp. Pozostała część tego rozdziału zawiera opis systemu Sphinx, opracowanego przez firmę Sphinx Technologies. Strona tej firmy znajduje się pod adresem http://sphinxsearch.com/. Sphinx jest z reguły dostępny jako pakiet systemu operacyjnego; jego instalacja jest bardzo łatwa. Jeżeli program nie został zainstalowany natywnie, można go skompilować praktycznie w każdym systemie operacyjnym. PHP potrzebuje specjalnego modułu, instalowanego za pośrednictwem programu PECL. Sphinx składa się z dwóch części: indeksera, który buduje żądany indeks tekstowy, oraz procesu wyszukiwania, który uruchamia wyszukiwanie. Oba komponenty są kontrolowane przez plik konfiguracyjny o nazwie sphinx.conf. Pierwszym krokiem jest zbudowanie indeksu. Indekser odczytuje źródło dokumentu i buduje indeks zgodnie z pewnymi zasadami. Źródłem dokumentu może być baza danych lub program tworzący XML („xmlpipe”). Wspierane bazy to PostgreSQL i MySQL. Bazą danych wykorzystaną do zademonstrowania możliwości systemu Sphinx jest system open source PostgreSQL. Tabela użyta do zaindeksowania ma nazwę artykuly i została utworzona z tekstów na temat jedzenia znalezionych za pośrednictwem wyszukiwarki Google. W tabeli tej znajduje się 50 artykułów wraz z ich autorami, adresami URL, pod którymi znaleźliśmy teksty, oraz datą wyszukania tekstów — wszystkie zostały znalezione 30 stycznia 2011, więc kolumna z datą jest trochę nudna, jednak konieczna na potrzeby przykładów prezentowanych w książce. Znaki nowej linii w artykułach zamieniliśmy na — znacznik HTML oznaczający nową linię. Było to zło konieczne, wynikające z metody wykorzystanej do załadowania artykułów do bazy. Wszystkie artykuły zebraliśmy w jednym dużym pliku CSV przy zastosowaniu edytora vi. Wynikowy plik CSV został załadowany do bazy. Tabela 8.1 pokazuje wygląd tabeli z artykułami. Tabela 8.1. Tabela zawierająca artykuły Kolumna
Typ
Modyfikatory
id_dokumentu
bigint
not null
autor
varchar(200)
opublikowany
date
url
varchar(400)
artykul
text
Indeksy: „pk_artykuly” PRIMARY_KEY, btree (id_dokumentu) Pole id_dokumentu jest kluczem głównym i zawiera kolejne numery artykułów. Pole to jest typu bigint, który przechowuje 64-bitowe liczby całkowite. Zbudujmy teraz indeks tekstowy. Indeks budowany jest przez program nazywany indekserem, który jest częścią pakietu Sphinx. Na początek potrzebny jest nam plik konfiguracyjny, który z reguły ma nazwę sphinx.conf. Lokalizacja tego pliku zależna jest od wykorzystywanego systemu operacyjnego. Oto typowy plik konfiguracyjny utworzony na podstawie przykładowego pliku załączonego w pakiecie Sphinx (listing 8.7): Listing 8.7. Plik konfiguracyjny sphinx.conf ################################################### ## data source definition ################################################### source jedzenie { # data source type. mandatory, no default value
166
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Struktura pliku jest prosta. Pierwsza część definiuje źródło danych. Każde źródło ma swoją nazwę — przykładowe źródło zostało nazwane jedzenie. Najpierw definiowana jest baza danych — podane są jej rodzaj,
167
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
nazwa, nazwa użytkownika, hasło i numer portu. Kolejną rzeczą, jaką trzeba określić, jest źródło pochodzenia samych danych. Zapisane zostały dwa zapytania: jedno pobierające dane, drugie pobierające informacje o dokumencie o podanym identyfikatorze. Sphinx oczekuje, że pierwsza kolumna na liście jest kluczem głównym, ponadto oczekuje, że klucz główny będzie liczbą całkowitą — może być 64-bitową liczbą całkowitą. UWAGA. Pierwsza kolumna w zapytaniu, w źródle danych Sphinx, musi być kluczem głównym. Klucz główny musi być liczbą całkowitą. Akceptowane są także 64-bitowe liczby całkowite.
To był powód, dla którego zdefiniowaliśmy identyfikator jako 64-bitową liczbę całkowitą pomimo faktu, że w bazie wpisanych zostało tylko 50 artykułów. Zauważ także, że nie ma potrzeby wybierania wszystkich kolumn z tabeli. Wybranie tylko tych kolumn, które mają być zaindeksowane, pozwala zaoszczędzić czas i miejsce. Następnie możemy zdefiniować opcjonalne atrybuty. Atrybuty nie są kolumnami indeksu, nie mogą być wykorzystane do wyszukiwania tekstowego — mogą być użyte jedynie do sortowania oraz wyszukiwania zakresów. Moglibyśmy zażądać danych z lutego, jednak nie otrzymalibyśmy żadnych rekordów — powodem jest zakres dat w danych testowych. Atrybuty mogą być liczbami lub datami typu timestamp. Typ timestamp jest zdefiniowany jako liczba sekund od rozpoczęcia epoki (1970-01-01). Pola dat nie mogą być wykorzystane bezpośrednio, muszą być zmapowane na format epoki. UWAGA. Pola składające się z kilku linii, takie jak nasze pola SQL, muszą być zakończone znakiem lewego ukośnika — jak w poprzednim przykładzie.
Poniżej przedstawiamy definicję indeksu. Musi ona zawierać nazwę źródła danych, z którego indeks będzie korzystał przy pobieraniu danych, ścieżkę, gdzie będą zapisywane pliki indeksu, oraz zestaw znaków, który ma być użyty. W naszym przykładzie znajduje się także opcjonalny parametr dotyczący wydajności — preopen — który instruuje proces wyszukiwania, aby podczas startu otworzył indeks, zamiast czekać na pierwsze wyszukanie. Dzięki temu pierwsze wyszukiwanie będzie szybsze. Potem następują opcje dotyczące pamięci dla indeksera, programu wykorzystywanego do budowania indeksów tekstowych i procesu wyszukiwania, który wykonuje wyszukiwania. Ważną dla procesu wyszukiwania opcją jest max_matches. Definiuje ona maksymalną liczbę zwracanych elementów. Jeżeli proces wyszukiwania znajdzie więcej wyników, zostanie zwrócona tylko ich część wynikająca z ograniczenia. W PHP jest to maksymalna wielkość tablicy, która może zostać zwrócona przez wyszukiwanie. Nasz plik konfiguracyjny jest gotowy. Zbudujmy indeks: indexer jedzenie-idx Sphinx 1.10-beta (r2420) Copyright (c) 2001-2010, Andrew Aksyonoff Copyright (c) 2008-2010, Sphinx Technologies Inc (http://sphinxsearch.com) using config file '/usr/local/etc/sphinx.conf'... indexing index 'jedzenie-idx'... collected 50 docs, 0.2 MB sorted 0.0 Mhits, 100.0% done total 50 docs, 230431 bytes total 0.038 sec, 5991134 bytes/sec, 1299.98 docs/sec total 3 reads, 0.000 sec, 38.9 kb/call avg, 0.0 msec/call avg total 9 writes, 0.000 sec, 31.6 kb/call avg, 0.0 msec/call avg
Indekser został wywołany z nazwą indeksu jako argumentem, i to wszystko. Jedynym nietrywialnym zadaniem było utworzenie pliku konfiguracyjnego. Sphinx bywa uważany za najszybszy program budujący indeksy. I faktycznie, jest bardzo szybki, a to może mieć znaczenie, kiedy do zaindeksowania jest wiele danych. Gdy indeks zostanie utworzony, można uruchomić proces wyszukiwania wywołaniem polecenia searchd w wierszu poleceń. W systemie Windows dostępne jest menu rozpoczynania procesu wyszukiwania. Jeśli wszystko przebiegło poprawnie, proces rozpocznie się jak poniżej:
168
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Sphinx 1.10-beta (r2420) Copyright (c) 2001-2010, Andrew Aksyonoff Copyright (c) 2008-2010, Sphinx Technologies Inc (http://sphinxsearch.com) using config file '/usr/local/etc/sphinx.conf'... listening on all interfaces, port=9312 precaching index 'jedzenie-idx' precached 1 indexes in 0.001 sec
Teraz możemy przetestować indeks, wykorzystując program wyszukujący. Jest to narzędzie wiersza poleceń, które komunikuje się z procesem wyszukiwania i wywołuje przekazywane wyszukiwania. search "jajko & wino" Sphinx 1.10-beta (r2420) Copyright (c) 2001-2010, Andrew Aksyonoff Copyright (c) 2008-2010, Sphinx Technologies Inc (http://sphinxsearch.com) using config file '/usr/local/etc/sphinx.conf'... index 'jedzenie-idx': query 'jajko & wino': returned 2 matches of 2 total in 0.000 sec displaying matches: 1. document=9, weight=1579, publ_date=Sun Jan 30 00:00:00 2011 2. document=36, weight=1573, publ_date=Sun Jan 30 00:00:00 2011 words: 1. 'jajko': 8 documents, 9 hits 2. 'wino': 20 documents, 65 hits
Wyszukiwane były dokumenty zawierające oba słowa: „jajko” i „wino”. Otrzymaliśmy także dokładne informacje na temat znalezionych dokumentów. Oto kilka ważnych uwag na temat wyszukiwania: • Wyszukanie frazy jajko | wino zwróci wszystkie dokumenty zawierające którekolwiek z podanych słów. Znak | jest operatorem logicznym „lub”. • Wyszukanie frazy jajko & wino zwróci dokumenty zawierające oba podane słowa. Znak & jest logicznym operatorem „i”. • Wyszukanie frazy !jajko, zwróci wszystkie dokumenty niezawierające słowa jajko. Operator ! jest logicznym zaprzeczeniem — operatorem „nie”. Jeżeli zostanie wykorzystany podczas wyszukiwania z wiersza poleceń, pojedyncze apostrofy muszą ujmować słowo, ponieważ wykrzyknik ma także specjalne znaczenie dla powłoki, a znaki wewnątrz pojedynczych apostrofów nie są przez powłokę interpretowane. Odnosi się to jedynie do powłoki Linuksa i Uniksa i nie dotyczy wiersza poleceń systemu Windows. • Wyszukanie frazy "oliwa z oliwek" (cudzysłów jest częścią frazy) zwróci dokumenty, które zawierają całą frazę. • Wyszukanie frazy "sok pomidorowy" ~5 zwróci dokumenty zawierające słowa sok i pomidorowy będące względem siebie w odległości nie większej niż pięć słów. • Wyszukanie frazy "olej ocet pomidor sałata"/3 zwróci dokumenty, które zawierają przynajmniej trzy z wymienionych słów. Są to podstawowe operacje, które mogą być wykorzystane przy konstruowaniu złożonych wyrażeń. Czas napisać skrypt PHP, który przeszuka indeks tekstowy. Ze względu na wielkość i rodzaj wyniku działania skrypt będzie uruchamiany w przeglądarce, co oznacza, że musimy zbudować prosty formularz HTML oraz tabelę HTML zawierającą wyniki wyszukiwania. Zostanie to wykonane przy zastosowaniu dwóch modułów PEAR: HTML_Form i HTML_Table. HTML_Form jest trochę przestarzały, ale za to prosty i łatwy w użyciu. Skrypt pokazano na listingu 8.8. Ten skrypt jest o wiele bardziej zbliżony do skryptów, których oczekuje się od programistów, niż zapytania pisane w wierszu poleceń, pokazane w pozostałej części tego podrozdziału, i łączy ADOdb, proste moduły WEB oraz system Sphinx. Wynik pokazany jest na rysunku 8.1.
169
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Listing 8.8. Przeszukiwanie indeksu tekstowego (skrypt PHP) "rows,cols", "border" => "3", "align" => "center"); $tabela = new HTML_Table($attrs); $tabela->setAutoGrow(true); /* ustawienie nagłówka tabeli */ foreach (range(0, count($naglowki) - 1) as $i) { $tabela->setHeaderContents(0, $i, $naglowki[$i]); } /* pobranie dokumentu z bazy danych */ $zapytanie = "select * from artykuly where id_dokumentu=?"; $wyszukanie = null; if (!empty($_POST['wyszukanie'])) { $wyszukanie = trim($_POST['wyszukanie']); } /* wyświetlenie prostego formularza, składającego się z jednego pola tekstowego */ echo "
Wyszukiwanie Sphinx
"; $formularz = new HTML_Form($_SERVER['PHP_SELF'], "POST"); $formularz->addTextarea("wyszukanie", 'Szukana fraza:', $wyszukanie, 65, 12); $formularz->addSubmit("submit", "Szukaj"); $formularz->display(); /* stop - nie ma czego szukać */ if (empty($wyszukanie)) exit; try { $db->Connect("localhost", "postgres", "test", "mgogala"); $stmt = $db->Prepare($zapytanie); /* połączenie z procesem "searchd" */ $cl = new SphinxClient(); $cl->SetServer("localhost", 9312); /* ustawienie trybu rozszerzonego wyszukiwania */ $cl->SetMatchMode(SPH_MATCH_EXTENDED2); /* rezultaty zostaną posortowane według daty */ $cl->SetSortMode(SPH_SORT_ATTR_DESC, "data_publikacji"); /* uruchomienie wyszukiwania i sprawdzenie problemów */ $wynik = $cl->Query($wyszukanie); if ($wynik === false) { throw new Exception($cl->GetLastError()); } else { if ($cl->GetLastWarning()) {
170
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Rysunek 8.1. Wynik działania skryptu z listingu 8.8 echo "UWAGA: " . $cl->GetLastWarning() . " "; } } /* pobranie wyników i wykorzystanie ich w zapytaniu do bazy */ foreach ($wynik["matches"] as $doc => $docinfo) { $rs = $db->Execute($stmt, array($doc)); $wiersz = $rs->FetchRow(); /* dodanie rezultatu zapytania do tabeli wynikowej */ $tabela->addRow($wiersz); } /* wyświetlenie rezultatów */ echo $tabela->toHTML(); } catch(Exception $e) { die($e->getMessage()); }
Formularz wykorzystywany jest do wpisywania warunków wyszukiwania. Gdy zostaną wpisane, skrypt łączy się z bazą danych i systemem Sphinx — pobiera dane poprzez wywołanie $wynik=$cl->Query($wyszukanie). Sphinx przetworzy warunki wyszukiwania i zwróci dane. Wynikiem jest następująca tablica asocjacyjna: Array ( [error] => [warning] => [status] => 0 [fields] => Array ( [0] => artykul ) [attrs] => Array ( [publ_date] => 2 ) [matches] => Array (
171
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Wyrazy pasujące do naszych warunków wyszukiwania zostały znalezione w dokumencie o identyfikatorze 13. Wyrażenie, którego szukaliśmy, miało postać stan & zdrowia & jemy & odporność; szukaliśmy artykułów zawierających wszystkie te słowa. Pasujące rekordy zostały umieszczone w tablicy $wynik["matches"], w której znajdują się także informacje o objętości dokumentu. Objętość wyliczana jest na podstawie funkcji statystycznej „BM25”, uwzględniającej częstotliwość występowania słów. Im większa jest objętość dokumentu, tym lepsze jest dopasowanie. Sam artykuł nie jest pobierany wraz z danymi ze Sphinksa. Aby uzyskać wiersz z dokumentem 13, musimy odwołać się do bazy danych. Może to być niewygodne, jednak duplikowanie danych z bazy w indeksie byłoby marnowaniem miejsca. Nie ma to znaczenia przy pięćdziesięciu rekordach, jednak kiedy rekordów są miliony, duplikowanie danych może być bardzo kosztowne. Przecież mamy dane już w bazie, nie ma potrzeby przechowywania ich także w indeksie. Sphinx jest wszechstronnym narzędziem. Udostępnia indeksowanie w czasie rzeczywistym, ma składnię zbliżoną do składni SQL-a, może tworzyć indeksy zgrupowane — co oznacza, że jeden indeks wskazuje inne indeksy — wspiera UTF-8 i współpracuje z wieloma bazami danych. Składnia wyszukiwania jest bardzo elastyczna. Sphinx jest wbudowany w bazę MySQL, ale może, jak pokazano w tym rozdziale, współpracować z innymi bazami, emulować MySQL i być wykorzystany do połączeń z rozszerzeniem MySQL dla PHP i sterownikiem ODBC dla MySQL, a nawet z klientem MySQL dostępnym z wiersza poleceń. Dodatkowo klient PHP do komunikacji ze Sphinksem jest często aktualizowany i dobrze udokumentowany.
172
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
Podsumowanie W tym rozdziale zostały omówione: • MySQL • PDO • ADOdb • Sphinx MySQL jest pełnoprawną bazą relacyjną, na której temat napisano już wiele książek. Wszystkie podrozdziały tego rozdziału, oprócz ostatniego, dotyczą MySQL. Nie był tutaj istotny typ bazy danych. PDO, ADOdb i Sphinx można by równie dobrze zademonstrować dla SQLite, PostgreSQL, Oracle czy DB2. Skrypty wyglądałyby dokładnie tak samo. W przypadku Sphinksa potrzebowalibyśmy skryptu odczytującego z bazy i zapisującego w niej pliki XML dla baz innych niż MySQL i PostgreSQL, nie jest to jednak duży problem. Moglibyśmy wykorzystać do tego celu ADOdb. Ten rozdział nie jest pełnym omówieniem tych zagadnień; stanowi jedynie wprowadzenie. Nie omówiono w nim wszystkich opcji i możliwości przedstawionych bibliotek i pakietów oprogramowania.
173
ROZDZIAŁ 8. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ II
174
ROZDZIAŁ 9
Integracja z bazami danych. Część III Do tej pory pracowaliśmy głównie z bazą MySQL. Czas przedstawić bazę Oracle i jej możliwości. Oracle jest obecnie najpopularniejszym systemem zarządzania bazą danych (RDBMS) na rynku. Jest najczęściej spotykanym systemem w serwerowniach — przynajmniej w tych z górnej półki. W tym rozdziale przedstawimy RDBMS Oracle oraz interfejs PHP OCI8 (łączenie z bazą, wykonywanie zapytań i przypisywanie wartości). Omówimy także interfejs tablicowy, procedury PL/SQL, argumenty wejścia-wyjścia i przypisywanie kursorów. Następnie przejdziemy do dużych obiektów oraz sposobu obchodzenia się z kolumnami LOB. Na końcu przyjrzymy się pulom połączeń. Oracle ma bardzo duże możliwości. Pełny ich opis mógłby stanowić małą bibliotekę. Rozpoczniemy od nakreślenia najistotniejszych cech Oracle z perspektywy programisty PHP.
Wprowadzenie do Oracle Baza Oracle jest bazą relacyjną w pełnym tego słowa znaczeniu i jest zgodna z właściwościami ACID opisanymi w rozdziale 7. Wspiera wersjonowanie oraz spójność danych w taki sposób, że użytkownicy odczytujący dane nigdy nie blokują użytkowników je zapisujących. To oznacza, że procesy wykonujące zapytania na danej tabeli nie zablokują procesów, które tę tabelę modyfikują, ani nie zostaną przez te procesy zablokowane. W przeciwieństwie do wielu innych baz danych Oracle posiada zcentralizowany słownik i nie pojawia się w nim sformułowanie baza danych. Instancja Oracle, która jest kolekcją procesów i współdzielonej pamięci, zawsze odnosi się do jednej bazy. Sesje łączą się z instancją poprzez podłączanie się do procesów serwerowych. To połączenie może być dedykowane i w tym przypadku proces serwerowy jest dedykowany dla jednego klienta, który się do niego podłączył. Może także być współdzielone, a proces będzie wykorzystywany przez wiele połączeń klienckich. W wersji 11g i późniejszych można łączyć się z pulą — w tym przypadku istnieje pula procesów i każdy z tych procesów może w każdej chwili obsłużyć dane połączenie. Sesja Oracle jest kosztownym obiektem. Liczba tych obiektów jest ograniczona przez parametr podczas inicjalizacji instancji; nie można ich tworzyć lekkomyślnie. W przeciwieństwie do niektórych innych baz danych, przede wszystkim Microsoft SQL Server, wielokrotne ustanawianie połączenia z bazą dla jednego klienta jest uznawane za złą praktykę. Baza Oracle jest ogólną encją, podzieloną na przestrzenie tabel (tablespace). Przestrzeń tabel jest po prostu zbiorem plików — fizycznym miejscem przechowywania obiektów. Właścicielem każdego obiektu w bazie, np. tabeli lub indeksu, jest jakiś użytkownik. W kontekście Oracle pojęcia użytkownika i schematu są synonimami. To oznacza, że do każdego schematu, zdefiniowanego w standardzie ANSI SQL jako logiczna kolekcja obiektów, przypisana jest nazwa użytkownika. Powoduje to powstanie dużej liczby użytkowników, ale nie jest to czymś złym. Oracle wspiera także globalne tabele tymczasowe. Dane w globalnej tabeli tymczasowej są utrzymywane tylko przez czas transakcji lub sesji. Nazywane są globalnymi tabelami tymczasowymi, ponieważ ich widoczność jest globalna. Istnieją nawet wtedy, kiedy już wszystkie sesje korzystające z nich się rozłączą. Oracle nie wspiera
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
lokalnych tabel tymczasowych (takich jak w SQL Server czy PostgreSQL), istniejących tylko na czas trwania sesji, w której zostały utworzone. W zamian wspiera kursory, jednak nie są one tak wszechstronne, jak lokalne tabele tymczasowe. Może to powodować problemy z przenośnością rozwiązań — w szczególności przy przenoszeniu baz z SQL Server do Oracle. Wiele innych baz danych, takich jak DB2, SQL Server, MySQL i PostgreSQL, wspiera lokalne tabele tymczasowe, które istnieją tylko na czas sesji lub nawet transakcji. Programiści korzystający z tych baz często używają takich tabel — gdyby dokładnie te same zapytania zostały zastosowane w Oracle, mogłoby to skutkować dużą liczbą niepotrzebnych obiektów pozostających w bazie. Jeżeli jest to możliwe, w takich przypadkach tabele tymczasowe zastępowane są kursorami. Oracle wspiera także obiekty zwane synonimami, które mogą wskazywać na inny schemat lub nawet inną bazę danych. Oracle jest bazą w pełni rozproszoną, pozwala na zapytania do zdalnych baz, a nawet tworzenie transakcji obejmujących kilka baz danych. Należy jednak ostrożnie korzystać z tej możliwości, ponieważ bazy rozproszone mają nietypowe i niespodziewane właściwości, które mogą w znacznym stopniu wpłynąć na aplikację. Blokowanie danych na poziomie wiersza zostało wprowadzone w trosce o zwiększenie współbieżności; domyślnie blokowaną jednostką jest wiersz. Blokowanie w Oracle zaimplementowane jest w dość unikalny sposób — bez globalnej kolejki blokowania oraz dużego zapotrzebowania na pamięć. Dzięki temu blokowanie w Oracle nie jest kosztowne. W gruncie rzeczy koszt zablokowania wiersza w tabeli jest zwykle taki sam jak koszt zablokowania wielu wierszy. Oracle nie eskaluje blokad — blokada wiersza nie zostanie nigdy przekształcona w blokadę tabeli. Jawne blokowanie tabel w Oracle jest na ogół mało wydajne i może mieć bardzo negatywny wpływ na wydajność i współbieżność aplikacji. Podobnie jak w przypadku wielu innych systemów baz danych, Oracle ma swój język transakcyjny lub rozwinięcie proceduralne — PL/SQL. Jest to w pełni zdefiniowany język programistyczny, oparty na języku Ada. Można go użyć do tworzenia funkcji, procedur, wyzwalaczy oraz pakietów. Oprócz PL/SQL do tworzenia procedur można wykorzystać język Java. Wirtualna maszyna Javy jest częścią jądra systemu Oracle. Ta możliwość jest bardzo ważna, ponieważ czysty SQL nie jest wystarczający podczas definiowania reguł biznesowych. Reguły biznesowe są przeważnie implementowane jako wyzwalacze, co pomaga w zachowaniu spójności we wszystkich aplikacjach odwołujących się do bazy. Istnieją dwa podejścia do implementowania reguł biznesowych. Pierwsze z nich skupione jest na bazie danych, drugie na aplikacji. Uważamy, że reguły biznesowe powinny być implementowane w bazie danych, ponieważ utrzymywanie spójnej implementacji reguł biznesowych w warstwie aplikacji jest trudne i ryzykowne. Pojawia się wtedy wiele możliwości popełnienia błędu. Nieznaczne różnice powstałe w wyniku nieporozumień są bardzo prawdopodobne podczas cyklu życia modelu danych — może to powodować logiczne niespójności. Trzeba jeszcze wspomnieć o klastrach aplikacji (RAC). Oracle wspiera dzielone klastry dyskowe, które są o wiele bardziej złożone niż osobne bazy danych, z reguły nazywane nic niedzielącą architekturą. W przypadku RAC Oracle wiele instancji bazy może uzyskać dostęp do tej samej bazy ulokowanej na współdzielonym dysku. Ilustruje to rysunek 9.1.
Rysunek 9.1. Oracle RAC — kilka instancji Oracle może odwoływać się do jednej bazy danych przechowywanej na współdzielonym zasobie
176
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Instancje serwerów 1 i 2 jednocześnie uzyskują dostęp do bazy danych przechowywanej na współdzielonej macierzy. Jest to o wiele bardziej skomplikowane niż klastry niedzielące zasobów, ponieważ blokowanie musi być wykonywane pomiędzy węzłami — potrzebny jest złożony menedżer blokowania (Distributed Lock Manager — DLM). Plusem jest to, że utrata jednego z węzłów nie powoduje utraty danych. W architekturze niedzielącej zasobów utrata węzła oznacza, że użytkownicy nie będą mieli dostępu do danych zarządzanych przez ten serwer. RAC jest bardziej złożony, ale pozwala na balansowanie obciążenia oraz przejmowanie zadań przez działające maszyny, co oznacza, że cała baza jest dostępna tak długo, jak długo dostępny jest chociaż jeden z serwerów. Istnieje wiele innych możliwości serwerów baz danych Oracle, których nie będziemy omawiać w tej książce, ale które warto poznać. Oracle publicznie udostępnia przydatne informacje pod adresem http://www.oracle.com/technetwork/indexes/documentation/index.html. Polecamy wiadomości dotyczące ogólnej koncepcji serwera baz danych Oracle. Dokładniejsze informacje można znaleźć w książkach Toma Kyte’a, w szczególności w Expert Database Architecture (Apress 2010). Tom Kyte, wiceprezes Oracle, jest doskonałym autorem, ma ogromną wiedzę i jego książki czyta się naprawdę przyjemnie. Baza Oracle jest bardzo popularną bazą relacyjną, wyposażoną w wiele opcji; podąża za standardami. Nie można jednak zapędzić się w pułapkę tworzenia oprogramowania niezależnego od systemu bazodanowego. Bazy danych są złożonym oprogramowaniem z wieloma różnymi implementacjami tych samych funkcjonalności. Tworzenie oprogramowania dedykowanego dla jednego systemu pozwala na uzyskanie maksymalnie wysokiej wydajności dla danego oprogramowania i sprzętu. Podczas tworzenia oprogramowania korzystającego z bazy Oracle należy zapoznać się ze standardami Oracle, a nie z ogólnymi standardami dotyczącymi wszystkich baz danych. Niezależność od silnika bazodanowego oznacza z reguły, że aplikacja będzie działała tak samo wolno dla wszystkich baz danych, które wspiera — co nie będzie satysfakcjonującym rezultatem. Z drugiej strony tworzenie aplikacji bez jakiejkolwiek możliwości przeniesienia na inny system może spowodować uzależnienie się od sprzedawcy, a w efekcie wzrost ceny aplikacji. Zobaczmy teraz szczegóły interfejsu OCI8. Zakładamy, że moduł OCI8 jest zainstalowany albo poprzez bezpośrednie linkowanie, albo poprzez moduł PECL.
Podstawy. Połączenie i wykonywanie zapytań Rozszerzenie OCI8 posiada wszystkie metody, z którymi się zetknęliśmy podczas pracy z rozszerzeniami MySQL i SQLite. Są to metody do połączenia z instancją Oracle, przygotowania zapytania, wywołania go i pobrania wyników. Niestety, rozszerzenie OCI8 jest z natury proceduralne, co oznacza, że sprawdzanie wystąpienia błędów musi być wykonywane ręcznie. Aby zautomatyzować sprawdzanie błędów, można wykorzystać wrapper ADOdb, który ma wiele (ale nie wszystkie) opcji oferowanych przez rozszerzenie OCI8. Zgodnie z naszym dotychczasowym podejściem przykład zastąpi tysiąc słów. Podobnie jak w przypadku poprzednio omawianych baz, pokazane zostaną dwa skrypty: pierwszy — wczytujący dane z plików CSV do bazy danych, drugi — wykonujący zapytanie. Oba skrypty są wykonywane z wiersza poleceń. Na przykładzie tych dwóch skryptów będziemy mogli przeanalizować większość metod potrzebnych do współpracy z bazą danych Oracle, tak jak było w przypadku MySQL i SQLite. Listing 9.1 przedstawia pierwszy skrypt, który wczyta pliki CSV do bazy; przyjmuje on connection string, nazwę tabeli oraz nazwę pliku, a następnie jako argumenty wiersza poleceń wstawia dane z pliku do wskazanej tabeli. Nie zakładamy żadnego szczególnego schematu ani struktury tabel. Listing 9.1. Skrypt ładujący dane z pliku CSV \n"); } $polaczenie = $argv[1]; $nazwaTabeli = $argv[2]; $nazwaPliku = $argv[3]; $zapytanie = "select * from $nazwaTabeli"; $dsn = array();
177
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
$liczbaWierszy = 0; if (preg_match('/(.*)\/(.*)@(.*)/', $polaczenie, $dsn)) { $polaczenie = array_shift($dsn); } elseif (preg_match('/(.*)\/(.*)/', $polaczenie, $dsn)) { $polaczenie = array_shift($dsn); } else die("Identyfikator połączenia powinien mieć formę u/h@baza."); if (count($dsn) == 2) { $dsn[2] = ""; } function utworz_polecenie_insert($tabela, $liczbaKolumn) { $zapytanie = "insert into $tabela values("; foreach (range(1, $liczbaKolumn) as $i) { $zapytanie.= ":$i,"; } $zapytanie = preg_replace("/,$/", ')', $zapytanie); return ($zapytanie); } try { $dbh = oci_connect($dsn[0], $dsn[1], $dsn[2]); if (!$dbh) { $err = oci_error(); throw new exception($err['message']); } $res = oci_parse($dbh, $zapytanie); // Oracle musi wykonać polecenie, zanim funkcje opisujące będą dostępne. // Dostępny jest jednak specjalny tryb wywoływania poleceń, // dzięki któremu nie ma problemów z wydajnością. if (!oci_execute($res, OCI_DESCRIBE_ONLY)) { $err = oci_error($dbh); throw new exception($err['message']); } $liczbaKolumn = oci_num_fields($res); oci_free_statement($res); $ins = utworz_polecenie_insert($nazwaTabeli, $liczbaKolumn); $res = oci_parse($dbh, $ins); $fp = new SplFileObject($nazwaPliku, "r"); while ($wiersz = $fp->fgetcsv()) { if (count($wiersz) < $liczbaKolumn) continue; foreach (range(1, $liczbaKolumn) as $i) { oci_bind_by_name($res, ":$i", $wiersz[$i - 1]); } if (!oci_execute($res,OCI_NO_AUTO_COMMIT)) { $err = oci_error($dbh); throw new exception($err['message']); } $liczbaWierszy++; } oci_commit($dbh); print "Do tabeli $nazwaTabeli wstawiono $liczbaWierszy wierszy.\n"; } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
178
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Uzyskamy wynik taki jak w przypadku innych baz: ./listing9_1.php test/test prac emp.csv Do tabeli prac wstawiono 14 wierszy.
Pliki CSV są takie same jak dla SQLite z rozdziału 7. Rozszerzenie OCI8 jest bardziej nieporęczne niż elegancka wersja ADOdb, jednak wykorzystanie go pozytywnie wpływa na wydajność, co pokażemy w kolejnym podrozdziale. Poszczególne polecenia powinny być już rozpoznawalne. oci_connect wykorzystywane jest oczywiście do połączeń z instancją bazą danych. Connection string dla Oracle przyjmuje z reguły postać nazwa_użytkownika/hasło@baza, czasami bez ostatniej części, dlatego konieczne było sparsowanie tego argumentu. Jest to coś, co funkcja preg_match może zrobić w bardzo elegancki sposób. Szczegóły wyrażeń regularnych omówimy dalej. Wywołanie oci_error służy do wykrywania błędów, oci_parse parsuje zapytanie, a oci_execute je wykonuje. Podczas wykrywania błędów funkcja oci_error przyjmuje uchwyt do bazy jako jedyny parametr; zwraca ostatni napotkany błąd. Wywołanie oci_execute, które wstawia dane do bazy, wykonywane jest z dodatkowym parametrem OCI_NO_AUTO_COMMIT. Bez tego parametru po wstawieniu każdego wiersza następowałoby zatwierdzenie transakcji. Jak wspominaliśmy w rozdziale 7., w części poświęconej MySQL, zatwierdzanie jest bardzo kosztowne. Nie tylko pogorszylibyśmy wydajność, ale możliwe byłoby także niepełne wstawienie. Część wierszy zostałaby wczytywana, a część nie — konieczne byłoby ręczne sprzątanie po takiej operacji. Domyślnie po każdym wstawieniu nastąpiłoby zatwierdzenie operacji. Liczba kolumn jest zwracana przez funkcję oci_num_fields, która jako parametr przyjmuje uchwyt do wykonanego zapytania. Byłoby to niepraktyczne przy dużych tabelach, w związku z tym dostępny jest specjalny tryb wykonywania zapytań, który nie zwraca wyniku, a więc nie powoduje zmniejszenia wydajności. Dodatkowo faktyczne przetwarzanie zapytania jest zazwyczaj opóźnione aż do chwili jego wykonania, aby zmniejszyć komunikację sieciową. Oznacza to, że po wywołaniu oci_parse nie ma potrzeby sprawdzania wystąpienia błędów, sprawdzanie błędów należy wykonywać po wywołaniu funkcji oci_execute. Pewien spadek wydajności spowodowany jest jednak sposobem wykonywania tego skryptu. Dla każdego wiersza sprawdzamy rezultaty jego wykonania. Jeżeli baza znajduje się na innej maszynie niż ta, na której wykonywany jest skrypt PHP, wykonanych zostanie tyle odwołań za pośrednictwem sieci, ile jest wierszy do wstawienia. Nawet przy szybkim połączeniu podczas wstawiania dużych ilości wierszy opóźnienie sieciowe może wpływać na wydajność. Niestety, tak jak kilka innych języków, PHP nie wspiera bezpośrednich przypisań tablic do zapytań. Można na szczęście wykorzystać sztuczkę z zastosowaniem klasy OCI-Collection. Będzie ona opisana w kolejnym podrozdziale. Podstawową funkcją w skrypcie z listingu 9.1, której nie omówiliśmy, jest oci_fetch_row. Zostanie ona pokazana na listingu 9.2 — podobne skrypty przedstawialiśmy już w poprzednich rozdziałach dotyczących baz danych. Skrypt wykonuje zapytanie, pobiera dane wynikowe i wyświetla standardowy raport. Listing 9.2. Skrypt wykonujący zapytanie
179
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Funkcja oci_fetch_array pobierze kolejny wiersz do tego typu tablicowego, który zostanie wybrany przez programistę. Wybraliśmy tablicę indeksowaną liczbami poprzez przekazanie parametru OCI_NUM. Mogliśmy także przekazać jako parametr zmienną OCI_ASSOC, aby zwrócona została tablica asocjacyjna, lub OCI_BOTH, aby zwrócone zostały obie tablice. Podobnie jak wstawianie danych, pobieranie danych odbywa się wiersz po wierszu. Na szczęście przy zapytaniach dostępna jest bardzo prosta sztuczka, która może nam pomóc. OCI8 udostępnia funkcję oci_set_prefetch, która ma następującą składnię: bool oci_set_prefetch($zapytanie,$liczbaWierszy);
To utworzy bufor utrzymywany i wykorzystywany przez Oracle, mogący przechować liczbę wierszy zgodną z wartością zmiennej $liczbaWierszy. Zachowanie funkcji pobierających dane nie ulegnie zmianie, jednak prędkość znacznie się poprawi. Bufor tworzony jest przez zapytanie i nie może być współdzielony ani ponownie wykorzystany. Skrypty z listingów 9.1 i 9.2 przedstawiają podstawy: w jaki sposób nawiązać połączenie z bazą Oracle oraz jak wykonać zapytanie i pobrać wyniki. Jest jeszcze kilka funkcji zaliczanych do kategorii podstawowych. Są to funkcje opisujące pola w zestawie wynikowym: oci_field_name, oci_field_type, oci_field_size, oci_field_precision i oci_field_scale. Wszystkie one przyjmują jako argumenty wykonane polecenie i numer pola; w rezultacie otrzymamy żądane dane: nazwę, typ, rozmiar, precyzję i skalę.
Interfejs tablicowy Teraz zademonstrujemy, jak łatwo można wstawić dużą liczbę wierszy do bazy Oracle w akceptowalnym czasie. Wczytywanie dużych ilości danych jest dość częstym działaniem w pracy z nowoczesnymi bazami korporacyjnymi. Utwórzmy więc następującą tabelę i spróbujmy wczytać do niej duży plik z danymi: SQL> create table test_ins ( 2 kol1 number(10) 3 ) storage (initial 100M); Table created.
Polecenie storage alokuje dla tabeli 100M. Dzięki temu unikniemy dynamicznej alokacji pamięci, co byłoby najgorszą rzeczą, jaka mogłaby nas spotkać podczas wczytywania danych. Dynamiczna alokacja jest bardzo wolna, może powodować problemy ze współbieżnością i należy jej unikać. Potrzebujemy teraz danych do wstawienia: php -r "for($i=0;$i<10000123;$i++) { print $i.\"\n\"; }">plik.dat
Powyższe polecenie spowoduje wygenerowanie ponad 10 milionów wierszy. Zobaczmy najpierw, jak zadziała wstawianie wykonane za pomocą metody z poprzedniego podrozdziału. Listing 9.3 przedstawia bardzo prosty skrypt, który otwiera plik i wczytuje jego zawartość do tabeli utworzonej przez nas przed chwilą. Listing 9.3. Prosty skrypt wczytujący do bazy dane z pliku ");
180
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Ten prosty skrypt napisany jest zgodnie z najlepszymi zasadami programowania. Przypisywanie zastosowane zostało tylko raz, a zatwierdzanie zmian wykonywane jest w interwałach zdefiniowanych jako parametr wiersza poleceń. Koncepcję przypisywania zmiennej do pola omówiliśmy w poprzednim rozdziale. Wykonajmy więc skrypt i zobaczmy wynik: time ./listing9_3.php 10000 Wielkość paczki:10000 Wstawiono 10000123 wierszy. real 16m44.110s user 2m35.295s sys 1m38.790s
Dla 10 milionów prostych rekordów potrzebowaliśmy zatem 16 minut na serwerze lokalnym. Wstawianie było bardzo wolne. Głównym problemem jest to, że poprzedni skrypt komunikuje się z serwerem wiersz po wierszu, sprawdzając rezultat za każdym razem. Rzadsze zatwierdzanie, tak jak w tym przypadku, co 10 000 wierszy, pomaga, ale jest niewystarczające. Aby przyspieszyć wczytywanie, potrzebujemy dodatkowych obiektów w bazie: SQL> create type tabela_numerow as table of number(10); 2 / Type created. SQL> create or replace procedure wstawiaj(in_tab tabela_numerow) 2 as 3 begin 4 forall i in in_tab.first..in_tab.last 5 insert into test_ins values (in_tab(i));
181
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
6 end; 7 / Procedure created.
Utworzyliśmy procedurę przyjmującą jako parametr tabelę PL/SQL będącą typem w Oracle (można go porównać z tablicą w PHP) — jest to typ, bez którego ta procedura nie mogłaby powstać. Wstawia ona zawartość tabeli PL/SQL do tabeli TEST_INS poprzez mechanizm bulk insert. Kiedy mamy już potrzebną infrastrukturę, możemy utworzyć skrypt wczytujący dane — listing 9.4 przedstawia nową wersję skryptu z listingu 9.3. Listing 9.4. Nowa wersja skryptu z listingu 9.3 "); } $paczka = $argv[1]; print "Wielkość paczki:$paczka\n"; $liczbaWierszy = 0; $ins = <<<'EOS' begin wstawiaj(:WARTOSC); end; EOS; try { $dbh = oci_connect("test", "test", "localhost"); if (!$dbh) { $err = oci_error(); throw new exception($err['message']); } $wartosci = oci_new_collection($dbh, 'TABELA_NUMEROW'); $res = oci_parse($dbh, $ins); oci_bind_by_name($res, ":WARTOSC", $wartosci, -1, SQLT_NTY); $fp = new SplFileObject("plik.dat", "r"); while ($wiersz = $fp->fgets()) { $wartosci->append(trim($wiersz)); if ((++$liczbaWierszy) % $paczka == 0) { if (!oci_execute($res)) { $err = oci_error($dbh); throw new exception($err['message']); } $wartosci->trim($paczka); } } if (!oci_execute($res)) { $err = oci_error($dbh); throw new exception($err['message']); } print "Wstawiono $liczbaWierszy wierszy.\n"; } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Zobaczmy, jak wygląda czas wykonywania w porównaniu ze skryptem z listingu 9.3. Skrypt z listingu 9.4 jest nieco bardziej złożony, ponieważ potrzebował dodatkowych obiektów w bazie, jednak wysiłek zdecydowanie się opłacił:
182
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
time ./listing9_4.php 10000 Wielkość paczki:10000 Wstawiono 10000123 wierszy. real 0m58.077s user 0m42.317s sys 0m0.307s
Czas wczytywania 10 milionów rekordów zmniejszył się z ponad 16 minut do 58 sekund. Skąd ten wzrost wydajności? Po pierwsze, po stronie PHP utworzyliśmy obiekt OCI-Collection, który zawierał kolekcję wierszy do wstawienia. Kolekcje Oracle mają wszystkie metody, których moglibyśmy potrzebować: append, trim, size oraz getElem. Metoda append doda zmienną do kolekcji, trim usunie podaną liczbę elementów z kolekcji, size zwróci liczbę elementów w kolekcji, a getElem zwróci element o zadanym indeksie. Gdyby tabela miała więcej kolumn, potrzebowalibyśmy obiektu kolekcji dla każdej kolumny oraz typu dla każdego obiektu. Skrypt zbiera 10 000 wierszy w obiekcie kolekcji i dopiero wtedy przekazuje je do bazy danych, stąd nazwa „interfejs tablicowy”. Po drugie, mechanizm wykonuje wstawienie grupowe, które jest znacznie szybsze niż wstawianie pojedynczych wierszy w pętli. Gdyby baza docelowa była na innej maszynie, nawet z szybkim, jednogigabajtowym połączeniem, wykonanie pierwszego skryptu trwałoby do 45 minut. Drugi skrypt nadal wykonywałby się w czasie poniżej dwóch minut — dzięki znacznie obniżonej liczbie odwołań do sieci. Oba skrypty zatwierdzają zmiany w tych samych odstępach. W skrypcie z listingu 9.3 funkcja oci_execute wywoływana jest z parametrem OCI_NO_AUTO_COMMIT, a zatwierdzanie wykonywane jest co 10 000 wierszy poprzez wywołanie funkcji oci_commit. W skrypcie z listingu 9.4 funkcja oci_execute została wywołana bez wyłączania automatycznego zatwierdzania, co oznacza, że dane były zatwierdzane po każdym wstawieniu. Tego skryptu nie da się przepisać przy wykorzystaniu ADOdb lub PDO, ponieważ nie wspierają one typu OCI-Collection. Skrypty wczytujące duże ilości danych do hurtowni danych najlepiej tworzyć przy użyciu oryginalnego interfejsu OCI8. Czy są jakieś problemy ze skryptem z listingu 9.4? Przede wszystkim ignoruje błędy. Należy je obsłużyć w procedurze wstawianie. Obsługa błędów została pominięta ze względu na przejrzystość przykładu. Polecenie FORALL w PL/SQL posiada opcję SAVE EXCEPTIONS, która może być wykorzystana do sprawdzenia błędów w każdym wierszu, dla którego będzie zgłoszony wyjątek. PL/SQL jest bardzo rozbudowanym językiem i ma o wiele więcej zastosowań niż te, które omówiliśmy. Dokumentacja Oracle, zawierająca doskonały opis języka PL/SQL, jest dostępna na stronie wspomnianej wcześniej w rozdziale. Kolejny podrozdział traktuje także o PL/SQL.
Procedury i kursory w PL/SQL W poprzednim podrozdziale przedstawiliśmy sposób przypisywania danych do zapytań PL/SQL. Wartości muszą być przypisywane do specjalnych pól przygotowanych wewnątrz zapytania PL/SQL (listing 9.5). Listing 9.5. Przypisywanie zmiennych do pól w zapytaniach PL/SQL
183
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
if (!$dbh) { $err = oci_error(); throw new exception($err['message']); } $res = oci_parse($dbh, $proc); oci_bind_by_name($res,":DNI",&$dni,20,SQLT_CHR); oci_bind_by_name($res,":DAWNO_TEMU",&$dawno_temu,128,SQLT_CHR); oci_bind_by_name($res,":LINIA",&$linia,128,SQLT_CHR); if (!oci_execute($res)) { $err=oci_error($dbh); throw new exception($err['message']); } print "Oto linia zwrócona przez procedurę: $linia\n"; } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Po wykonaniu skrypt zwraca następującą linię: ./listing9_5.php Oto linia zwrócona przez procedurę: Dawno, dawno temu:2011-09-27 13:21:13
Funkcja dni_temu jest raczej prostą funkcją i wygląda następująco: CREATE OR REPLACE FUNCTION dni_temu( dni IN NUMBER) RETURN VARCHAR2 AS BEGIN RETURN(TO_CHAR(sysdate-days,'YYYY-MM-DD HH24:MI:SS')); END;
W niewielkim skrypcie z listingu 9.5 zawarto wiele interesujących elementów; są to: funkcja utworzona przez użytkownika, przyjmująca jeden argument, wykorzystanie pakietu systemowego DBMS_OUTPUT oraz argumentów wyjściowych — wszystko połączone w anonimowym kodzie PL/SQL. Przypisywane zmienne muszą być zadeklarowane przez wywołanie funkcji oci_bind_by_name. Nie ma potrzeby deklarowania parametrów wejściowych i wyjściowych, jak w niektórych interfejsach — funkcja oci_bind_by_name wykona to za nas. Przypisywane zmienne mogą być różnych typów. Oczywiście mogą być liczbami, znakami alfanumerycznymi i jak zauważyliśmy wcześniej, mogą także być typu OCI-Collection. Możliwe jest również przypisanie uchwytu do zapytania. W terminologii Oracle uchwyt do zapytania nazywany jest kursorem (cursor). Język PL/SQL radzi sobie bardzo dobrze z operacjami na kursorach i może przekazywać jego obsługę do PHP. Listing 9.6 przedstawia przykład. Listing 9.6. Przykład wykorzystania kursora PL/SQL
184
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
if (!$dbh) { $err = oci_error(); throw new exception($err['message']); } $krs = oci_new_cursor($dbh); $res = oci_parse($dbh, $proc); oci_bind_by_name($res, ":KRS", $krs, -1, SQLT_RSET); if (!oci_execute($res)) { $err = oci_error($dbh); throw new exception($err['message']); } if (!oci_execute($krs)) { $err = oci_error($dbh); throw new exception($err['message']); } while ($wiersz = oci_fetch_array($krs, OCI_NUM)) { foreach ($wiersz as $r) { printf("%-12s", $r); } print "\n"; } } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
Wywołanie funkcji oci_execute ma miejsce w tym skrypcie dwukrotnie. Za pierwszym razem wykonujemy skrypt ze zmiennej $proc. Skrypt otwiera kursor typu ref cursor dla zapytania SQL wybierającego trzy kolumny z tabeli PRAC, przypisuje kursor do zmiennej przypisanej do pola :KRS i kończy pracę. Cała reszta to już PHP. Podczas wykonywania kodu PL/SQL kursor przypisywany jest do zmiennej $krs, która została utworzona przez wywołanie funkcji oci_new_cursor. Kursory są, jak już wcześniej powiedziano, sparsowanymi zapytaniami SQL. Kiedy $krs został już zasilony, musi być wykonany, a dane muszą być pobrane. Drugie wywołanie oci_execute posłużyło do wykonania kursora. Następnie dane zostały pobrane i wyświetlone. Rezultat wygląda tak: ./listing9_6.php KOWALSKI SPRZEDAWCA NOWAK SPRZEDAWCA BONIEK SPRZEDAWCA DOBROWOLSKI MENEDZER MATUSZCZYK SPRZEDAWCA CIBORSKI MENEDZER KOS MENEDZER KACZMAREK ANALITYK KROL PREZES JANKIEWICZ SPRZEDAWCA DORUCH SPRZEDAWCA MIREK SPRZEDAWCA KOWALEWSKI ANALITYK KOREK SPRZEDAWCA
20 30 30 20 30 30 10 20 10 30 20 30 20 10
PL/SQL utworzył kursor SQL, sparsował go i przekazał jego obsługę do PHP. PHP przetworzył go i wyświetlił wynik. Jest to kombinacja, która może być zastosowana podczas tworzenia aplikacji i dawać doskonałe efekty. Jeżeli kursor zwrócony z PL/SQL wykorzystuje blokowanie, funkcja oci_execute musi być wykonywana z opcją OCI_NO_AUTO_COMMIT, ponieważ zatwierdzenie po transakcji zwolni wszystkie blokady i spowoduje następujący błąd:
185
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
PHP Warning: oci_fetch_array(): ORA-01002: fetch out of sequence in/home/mgogala/work/book/ ´ChapterDB/listing9_6.php on line 29
Błąd wywołało dodanie do powyższego skryptu frazy for update of stanowisko. Zapytanie zostało zmodyfikowane i przyjęło następującą formę: select pNazwisko,stanowisko,dzialId from prac for update of stanowisko. Takie zapytanie blokuje wybrane wiersze; zachowanie to jest zdefiniowane w standardzie SQL. W bazach relacyjnych blokowanie następuje na czas transakcji. Po zakończeniu transakcji, np. w wyniku zatwierdzenia zmian, kursor staje się niepoprawny, a dane nie mogą być pobrane. Domyślnie funkcja oci_execute zatwierdza zmiany, a co za tym idzie, psuje zapytania zawierające frazę for update. Pojawi się podobny błąd, przedstawiony w kolejnym podrozdziale. UWAGA. Funkcja oci_execute wykona zatwierdzenie transakcji po każdym udanym wywołaniu, nawet jeżeli wykonywany kod SQL jest zapytaniem. Jeśli chcemy zmienić domyślne zachowanie, należy przekazać do funkcji flagę OCI_NO_AUTO_COMMIT.
Możemy teraz przejść do innego ważnego obiektu.
Praca z typami LOB LOB oznacza duży obiekt. Może to być obiekt typu tekstowego (CLOB), obiekt binarny (BLOB) lub oracle’owy wskaźnik na plik (BFILE). Podstawową właściwością typów LOB jest wielkość. W tym przypadku rozmiar zdecydowanie ma znaczenie. Dopóki nie pojawiły się bazy relacyjne, dokumenty, pliki multimedialne i pliki graficzne nie były przechowywane w bazie, lecz w systemie plikowym, który można porównać do szafki na dokumenty, z szufladami i oznaczeniami literowymi. Należało wiedzieć dokładnie, czego się szuka — najlepiej znać numer dokumentu. Zadania w rodzaju: „Proszę, znajdź wszystkie kontrakty z roku 2008 dotyczące mebli biurowych, takich jak krzesła, stoły i szafki”, nie były łatwe do wykonania. System plików przechowuje bardzo mało dostępnych zewnętrznie danych o dokumencie. Zazwyczaj mamy do dyspozycji tylko nazwę pliku, nazwisko właściciela, rozmiar i datę. Nie ma słów kluczowych, żadnych uwag, autora ani żadnych innych użytecznych informacji o dokumencie. Przechowywanie wszystkich takich informacji oznacza, że „szafka z aktami” nie jest już wystarczająca — coraz częściej dokumenty przechowywane są w bazie danych. Oracle udostępnia rozwiązanie o nazwie Oracle*Text, dostarczane z każdą bazą bez żadnych dodatkowych opłat. Opcja ta pozwala na zakładanie indeksów tekstowych dotyczących dokumentów, parsowanie dokumentów MS Word, Adobe PDF, dokumentów HTML i wielu innych. Może także przeszukiwać teksty, dokładnie tak jak Sphinx, a jego indeksy tekstowe są silnie zintegrowane z bazą. Istnieje ponadto możliwość analizowania map, mierzenia odległości między dwoma punktami, a nawet analizowania zdjęć rentgenowskich. Wszystkie te rozwiązania są oparte na obiektach LOB przechowywanych w bazie danych. Oczywiście PHP jest językiem często wykorzystywanym w aplikacjach webowych i ma świetne mechanizmy do wczytywania plików. Sprawia to, że operacje na obiektach LOB są szczególnie ważne w kontekście aplikacji PHP. Wczytywanie plików i ich przechowywanie w bazie danych to nieodłączne czynności związane z pracą z Oracle i PHP. Nasz kolejny skrypt wczyta zawartość pliku tekstowego do bazy danych. W pliku tym znajduje się świetne opowiadanie Kurta Vonneguta, Harrison Bergeron, dostępne pod adresem http://www.vonnegut.art.pl/english/harrison.html. Tekst opowiadania został zapisany na dysku w pliku o nazwie harrison_bergeron.txt. Opowiadanie jest raczej krótkie, ma około 12 000 znaków, ale i tak jest większe niż maksymalna dozwolona liczba znaków dla typu VARCHAR2, która wynosi 4 000 znaków: ls -l harrison_bergeron.txt -rw-r--r-- 1 mgogala users 12756 Apr 2 23:28 harrison_bergeron.txt
Dokument ma dokładnie 12 756 znaków. Ta informacja przyda się nam, kiedy będziemy sprawdzali wynik wywołania skryptu. Oczywiście będziemy potrzebowali tabeli, do której będziemy wstawiać dokumenty. Oto tabela wykorzystywana w następnych dwóch przykładach:
186
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
CREATE TABLE TEST2_INS ( NAZWAPLIKU VARCHAR2(128), ZAWARTOSC CLOB ) LOB(ZAWARTOSC) STORE AS SECUREFILE OPOWIADANIA_SF ( DISABLE STORAGE IN ROW DEDUPLICATE COMPRESS HIGH ) ;
Podczas tworzenia takiej tabeli naturalna może być chęć nazwania kolumn angielskimi wyrazami NAME i CONTENT, mogą to jednak być słowa zastrzeżone lub mogą się takimi stać w kolejnych wersjach bazy Oracle. Może to spowodować nieprzewidywalne problemy, więc dobrą zasadą jest nienazywanie kolumn w ten sposób. UWAGA. Wykorzystywanie słów takich NAME, CONTENT, SIZE itp. może być niebezpieczne ze względu na możliwość wystąpienia konfliktów ze słowami kluczowymi.
Podczas tworzenia kolumn LOB dostępnych jest wiele opcji — ich liczba jest uzależniona od naszej wersji bazy Oracle. Opcje wybrane przy tworzeniu kolumny mogą znacząco wpłynąć na ilość miejsca koniecznego do przechowywania danych, wydajność indeksów tekstowych oraz wydajność zapytań wykonywanych na tabeli. Wersja bazy, na której tworzona była tabela, to 11.2. Nie wszystkie z zastosowanych opcji są dostępne we wcześniejszych wersjach, które mogą być jeszcze wykorzystywane. Opcją dostępną od wersji 9i jest DISABLE STORAGE IN ROW. Jeżeli opcja ta zostanie użyta, Oracle będzie przechowywał dane z kolumny w odrębnym miejscu, w tzw. segmencie LOB, pozostawiając w wierszu tylko dane nazywane lokatorami LOB, pozwalające na znalezienie pliku. Lokatory LOB mają z reguły rozmiar 23 bajtów. Dzięki temu zapis kolumn niebędących typu LOB będzie gęstszy, a co za tym idzie, ich odczyt będzie dużo szybszy. Aby uzyskać dostęp do danych LOB, Oracle będzie zmuszony do wykonania dodatkowych operacji wejścia-wyjścia, przez co wydajność spadnie. Bez opcji DISABLE STORAGE IN ROW Oracle będzie przechowywał do 4 000 znaków z zawartości pliku w normalnej kolumnie razem z danymi niebędącymi obiektami LOB. Gęstość zapisu danych będzie przez to mniejsza, co z kolei przełoży się na wydajność indeksów zakładanych dla kolumn innych niż LOB. Jednocześnie zmniejszy to liczbę odczytów potrzebną do odczytania danych LOB. Generalną zasadą jest przechowywanie danych LOB razem z innymi danymi, kiedy podczas odczytu tabeli dane LOB zawsze są pobierane. Jeśli jednak dane LOB nie są potrzebne, lepiej przechowywać je osobno, co oznacza skorzystanie z opcji DISABLE STORAGE IN ROW. Domyślnie Oracle przechowuje wszystkie dane razem. Teraz wstawimy nazwę pliku i jego zawartość do utworzonej tabeli. Skrypt 9.7 pokazuje, jak to zrobić. Listing 9.7. Wstawianie danych LOB do bazy Oracle
187
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Wynik wywołania skryptu jest następujący: C:\xampp\htdocs>c:\xampp\php\php listing9_7.php Nazwa pliku = harrison_bergeron.txt Rozmiar pliku = 12756
Udało nam się zatem wstawić plik tekstowy do bazy. Listing 9.7 zawiera kilka ważnych elementów. Odwrotnie niż w przypadku typu OCI-Collection deskryptory OCI-Lob muszą być inicjalizowane w bazie — stąd klauzula returning. Gdybyśmy próbowali zasilić deskryptor LOB po stronie klienta i po prostu wstawić go do bazy, bez komplikacji z returning i empty_clob(), otrzymalibyśmy błąd dotyczący nieprawidłowego deskryptora LOB. Spowodowane jest to tym, że kolumny LOB są tak naprawdę plikami wewnątrz bazy danych. Musi zostać zaalokowane miejsce, a informacje o pliku muszą być przekazane do deskryptora poprzez przypisanie. Metoda pokazana poprzednio — wykorzystująca klauzulę returning — jest ogólną metodą stosowaną przy wstawianiu danych LOB do bazy Oracle. Ponadto deskryptor LOB jest obiektem ważnym tylko na czas trwania transakcji. Bazy relacyjne wykorzystują transakcje. Kiedy obiekty LOB już znajdą się w bazie danych, podlegają tym samym zasadom ACID co wszystkie inne dane. Kolumna LOB jest w końcu tylko kolumną tabeli. Gdy transakcja zostanie zakończona, nie ma gwarancji, że ktoś inny nie zablokuje wiersza, który właśnie wstawiliśmy, i nie dopisze czegoś do naszego pliku, zmieniając tym samym jego rozmiar lub nawet umiejscowienie. W związku z tym deskryptory LOB są ważne tylko na czas transakcji, co oznacza, że podczas wywoływania funkcji oci_execute musi być wykorzystany parametr OCI_NO_AUTO_COMMIT. Zmiany możemy zatwierdzić dopiero po zakończeniu wszystkich modyfikacji dla wiersza. Bez opcji OCI_NO_AUTO_COMMIT otrzymalibyśmy następujący błąd: ./listing9_7.php PHP Warning: OCI-Lob::import(): ORA-22990: LOB locators cannot span transactions in /home/mgogala/ ´work/book/ChapterDB/listing9_7.php on line 18
Oczywiście pusty LOB zostałby wstawiony do bazy, co oznacza, że nazwa pliku byłaby poprawna, jednak jego zawartość nie. Innymi słowy, baza danych byłaby logicznie niepoprawna. Słowo niepoprawna oznacza, że dane w bazie byłyby niespójne. Wpis z nazwą pliku, ale bez jego zawartości, byłby niespójny. Jest to podobne do problemu z blokowaniem kursorów, pokazanego w poprzednim rozdziale, ale o wiele bardziej niebezpieczne. Interfejs OCI8 zawiera klasę OCI-Lob. Nowy obiekt tej klasy jest tworzony poprzez wywołanie funkcji oci_new_descriptor. Klasa ma mniej więcej te same właściwości co wewnętrzny pakiet PL/SQL o nazwie DBMS_LOB, wykorzystywany do wykonywania operacji na danych LOB. Dane LOB należy traktować jak pliki przechowywane 188
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
w bazie danych. Na plikach można przeprowadzić wiele operacji: odczytać je, zapisać, edytować, sprawdzić ich rozmiar, aktualną lokalizację, przeszukać je, ustawić ich buforowanie, ustawić ich pozycję na początek (przewinąć) oraz pobrać je na dysk. Wszystkie te operacje mogą być wykonane za pośrednictwem metod z klasy OCI-Lob. Dla uproszczenia wykorzystaliśmy klasę OCI-Lob->import. Mogliśmy jednak użyć metody OCI-Lob->write — analogicznej do zapisu w systemie plików. Jej składnia jest następująca: OCI-Lob->write($bufor,$dlugosc). Metoda write zwraca liczbę bajtów zapisanych w kolumnie LOB. Metodę OCI-Lob->flush() wykorzystaliśmy, aby się upewnić, że w momencie zatwierdzania transakcji wszystkie dane z oryginalnego pliku zostały zapisane w kolumnie LOB. Jest to dobry sposób na upewnienie się, że wszystkie dane zostały przesłane na serwer, zanim nastąpiło zatwierdzenie transakcji, zwolnienie blokad i zanim deskryptor stracił ważność. Co więcej, metoda OCI-Lob->import jest odpowiednia dla małych plików. Przy dużych plikach możliwe są rozmaite błędy związane z pamięcią. Skrypty PHP mają ograniczenia dotyczące pamięci wynikające z ustawień w pliku php.ini, a większość administratorów nie jest zbyt hojna i niechętnie pozwala skryptom na zajmowanie dużych ilości pamięci. Typowe wartości oscylują pomiędzy 32 MB a 256 MB maksymalnej dostępnej dla skryptu pamięci. Jeżeli obciążenie na stronie będzie wysokie, taka szczodrość może spowodować niedostępność całego serwera. Bardzo duże pliki, zawierające setki megabajtów danych, mogą być wczytywane wyłącznie fragmentami, poprzez wczytywanie części o sensownej wielkości do bufora, a następnie zapisywanie jego zawartości w kolumnie LOB metodą write klasy OCI-Lob. Maksymalny rozmiar obiektu LOB to 4 GB, jednak rzadko zachodzi potrzeba wczytywania do bazy tak dużych plików. W naszej karierze najczęściej spotykaliśmy przypadki wczytywania do bazy plików tekstowych, a te z reguły mają rozmiar w granicach kilku megabajtów. Dla tego rodzaju plików zazwyczaj wykorzystywana jest metoda OCI-Lob->import(). Na zakończenie rozdziału skrypt z listingu 9.8 pokazuje przykład odczytu pliku LOB przy użyciu metody OCI-Lob->read(). Listing 9.8. Skrypt demonstrujący wykorzystanie metody OCI-Lob->read() read(65536); printf("Liczba znaków w tekście %d\n", strlen($opowiadanie)); } catch(Exception $e) { print "Wyjątek:\n"; die($e->getMessage() . "\n"); } ?>
189
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Dlaczego nasze zapytanie utworzyliśmy wewnątrz anonimowego bloku PL/SQL? Odpowiedź jest prosta — zwyczajne przypisanie zmiennej do pola w zapytaniu SELECT … INTO nie działa dla deskryptora LOB. Zwraca niepoprawny uchwyt. Zapisanie zapytania wewnątrz anonimowego bloku nie jest trudne. Część dotycząca wykonania była już wielokrotnie omawiana: parsowanie, przypisanie zmiennych i wykonanie. Czytanie z kolumny LOB jest proste jak czytanie z systemu plików. UWAGA. Kolumny LOB należy traktować jak pliki przechowywane w bazie danych.
Istnieje więcej opcji, sztuczek i kruczków, które można wykorzystać w pracy z typami LOB. W najnowszej wersji Oracle 11g możliwe jest kompresowanie kolumn LOB, z zastosowaniem opcji zaawansowanej kompresji, która sprzedawana jest odrębnie. Podręcznik dostępny jest razem z pozostałą dokumentacją Oracle i jest zatytułowany Large Objects Developer’s Guide, a od wersji 11g Securefiles and Large Objects Developer’s Guide.
Inne podejście do połączeń — pule połączeń Podrozdział ten dotyczy nowej technologii, którą należy traktować z pewną ostrożnością. Pule połączeń dostępne są wyłącznie w wersji 11g — najnowszej i najlepszej wersji bazy Oracle. Wielu użytkowników nie przeszło jeszcze na wersję 11g. Wdrożenie nowej wersji systemu produkcyjnego jest dużym przedsięwzięciem, które nie może być traktowane po macoszemu. Możliwość użycia puli połączeń jest poważnym argumentem przemawiającym za przejściem na wersję 11g, jeżeli z systemu korzystają aplikacje mogące mieć z tego pożytek. Pule nie są dostępne wyłącznie dla PHP — jest to ogólny mechanizm, który może być używany także z innymi narzędziami. Pojęcie pul połączeń znane jest każdemu, kto miał do czynienia z aplikacjami Java oraz serwerami aplikacji. Generalnie chodzi o alokację pewnej liczby procesów serwera, które mogą być wykorzystywane przez aplikację. DBA może zaalokować pulę procesów i udostępnić ją aplikacjom. Aby zrozumieć zalety tego rozwiązania, przyjrzyjmy się najpierw tradycyjnym opcjom dostępnym podczas łączenia się z bazą Oracle. Przed wprowadzeniem pul połączeń dostępne były dwie opcje; obie musiały zostać skonfigurowane przez DBA. Pierwsze było połączenie z dedykowanym serwerem. Kiedy aplikacja wysyła żądanie dedykowanego serwera, przypisywany jest do niej odpowiedni proces serwerowy. Proces przeznaczony jest wyłącznie dla tej aplikacji i jeżeli aplikacja jest bezczynna, nie może on obsłużyć żadnych innych żądań. Proces uruchamiany jest na czas połączenia, które go zainicjowało, i jest usuwany po zakończeniu połączenia. Jest to domyślny sposób zarządzania połączeniami, z reguły odpowiedni dla większości aplikacji. Każdy proces ma swój obszar roboczy, w terminologii Oracle zwany globalnym obszarem procesu (Process Global Area — PGA), wykorzystywany do sortowania i haszowania. Kiedy proces zostaje zakończony, jego PGA jest dealokowane, ponieważ nie jest to pamięć współdzielona, lecz indywidualnie przypisana do procesu. Każde dedykowane połączenie wymusza kosztowne operacje tworzenia procesu serwerowego. Baza musi być skonfigurowana tak, aby zezwalać na tworzenie procesów dla każdego połączenia. Drugi sposób, istniejący już od 7. wersji serwera, znany jest jako połączenie serwera współdzielonego. Baza może zostać skonfigurowana tak, że tworzona jest grupa procesów serwerowych, które będą wykonywały operacje w imieniu klientów. Kiedy proces zakończy wykonywanie operacji SQL dla aplikacji A, jest wolny i może zacząć pracę nad wykonaniem innej operacji dla aplikacji B. Nie ma gwarancji, że dwa następujące po sobie zapytania SQL wykonane z tego samego procesu klienckiego zostaną obsłużone przez ten sam współdzielony proces. Wszystkie współdzielone procesy mają swoje obszary robocze w pamięci, które Oracle nazywa dzielonymi obszarami globalnymi (Share Global Area — SGA), co oznacza, że konieczny jest spory wysiłek podczas konfiguracji serwera, aby wszystko działało poprawnie. Wymaga to także znacznej ilości współdzielonej pamięci, która jest przypisana do procesów na stałe i nie może być zwalniana, jeżeli nie jest wykorzystywana. Łączenie aplikacji z bazą danych nie wymaga tworzenia nowego procesu, a kilka procesów serwerowych może obsłużyć całkiem sporo procesów klienckich. Konfigurowanie i monitorowanie takich połączeń jest dość skomplikowane i rzadko stosowane. Pule połączeń dostępne od wersji 11g, zwane rezydentnymi pulami połączeń bazy danych (Database Resident Connection Pooling — DRCP), mają zalety obu poprzednich typów połączeń. Kiedy proces z puli zostanie
190
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
przypisany do sesji, jest do niej przypisany przez cały czas jej trwania. Co więcej, każdy proces z puli ma swoje własne PGA, a więc nie ma problemu z kosztowną i skomplikowaną konfiguracją pamięci współdzielonej. Konfiguracja pul ma miejsce głównie po stronie bazy danych i wykonywana jest przez administratora oraz po stronie PHP w pliku php.ini. Nie ma konieczności zmieniania składni skryptów. Istniejące skrypty mogą korzystać z pul połączeń bez żadnych zmian. Zobaczmy, w jaki sposób można skonfigurować pule połączeń. Najpierw po stronie bazy musimy skonfigurować pulę. Robimy to przy użyciu pakietu PL/SQL o nazwie DBMS_CONNECTION_POOL. Opis pakietu dostępny jest pod adresem http://docs.oracle.com/cd/E11882_01/appdev.112/e16760/toc.htm. Pakiet pozwala administratorom określić maksymalną i minimalną liczbę procesów w puli, maksymalny czas bezczynności, po którym proces wraca do puli, maksymalny czas sesji oraz czas życia (Time To Live — TTL). Kiedy sesja jest bezczynna przez czas dłuższy niż zdefiniowany maksymalny czas życia, jest usuwana. Pomaga to utrzymać wykorzystanie procesów na niskim poziomie. Oto przykład konfiguracji puli: begin dbms_connection_pool.configure_pool( pool_name => 'SYS_DEFAULT_CONNECTION_POOL', minsize => 5, maxsize => 40, incrsize => 5, session_cached_cursors => 128, inactivity_timeout => 300, max_think_time => 600, max_use_session => 500000, max_lifetime_session => 86400); end;
Aby wykonać powyższe polecenie, użytkownik musi połączyć się przy wykorzystaniu konta SYSDBA. Bez wdawania się w szczegóły zastosujemy domyślne argumenty i uruchomimy pulę. Oracle 11.2 wspiera tylko jedną pulę, a więc nie ma tu wyboru: SQL> connect / as sysdba Connected. SQL> exec dbms_connection_pool.start_pool(); PL/SQL procedure successfully completed.
Polecenie to uruchomi pulę domyślną, która będzie czynna bezustannie. Nawet jeżeli instancja zostanie zrestartowana, pula uruchomi się automatycznie. Gdy pula już jest uruchomiona, należy ustawić parametr oci8.connection_class, a jego wartość — na ciąg znaków, po którym instancja Oracle będzie mogła zidentyfikować naszą aplikację. Będzie można monitorować to za pomocą tabel Oracle. Oto ustawienia zastosowane w naszym pliku php.ini: oci8.connection_class = TEST oci8.ping_interval = -1 oci8.events = On oci8.statement_cache_size = 128 oci8.default_prefetch = 128
Parametr oci8.events pozwala instancji na przesyłanie notyfikacji. Ustawienie parametru oci8.ping_interval na wartość 1 wyłącza sprawdzanie przez PHP stanu inicjalizacji instancji. Nie jest to potrzebne, jeżeli włączone są notyfikacje. Ostatnie dwa parametry zostały ustawione ze względu na wydajność. Sesja OCI8 zbuforuje do 128 kursorów w pamięci podrzędnej i będzie próbowała pobierać rekordy w paczkach po 128. Plik z parametrami jest już gotowy. Wszystko, co musimy teraz zrobić, to połączyć się z bazą danych. Do tego celu wykorzystamy skrypt z listingu 9.2, w którym zamienimy linię: $dbh = oci_connect("test", "test", "local");
na następującą: $dbh = oci_pconnect("test", "test", "localhost/oracle.home:POOLED");
191
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
I to już wszystko. Nic więcej nie trzeba zmieniać. Skrypt wykona się teraz w dokładnie tak samo jak przy starej metodzie łączenia się z bazą. Czym więc jest pconnect? Funkcja oci_pconnect tworzy stałe połączenie. Kiedy połączenie zostanie nawiązane, nie zakończy się nawet po zakończeniu skryptu. Przy próbie następnego połączenia Oracle sprawdzi, czy jest wolne połączenie dla danych aplikacji, i z niego skorzysta. Istnieje także funkcja oci_new_connection, która spowoduje nawiązanie nowego połączenia za każdym razem. Standardowa funkcja oci_connect, której używaliśmy do tej pory, zamyka połączenie po zakończeniu działania skryptu, ale zwróci ten sam uchwyt do połączenia, jeżeli połączenie z tymi samymi danymi dostępowymi zostanie nawiązane więcej niż jeden raz. Stosowanie pul należy rozważyć, kiedy istnieje wiele procesów nawiązujących połączenie przy wykorzystaniu tych samych danych dostępowych i kiedy procesy te często łączą się z bazą. Dzięki stosowaniu pul oszczędzamy zasoby serwera, a administrator ma możliwość sprawniejszego zarządzania nimi. Zamiar wykorzystania pul trzeba omówić z administratorem bazy, ponieważ to on musi wykonać większość pracy.
Zestawy znaków w bazie danych i PHP Podczas pracy z bazami danych często spotykanym problemem jest zgodność kodowania. Oracle przechowuje dane zakodowane zgodnie z wartością parametru NLS_CHARACTERSET, który definiowany jest w momencie tworzenia obiektu, i nie można go w łatwy sposób zmienić. Zmiana kodowania możliwa jest tylko wtedy, kiedy nowy zestaw znaków jest nadzbiorem zestawu początkowego. Dane w bazie mogą zostać uszkodzone, jeżeli spróbujemy wykonać niedozwoloną konwersję. Jedynym sensownym sposobem zmiany kodowania jest zazwyczaj skorzystanie z importu lub eksportu, co może trwać bardzo długo w przypadku dużych baz. Na szczęście dla programistów PHP Oracle konwertuje dane przesyłane do klienta na wymagany przez niego format. Istnieje zmienna środowiskowa decydująca o konwersji. Utwórzmy kolejną tabelę: CREATE TABLE TEST3 ( KLUCZ NUMBER(10,0), WARTOSC VARCHAR2(64) )
Do tabeli został wstawiony jeden wiersz zawierający wartości: (1, 'Überraschung'). Wyraz Überraschung oznacza w języku niemieckim niespodziankę; został wybrany ze względu na pierwszy znak. Znak nad literą U to tzw. umlaut. Utwórzmy teraz mały skrypt PHP, który jest nieco zmodyfikowanym skryptem z listingu 9.2 (listing 9.9). Listing 9.9. Mały skrypt PHP
192
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
Skrypt pobiera wszystkie dane z tabeli TEST3, a następnie je wyświetla. W skrypcie tym nie ma nic szczególnie interesującego. Został pokazany ze względu na wyniki: Pierwsze uruchomienie: unset NLS_LANG ./listing9_9.php 1 Uberraschung
Drugie uruchomienie: export NLS_LANG=AMERICAN_AMERICA.AL32UTF8 ./listing9_9.php 1 Überraschung
Wynik skryptu jest zależny od zmiennej środowiskowej NLS_LANG. Składnia NLS_LANG wygląda następująco: _.zestaw_znaków. Dokładna składnia wraz z przykładami jest opisana w dokumentacji Oracle. W pierwszym wykonaniu nie było zdefiniowanej zmiennej. Oracle użył domyślnego zestawu znaków, czyli US7ASCII, dla systemu maszyny, na której były tworzone przykłady. Wynik nie zawiera żadnych znaków spoza zestawu US7ASCII; słowo zostało wyświetlone jako Uberraschung, bez znaku umlaut (kropek nad literą U). Za drugim razem, po zdefiniowaniu właściwości NLS_LANG, wynik był poprawny — zawierał znak umlaut. Jeżeli kontrola za pośrednictwem NLS_LANG nie przemawia do Ciebie lub Twoja aplikacja musi wyświetlać wyniki w wielu zestawach znaków, kodowanie można także wyspecyfikować podczas połączenia. Zestaw znaków jest czwartym parametrem funkcji oci_connect. Zamiast korzystać z NLS_LANG, mogliśmy napisać oci_connect("test","test","local","AL32UTF8") — wynik również w tym przypadku zawierałby umlaut. Nazwy Oracle dla poszczególnych zestawów znaków można sprawdzić w dokumentacji oraz w samej bazie. Użyte nazwy znajdują się w tabeli V$NLS_VALID_VALUES. Oracle wspiera ponad 200 różnych zestawów znaków. Aby uzyskać o nich szczegółowe informacje, odwołaj się do dokumentacji. Oczywiście, aby PHP było w stanie wyświetlić zawartość poprawnie, należy ustawić wartość parametru iconv.output_encoding na odpowiedni zestaw znaków. Z reguły wartości parametrów iconv ustawiamy następująco: iconv.input_encoding = UTF-8 iconv.internal_encoding = UTF-8 iconv.output_encoding = UTF-8
Parametr input_encoding nie jest wykorzystywany — został po prostu uzupełniony razem z innymi. Dzięki temu PHP będzie mogło poprawnie wyświetlić teksty, a ciągi znaków będą poprawnie sformatowane.
Podsumowanie W tym rozdziale szczegółowo omówiliśmy sposób wykorzystania rozszerzenia OCI8. Najważniejszym zagadnieniem był interfejs tablicowy. Pozwala on na szybkie wczytywanie danych z PHP. Wymaga jednak wyjątkowych możliwości rozszerzenia OCI8, czyli klasy OCI-Collection. Omówiliśmy także postępowanie z typami LOB, kursorami oraz wartościami przypisywanymi, które mogą się przydać podczas tworzenia oprogramowania. Zestawy znaków czy pule połączeń stały się integralną częścią współczesnego oprogramowania. W kolejnych wersjach systemu zarządzania bazą danych Oracle z pewnością pojawią się nowe możliwości interfejsu OCI8. Niniejszy rozdział z pewnością zawiera wystarczające informacje, abyś mógł z tego interfejsu sprawnie korzystać.
193
ROZDZIAŁ 9. INTEGRACJA Z BAZAMI DANYCH. CZĘŚĆ III
194
ROZDZIAŁ 10
Biblioteki
PHP jest wszechstronnym językiem — z szerokim wachlarzem zastosowań. Dostępnych jest wiele rozbudowanych bibliotek typu open source, napisanych w tym języku. Cieszy to nas, programistów, ponieważ nie lubimy powtórnie wynajdować koła. Dzięki bibliotekom praca wymaga od nas mniej wysiłku i możemy zaoszczędzić czas. W tym rozdziale położymy nacisk na praktykę. Pokażemy, w jaki sposób: • parsować wiadomości RSS przy zastosowaniu SimplePie, • wykorzystać bibliotekę TCPDF do generowania dokumentów PDF, • uzyskiwać dane ze stron internetowych za pomocą cURL i phpQuery, • wykonać integrację z Mapami Google za pośrednictwem php-google-map-api, • generować wiadomości e-mail i SMS z biblioteką PHPMailer, • wykorzystać Google Chart API za pośrednictwem gChartPHP.
KONFIGURACJA SERWERA W odniesieniu do wszystkich przykładów z tego rozdziału załóżmy, że głównym katalogiem jest /htdocs/. Systemem plików naszego serwera, relatywnym do katalogu głównego, będzie: /htdocs/biblioteka1/ /htdocs/biblioteka2/ … /htdocs/przyklad1.php /htdocs/przyklad2.php …
Przekłada się to na następujące adresy URL: http://localhost/biblioteka1/ http://localhost/biblioteka2/ http://localhost/przyklad1.php http://localhost/przyklad2.php
ROZDZIAŁ 10. BIBLIOTEKI
SimplePie SimplePie jest biblioteką pozwalającą na bardzo łatwą obsługę wiadomości RSS i Atom. Oferuje rozbudowaną funkcjonalność, jest bezpłatna i doskonale udokumentowana. Bibliotekę tę należy pobrać pod adresem http://www.simplepie.org/, a następnie umieścić w katalogu /htdocs/simplepie/. Strona http://localhost/simplepie/compatibility_test/sp_compatibility_test.php pomoże Ci rozwiązać problemy z ustawieniami serwera. Możesz włączyć rozszerzenie cURL, jeżeli pojawi się następujący komunikat: "cURL: The cURL extension is not available. SimplePie will use fsockopen() instead."
Jak powiedzieliśmy powyżej, cURL jednak nie jest niezbędny, więc wybór należy do Ciebie. Wykorzystamy kanał RSS wydawnictwa Helion. Odwołamy się powtórnie do tego kanału bez użycia SimplePie w rozdziale 14., dotyczącym XML. Adres kanału to http://helion.pl/rss/. Biblioteka SimplePie wywołuje kilka błędów E_DEPRECATED, które są nowością w PHP 5.3. Wyłączymy wyświetlanie takich błędów za pomocą komendy error_reporting(E_ALL ^ E_NOTICE ^ E_DEPRECATED); (listing 10.1). Listing 10.1. Podstawowe wykorzystanie biblioteki SimplePie get_items () as $wiadomosc ) { echo '
Jeżeli pojawi się komunikat: Warning: ./cache is not writeable. Make sure you’ve set the correct relative or absolute path, and that the location is server-writable („Uwaga: brak uprawnień do zapisu katalogu /cache. Upewnij się, że podałeś poprawną ścieżkę absolutną lub relatywną oraz że serwer ma do niej uprawnienia zapisu”), trzeba to naprawić. Należy podać ścieżkę do katalogu zezwalającego na zapis jako drugi argument konstruktora albo utworzyć folder o nazwie cache w katalogu, w którym znajduje się skrypt, a następnie nadać mu odpowiednie uprawnienia. Na rysunku 10.1 został pokazany wynik działania skryptu z listingu 10.1. Nasz kod w rozdziale 14. nie jest dłuższy niż przykład z listingu 10.1. Biblioteka SimplePie jest natomiast bardziej konfigurowalna i dojrzalsza niż skrypt z rozdziału 14. Obsługuje ona różne rodzaje kanałów i może nam oszczędzić wiele pracy w przypadku zaawansowanych zadań. Ma wiele pomocniczych funkcji, takich jak pobranie ikony favicon lub pól dla mediów społecznościowych. Ma także wbudowane mechanizmy zarządzania subskrypcją, wtyczki do zewnętrznych frameworków, CMS-ów i API. W skrypcie prezentowanym na listingu 10.2 dodaliśmy ikonę favicon i sformatowaliśmy datę. Listing 10.2. Dodanie ikony favicon i formatowania daty za pomocą biblioteki SimplePie
196
ROZDZIAŁ 10. BIBLIOTEKI
Rysunek 10.1. Wynik działania skryptu z listingu 10.1 get_favicon (); foreach ( $simplepie->get_items () as $wiadomosc ) { echo '
Ostatni przykład dotyczy elementu będącego częścią przestrzeni nazw w kanale RSS. Jeżeli nie jesteś pewien, jakie pola są wypełniane dla danego kanału RSS, podejrzyj źródło w przeglądarce i sprawdź kod XML. W przykładowym kanale ze strony Wired dane autora są umieszczone w elemencie dc:creator.
Widzimy, że przestrzeń nazw dc odnosi się do http://purl.org/dc/elements/1.1/ i możemy wykorzystać metodę get_item_tags do sprawdzenia struktury elementu (listingi 10.3 i 10.4).
197
ROZDZIAŁ 10. BIBLIOTEKI
Listing 10.3. Sprawdzenie struktury elementu z przestrzeni nazw get_items()); $tworca = $element->get_item_tags("http://purl.org/dc/elements/1.1/", "creator"); var_dump($tworca);
Znamy już strukturę elementu i możemy wykorzystać go w naszym skrypcie. Listing 10.4. Dodanie elementu z przestrzeni nazw get_favicon (); foreach ( $simplepie->get_items () as $element ) { $tworca = $element->get_item_tags ( "http://purl.org/dc/elements/1.1/", "creator" ); echo '
Wynik działania skryptu z listingu 10.4 został pokazany na rysunku 10.2.
198
ROZDZIAŁ 10. BIBLIOTEKI
Rysunek 10.2. Wynik działania skryptu z listingu 10.4, wyświetlający ikonę favicon i dane autora Aby zapoznać się z dostępnymi metodami i dokumentacją SimplePie, można zajrzeć na oficjalną stronę biblioteki: http://simplepie.org/wiki/reference/start.
TCPDF TCPDF (tecnick.com PDF) jest biblioteką do generowania dokumentów PDF z poziomu kodu PHP. Nie wymaga żadnych zewnętrznych bibliotek, jest bardzo popularna i aktywnie rozwijana. Można ją pobrać pod adresem http://www.tcpdf.org/. TCPDF zawiera wszystkie funkcje: obsługuje grafikę za pośrednictwem PHP GD i imagemagick, kody kreskowe, gradienty, HTML, CSS, czcionki, zarządzanie rozkładem, a także nagłówki i stopki. Domyślne definicje i ustawienia znajdują się w pliku konfiguracyjnym: /htdocs/tcpdf/config/tcpdf_config.php. Podczas generowania dokumentów PDF wykonywanie skryptów z wiersza poleceń będzie szybsze niż wykonywanie ich w przeglądarce. Wydajność przeglądarek może być różna. Np. silnik renderujący wbudowany w przeglądarkę Chrome jest bardzo szybki. Generowanie dokumentów PDF za pomocą TCPDF może zająć dużo czasu i pamięci. Musimy zmodyfikować kilka ustawień w pliku php.ini: max_execution_time = 90 //należy zwiększyć lub zmniejszyć w miarę potrzeby memory_limit = 256M //należy zwiększyć lub zmniejszyć w miarę potrzeby
Skrypt z listingu 10.5 generuje dokument PDF zawierający linię tekstu przy wykorzystaniu minimalnej ilości kodu. Listing 10.5. Przykład wykorzystania biblioteki TCPDF1
Opis sposobu generowania polskich czcionek dla biblioteki TCPDF można znaleźć pod adresem http://blog.sznapka.pl/tcpdf-polskie-czcionki/. Dwie gotowe czcionki można pobrać pod adresem http://www.tutorials.pl/2009/06/tcpdf-polskie-znaki-czcionki-fonts-dejavusans-freesans/ — przyp. tłum.
199
ROZDZIAŁ 10. BIBLIOTEKI
//dodanie strony $pdf->AddPage(); //przypisanie tekstu $tekst = "Rozdział 10.: przykład wykorzystania biblioteki TCPDF"; //wstawienie bloku tekstu $pdf->Write( 20, $tekst ); //zapisanie dokumentu PDF $pdf->Output( 'przyklad.pdf', 'I' ); ?>
Na listingu 10.5 wykorzystujemy plik konfiguracyjny języka i główny plik biblioteki. Następnie tworzymy nowy obiekt TCPDF oraz dodajemy nową stronę poprzez wywołanie metody AddPage. Tworzymy jedną linię tekstu o wysokości 20, a następnie generujemy dokument. Opcja I oznacza, że dokument zostanie natychmiast użyty w przeglądarce za pomocą odpowiedniej wtyczki — jeżeli jest dostępna. Konstruktor ma wiele opcjonalnych parametrów, które pozwalają ustawić orientację strony, jednostkę, format, wykorzystanie Unicode, kodowanie oraz bufor dyskowy. Wartości domyślne dla tych parametrów to odpowiednio: portret, mm, A4, prawda, UTF-8, fałsz. Jeśli bufor dyskowy jest wyłączony, to jest zajmowana duża część RAM-u, ale praca jest szybsza, natomiast włączenie buforu powoduje częste zapisy na dysku (wolniejsza praca), ale oszczędzany jest RAM. Metoda Write wymaga podania dwóch parametrów — wysokości linii i tekstu — po nich następuje około dziesięciu parametrów opcjonalnych. Metoda Output jako pierwszy parametr przyjmuje nazwę pliku lub łańcuch z danymi pierwotnymi. W przypadku danych pierwotnych pierwszym znakiem łańcucha musi być znak @. W przypadku nazwy pliku niewłaściwe znaki są usuwane, a spacje zamieniane są na znak podkreślenia. Opcje zapisu pozwalają na wyświetlanie dokumentu w przeglądarce, jego wymuszone pobranie, jego zapis na serwerze, zwrócenie go jako łańcucha znaków oraz jako załącznika do wiadomości e-mail. UWAGA. Numery opcjonalnych argumentów w metodach takich jak Write są trudne do zapamiętania i można je łatwo pomylić. Podczas projektowania API warto zadbać, aby sygnatury funkcji były przyjazne dla programisty. Można to osiągnąć przez ograniczenie liczby argumentów metody lub przez przekazanie do niej tablicy asocjacyjnej bądź obiektu. Zbyt wiele parametrów nie jest dobrą praktyką programistyczną. Robert Martin opisuje to w książce Czysty kod (Helion 2010): „Idealną liczbą argumentów dla funkcji jest zero (funkcja bezargumentowa). Następnie mamy jeden (jednoargumentowa) i dwa (dwuargumentowa). Należy unikać konstruowania funkcji o trzech argumentach (trzyargumentowych). Więcej niż trzy argumenty (funkcja wieloargumentowa) wymagają specjalnego uzasadnienia — a nawet wtedy takie funkcje nie powinny być stosowane”. Mniejsza liczba parametrów sprawia, że zapamiętywanie ich jest łatwiejsze lub niepotrzebne. Jednakże interfejsy graficzne, takie jak Zend Studio lub Netbeans, oferują możliwość łatwego przejścia do źródła metody oraz podpowiedzi w postaci autouzupełniania.
Jeśli chodzi o grafikę, TCPDF zawiera metody pozwalające na wykorzystanie obrazów GD, PNG z kanałem alfa oraz EPS. Można także tworzyć kształty takie jak koła, linie i poligony. Podobnie jak w przypadku innych funkcji, mamy do dyspozycji mnóstwo opcjonalnych parametrów. Nie trzeba znać na pamięć wszystkich argumentów — jeżeli zajdzie taka potrzeba, można je znaleźć w pliku tcpdf.php. W skrypcie z listingu 10.6 demonstrujemy, w jaki sposób wstawić do dokumentu obraz oraz sformatowany HTML. Listing 10.6. Drugi przykład wykorzystania biblioteki TCPDF, wstawienie obrazu i tekstu HTML
200
Rezultat działania skryptu pokazany jest na rysunku 10.3.
Rysunek 10.3. Rezultat działania skryptu z listingu 10.6 — tekst nakładający się na obraz Na listingu 10.6 ustalamy metadane dla dokumentu oraz wskazujemy czcionkę. Podobnie jak na listingu 10.5, dodajemy blok tekstu za pomocą metody Write. Następnie ustawiamy właściwości obrazka i umieszczamy go w dokumencie za pomocą metody Image. W końcu wstawiamy kod HTML przy użyciu metody WriteHTML.
201
ROZDZIAŁ 10. BIBLIOTEKI
Domyślnie logo będzie wstawione w miejscu, gdzie ostatnio znajdował się kursor. Wskutek tego część tekstu została wyrenderowana na obrazku. Aby to naprawić, dodamy przełamania linii, korzystając z metody Ln — skrypt jest pokazany na listingu 10.7. Metoda Ln opcjonalnie przyjmuje parametr odpowiadający za wysokość. Domyślna wysokość jest równa wysokości elementu ostatnio zapisywanego w dokumencie. Listing 10.7. Rozwiązanie problemu nakładającego się tekstu poprzez wstawienie nowych linii SetCreator ( PDF_CREATOR ); $pdf->SetAuthor ( 'Brian Danchilla' ); $pdf->SetTitle ( 'PHP - Zaawansowane programowanie - Rozdział 10.' ); $pdf->SetSubject ( 'TCPDF — Przykład 2.' ); $pdf->SetKeywords ( 'TCPDF, PDF, PHP' ); //ustawienie czcionki $pdf->SetFont ( 'dejavusans', '', 20 ); //dodanie strony $pdf->AddPage (); $tekst = <<Write ( 0, $tekst ); $pdf->Ln (); //image scale factor $pdf->setImageScale ( PDF_IMAGE_SCALE_RATIO ); //jakość obrazu JPEG $pdf->setJPEGQuality ( 90 ); //przykładowy obraz $pdf->Image ( "logo.gif" ); $pdf->Ln ( 30 ); $tekst = "Powyżej: obraz
Osadzony HTML
Ten tekst powinien zawierać kursywę oraz pogrubienie, ´a nagłówek to
Rysunek 10.4. Modyfikacje skryptu rozwiązały problem W trzecim i ostatnim przykładzie, pokazanym na listingu 10.8, wyświetlimy kod kreskowy i gradient. Dostępne typy kodów kreskowych można znaleźć w pliku barcodes.php. Metody wyświetlające kody kreskowe to write1DBarcode, write2DBarCode i setBarcode. Metody dotyczące gradientów to Gradient, LinearGradient, CoonsPatchMesh i RadialGradient. Listing 10.8. Generowanie kodów kreskowych i gradientów za pomocą biblioteki TCPDF SetCreator ( PDF_CREATOR ); $pdf->SetAuthor ( 'Brian Danchilla' ); $pdf->SetTitle ( 'PHP - Zaawansowane programowanie - Rozdział 10.' ); $pdf->SetSubject ( 'TCPDF — Przykład 2.' ); $pdf->SetKeywords ( 'TCPDF, PDF, PHP' ); //ustawienie czcionki $pdf->SetFont ( 'dejavusans', '', 15 ); //ustawienie marginesów $pdf->SetMargins( PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT ); //dodanie strony $pdf->AddPage(); $tekst = <<Write( 20, $tekst ); $pdf->Ln();
W kodzie z listingu 10.8 została dodana funkcjonalność ustawiania marginesów, wyświetlenia kodu kreskowego oraz gradientu. Wywołanie skryptu spowoduje wyświetlenie wyniku pokazanego na rysunku 10.5.
Rysunek 10.5. Kod kreskowy i gradient wygenerowane za pomocą biblioteki TCPDF Musisz także wiedzieć, że możesz wpływać na zachowanie łamania stron za pomocą metody SetAutoPageBreak. Domyślnie strony łamią się automatycznie, tak jakby została wywołana metoda $pdf->SetAutoPageBreak( TRUE, PDF_MARGIN_FOOTER);. Aby wyłączyć automatyczne łamanie stron, należy wywołać funkcję $pdf->SetAutoPageBreak( FALSE );. Bez automatycznego łamania stron wszystkie dane, które nie zmieszczą się na stronie, nie będą wyświetlone. Programista musi wtedy wykonywać dodatkowe wywołania metody AddPage i sprawdzać wielkość bieżącej zawartości. Przy automatycznym łamaniu niemieszczące się dane przechodzą na kolejną stronę.
Pobieranie danych ze stron internetowych Czasami chcemy pobrać ze strony internetowej informacje, które nie są łatwo dostępne poprzez usługę sieciową (Web Service) ani kanały informacyjne. Dane, które nas interesują, są dostępne w czystym HTML-u. Chcielibyśmy uzyskać w sposób automatyczny dane do dalszej obróbki. Proces ten nazywany jest parsowaniem stron.
204
ROZDZIAŁ 10. BIBLIOTEKI
Parsowanie stron nie przynosi tak dokładnych informacji jak dane XML otrzymane za pośrednictwem API lub kanałów informacyjnych. Możemy jednak wykorzystywać klauzule kontekstowe, takie jak element CSS, identyfikator i wartości atrybutu class, a dane zwracać w formie uporządkowanych tabel. Strony dobrze sformatowane są łatwiejsze w parsowaniu, a dane są dokładniejsze. Parsowanie strony jest procesem dwuetapowym. Najpierw musimy pobrać zdalną zawartość, a następnie ją przetworzyć. Dane możemy pobrać za pomocą funkcji file_get_contents lub biblioteki cURL — ta druga metoda wymaga jednak więcej konfigurowania. Dane możemy np. wyświetlić bezpośrednio w formie, w jakiej je pobraliśmy, przefiltrować je bądź sparsować, aby uzyskać ściśle określone informacje, lub przechować w bazie. Interesuje nas druga możliwość — parsowanie w celu pobrania konkretnych danych. W odniesieniu do ogólnej zawartości moglibyśmy to zrobić za pomocą wyrażeń regularnych. W przypadku naszych przykładów lepsze będzie wczytanie danych do obiektu DOMDocument. Pokażemy, w jaki sposób korzystać z obiektu DOMDocument w połączeniu z DOMXPath. Przedstawimy też równoważne rozwiązanie przy zastosowaniu biblioteki phpQuery. Biblioteka phpQuery wykorzystuje DOMDocument i w zamierzeniu ma przenieść notację jQuery na stronę serwera. Jeżeli znasz jQuery, biblioteka phpQuery nie będzie dla Ciebie wyzwaniem. Więcej informacji na temat XML-a, drzewa DOM i jQuery można znaleźć w rozdziałach 14. i 15. UWAGA. Jeżeli pojawi się komunikat Fatal error: Call to undefined function curl_init(), to znaczy, że musisz zainstalować i włączyć rozszerzenie cURL. Być może będziesz musiał pobrać bibliotekę cURL, jeżeli nie ma jej w Twoim systemie. Dodaj lub włącz rozszerzenie w pliku php.ini: ;windows: extension=php_curl.dll ;linux: extension=php_curl.so
a następnie zrestartuj serwer WWW.
Na listingu 10.9 pobierzemy informacje ze strony http://www.helion.pl/ przy wykorzystaniu cURL. Listing 10.9. Podstawowe wykorzystanie cURL Błąd cURL: \n"; echo "#" . curl_errno ( $ch ) . " \n"; echo curl_error ( $ch ) . " \n"; echo "Dokładne informacje:"; var_dump ( curl_getinfo ( $ch ) ); die ();
205
ROZDZIAŁ 10. BIBLIOTEKI
} curl_close ( $ch ); return $dane; } ?>
Na listingu 10.9 za pomocą polecenia curl_init(), tworzymy uchwyt do obiektu cURL. Następnie konfigurujemy ustawienia cURL za pomocą kolejnych wywołań funkcji curl_setopt. Funkcja curl_exec wykonuje żądanie i zwraca rezultat. W końcu sprawdzamy, czy rezultatem nie jest null. Jeżeli jest, wykorzystujemy metody curl_errno, curl_error i curl_getinfo, aby uzyskać szczegóły. Funkcja curl_getinfo zawiera informacje o żądaniu. Typowy błąd wyglądałby następująco: cURL error: #6 Could not resolve host: www.zhelionz.pl; Host not found
Opcje konfiguracji cURL są bardzo rozbudowane. Oto niektóre z nich: curl_setopt( $ch, CURLOPT_POST, true ); //żądanie POST curl_setopt( $ch, CURLOPT_POSTFIELDS, "key1=value1&key2=value2" ); //pary klucz/wartość POST curl_setop($ch, CURLOPT_USERPWD, "username:password" ); //dla stron z uwierzytelnianiem //niektóre strony blokują żądania bez ustawionej informacji o przeglądarce curl_setopt( $ch, CURLOPT_USERAGENT, $userAgent );
Jeżeli nie potrzebujemy rozbudowanej konfiguracji, a w pliku php.ini włączona jest opcja file_get_contents, skrypt z listingu 10.9 może być zredukowany do postaci z listingu 10.10. Listing 10.10. Uproszczone pobieranie treści za pomocą file_get_contents
W kolejnym skrypcie, który rozszerza skrypt z listingu 10.9, sparsujemy dane i wyświetlimy rezultat. W tym przypadku wyszukamy wszystkie linki oraz ich tytuły zawarte na stronie (listing 10.11). Listing 10.11. Wykorzystanie cURL, DOMDocument i DOMXPath do wyszukania linków na stronie
206
ROZDZIAŁ 10. BIBLIOTEKI
//funkcja pobierająca dane function pobierzDane($url) { $ch = curl_init (); curl_setopt ( $ch, CURLOPT_URL, $url ); curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true ); //zwróć wynik jako zmienną curl_setopt ( $ch, CURLOPT_FAILONERROR, true ); //przerwij działanie w przypadku wystąpienia błędu curl_setopt ( $ch, CURLOPT_FOLLOWLOCATION, true ); //zezwól na przekierowania curl_setopt ( $ch, CURLOPT_TIMEOUT, 10 ); //maksymalny dozwolony czas wykonywania $dane = curl_exec ( $ch ); if (! $dane) { echo " Błąd cURL: \n"; echo "#" . curl_errno ( $ch ) . " \n"; echo curl_error ( $ch ) . " \n"; echo "Dokładne informacje:"; var_dump ( curl_getinfo ( $ch ) ); die (); } curl_close ( $ch ); return $dane; } //funkcja parsująca function parsujDane($dane) { $sparsowaneDane = array (); //wczytanie do DOM $dom = new DOMDocument (); @$dom->loadHTML($dane); //nie sprawdza błędów $xpath = new DOMXPath ( $dom ); $linki = $xpath->query ( "/html/body//a" ); if ($linki) { foreach ( $linki as $element ) { $nody = $element->childNodes; $link = $element->attributes->getNamedItem ( 'href' )->value; foreach ( $nody as $nod ) { if ($nod instanceof DOMText) { $sparsowaneDane [] = array ("title" => $nod->nodeValue, "href" => $link ); } } } } return $sparsowaneDane; } //funkcja wyświetlająca function wyswietlDane(Array $dane) { foreach ( $dane as $link ) { //escape output $wyczyszczony_tytul = htmlentities ( $link ['title'], ENT_QUOTES, "UTF-8" ); $wyczyszczony_link = htmlentities ( $link ['href'], ENT_QUOTES, "UTF-8" ); echo "
Skrypt z listingu 10.11 wczytuje dane do obiektu DOMDocument. Następnie wywołujemy metodę loadHTML i wykorzystujemy operator kontroli błędów — @. UWAGA. W normalnych przypadkach nie używamy operatora kontroli błędów, ponieważ powoduje to problemy podczas poprawiania błędów. Jednakże tutaj operator ten ukrywa wiele ostrzeżeń generowanych przez obiekt DOMDocument, które nas nie interesują.
Następnie wykorzystujemy obiekt DOMXPath do wyszukania linków i odpowiadających im tekstów, które wstawiamy to tablicy. Ponieważ dane pochodzą ze źródła zewnętrznego, nie powinniśmy im ufać. Stosujemy znaki ucieczki dla wszystkich wartości wypisywanych na ekranie. Jest to najlepszy sposób, aby zapobiec atakom typu XSS (Cross-Site Scripting) omówionym w rozdziale 11. Poniżej znajduje się uproszczony wynik wywołania skryptu z listingu 10.11. Świąteczne prezenty http://helion.pl/kategorie/swiateczne-prezenty/swiateczne-prezenty Aplikacje biurowe http://helion.pl/kategorie/aplikacje-biurowe Bazy danych http://helion.pl/kategorie/bazy-danych Biznes IT http://helion.pl/kategorie/biznes-it CAD/CAM http://helion.pl/kategorie/cad-cam
Pokażemy teraz, w jaki sposób wykorzystać bibliotekę phpQuery umożliwiającą zastosowanie selektorów podobnych jak w jQuery (listing 10.12). To upraszcza parsowanie w naszym skrypcie. Musisz najpierw pobrać bibliotekę dostępną pod adresem http://code.google.com/p/phpquery/. Listing 10.12. Wykorzystanie cURL i phpQuery do wyszukania linków na stronie Błąd cURL: \n"; echo "#" . curl_errno ( $ch ) . " \n"; echo curl_error ( $ch ) . " \n"; echo "Dokładne informacje:"; var_dump ( curl_getinfo ( $ch ) );
Zauważ, że jedyną różnicą między listingami 10.11 i 10.12 jest funkcja parsująca. Użyliśmy phpQuery zamiast obiektu DOMDocument za pomocą metody newDocumentHTML: phpQuery::newDocumentHTML ( $dane );
Nie będziemy omawiać wszystkich możliwości biblioteki phpQuery. Zamiast tego porównamy notacje XPath, phpQuery i jQuery (tabela 10.1). Tabela 10.1. Porównanie notacji XPath, phpQuery i jQuery phpQuery
jQuery
XPath
Ogólny selektor
pq()
$()
query()
Wybranie linków
pq("a")
$("a")
query("/html/body//a")
Atrybut HREF
pq($link)->attr('href')
$("a").attr('href');
getNamedItem('href')->value
Tekst
pq($link)->text();
$("a").text();
nodeValue
Integracja z Mapami Google Aby wykorzystać Mapy Google, użyjemy biblioteki php-google-map-api dostępnej pod adresem http://code.google.com/p/php-google-map-api/. Bezpośredni link do aktualnej wersji 3.0 nie jest obecnie dostępny. Konieczne będzie użycie klienta SVN do pobrania najnowszych źródeł — zastosuj następującą komendę:
Klienty SVN to np. tortoiseSVN (http://tortoisesvn.net/downloads.html) oraz slik svn (http://www.sliksvn.com/en/download). Biblioteka php-google-map-api jest bardzo rozbudowana i wciąż się rozwija. Utworzymy ogólny szablon, który zostanie wykorzystany w przykładowych skryptach do wyświetlenia wyników ich działania (listing 10.13). Listing 10.13. Szablon szablon_gmap.php
W pierwszym przykładzie wyświetlimy mapę Google z jednym markerem (listing 10.14). Listing 10.14. Obraz z satelity — przykład z jednym markerem addMarkerByAddress( "Łazienki Królewskie w Warszawie, Warszawa", "Łazienki Królewskie - tytuł", "Łazienki Królewskie - opis" ); require_once('szablon_gmap.php'); ?>
Jak widzisz, wyświetlenie mapy z użyciem tej biblioteki jest bardzo łatwe. Na listingu 10.14 tworzymy nowy obiekt GoogleMapAPI i podajemy adres. Metoda addMarkerByAddress przyjmuje tytuł i opis jako dodatkowe argumenty. Wynik działania skryptu pokazuje rysunek 10.6. W skrypcie z listingu 10.15 zamiast widoku z satelity pokażemy mapę. Ustawimy także poziom zbliżenia oraz pokażemy natężenie ruchu drogowego. Wynik przedstawiono na rysunku 10.7. Listing 10.15. Natężenie ruchu drogowego na mapie Google addMarkerByAddress( "New York, NY", "Natężenie ruchu w Nowym Jorku", "Opis natężenia ruchu" ); $gmap->setMapType( 'map' ); $gmap->setZoomLevel( 15 );
210
ROZDZIAŁ 10. BIBLIOTEKI
Rysunek 10.6. Mapa z zaznaczonymi Łazienkami Królewskimi $gmap->enableTrafficOverlay(); require_once('szablon_gmap.php'); ?>
W ostatnim przykładzie umieścimy kilka markerów na tej samej mapie. Typ mapy ustawimy tak, aby pokazywała teren. Zobacz listing 10.16, którego wynik został pokazany na rysunku 10.8. Listing 10.16. Wiele znaczników na jednej mapie addMarkerByAddress( "Warszawa", "", "Dom" ); $gmap->addMarkerByAddress( "Wrocław", "", "Uczelnia" ); $gmap->addMarkerByAddress( "Kraków", "", "Wawel" ); $gmap->addMarkerByAddress( "Gdynia", "", "Wakacje" ); $gmap->setMapType( 'terrain' ); require_once('szablon_gmap.php'); ?>
Wiadomości e-mail i SMS PHP ma wbudowaną funkcję mail pozwalającą na wysyłanie wiadomości. Jednakże w przypadku bardziej złożonych ustawień serwera lub poczty zewnętrzna biblioteka umożliwia obiektowe (łatwiejsze) tworzenie e-maili. Biblioteka PHPMailer pozwala na łatwe wysyłanie wiadomości e-mail i SMS. Można ją pobrać pod adresem http://sourceforge.net/projects/phpmailer/files/phpmailer%20for%20php5_6/. 211
ROZDZIAŁ 10. BIBLIOTEKI
Rysunek 10.7. Natężenie ruchu w Nowym Jorku
Rysunek 10.8. Teren i znaczniki na mapie Google Skrypt z listingu 10.17 pokazuje podstawowe wykorzystanie biblioteki.
212
ROZDZIAŁ 10. BIBLIOTEKI
Listing 10.17. Podstawowe wykorzystanie biblioteki PHPMailer From = "[email protected]"; $mail->AddAddress( "[email protected]" ); $mail->Subject = "Wiadomość PHPMailer"; $mail->Body = "Witaj, świecie!\n Mam nadzieję, że śniadanie to nie spam."; if( $mail->Send() ) { echo 'Wiadomość została wysłana.'; } else { echo 'Wiadomość nie została wysłana z powodu błędu: '; echo $mail->ErrorInfo; } ?>
UWAGA. Jeżeli wyświetli się komunikat Could not instantiate mail function, to jego przyczyną najprawdopodobniej jest adres e-mail niepoprawnie wpisany na serwerze, z którego wysyłasz wiadomość.
W kolejnym przykładzie wyślemy wiadomość w HTML-u oraz załącznik (listing 10.18). Listing 10.18. Wiadomość HTML z załącznikiem From = "[email protected]"; $mail->AddAddress( "[email protected]" ); $mail->Subject = "Wiadomość PHPMailer"; $mail->IsHTML(); //wykorzystujemy HTML $mail->Body = "Witaj, świecie! Mam nadzieję, że śniadanie to nie spam."; //wiadomość zapasowa, na wypadek gdyby klient nie obsługiwał wiadomości HTML $mail->AltBody = "Witaj, świecie!\n Mam nadzieję, że śniadanie to nie spam."; //dodanie załącznika $mail->AddAttachment( "dokument.txt" ); if( $mail->Send() ) { echo 'Wiadomość została wysłana.'; } else { echo 'Wiadomość nie została wysłana z powodu błędu: '; echo $mail->ErrorInfo; } ?>
213
ROZDZIAŁ 10. BIBLIOTEKI
W skrypcie z listingu 10.19 użyjemy serwera SMTP z uwierzytelnianiem. Ponadto wykorzystamy w pętli tablicę z adresami, aby wysłać serię wiadomości. Listing 10.19. Wysyłanie poczty seryjnej przy zastosowaniu SMTP IsSMTP(); //wykorzystanie SMTP $mail->Host = "smtp.przyklad.pl"; // adres serwera SMTP //uwierzytelnianie na serwerze SMTP $mail->SMTPAuth = true; $mail->Username = "brian"; $mail->Password = "hasloBriana"; $mail->From = "[email protected]"; $mail->Subject = "Wiadomość PHPMailer"; $adresy = array( array( "email" array( "email" array( "email" array( "email" );
foreach ( $adresy as $a ) { $mail->AddAddress( $a['email'] ); $mail->Body = "Witaj, {$a['nazwa']}!\n Czy podoba ci się mój serwer SMTP?"; if( $mail->Send() ) { echo 'Wiadomość została wysłana.'; } else { echo 'Wiadomość nie została wysłana z powodu błędu: '; echo $mail->ErrorInfo; } $mail->ClearAddresses(); } ?>
W ostatnim przykładzie wykorzystania biblioteki PHPMailer pokażemy (na listingu 10.20), w jaki sposób wysłać krótką wiadomość tekstową — SMS. Aby wysłać SMS, musimy znać numer telefonu odbiorcy oraz adres bramki SMS. Listing 10.20. Wysyłanie wiadomości SMS za pośrednictwem biblioteki PHPMailer IsSMTP(); $mail->Host = "smtp.przyklad.pl"; $mail->SMTPAuth = true;
214
ROZDZIAŁ 10. BIBLIOTEKI
$mail->Username = "brian"; $mail->Password = "hasloBriana"; $mail->From = "[email protected]"; $mail->Subject = "Wiadomość PHPMailer"; $numer_telefonu = "z+a 555 kfla555-@#122"; $czysty_numer_telefonu = filter_var( $numer_telefonu, FILTER_SANITIZE_NUMBER_INT ); //+555555-122 $czystszy_numer_telefonu = str_replace( array( '+' , '-' ), '', $czysty_numer_telefonu ); //555555122 $bramka_sms = "@sms.falszywyDostawca.pl"; //[email protected] $mail->AddAddress( $czystszy_numer_telefonu . $bramka_sms ); $mail->Body = "Witaj, odbiorco!\r\n oto tekst"; if ( strlen( $mail->Body ) < MAX_SMS_MESSAGE_SIZE ) { if ( $mail->Send() ) { echo 'Wiadomość została wysłana.'; } else { echo ''Wiadomość nie została wysłana z powodu błędu: '; echo $mail->ErrorInfo; } } else { echo "Twoja wiadomość jest zbyt długa."; } ?>
Najpierw sprawdzamy, czy numer telefonu zawiera wyłącznie cyfry. Wykorzystujemy funkcję filter_var, która usuwa z ciągu wszystkie znaki poza cyframi, plusem i minusem. Następnie używamy funkcji str_replace, aby usunąć wszystkie znaki plus i minus. Definiujemy także maksymalną długość wiadomości i sprawdzamy, czy wstawiany tekst jest krótszy. Łączymy wyczyszczony numer telefonu z domeną bramki SMS i wykorzystujemy to jako adres wiadomości. UWAGA. Większość bramek SMS wymaga, aby długość numeru telefonu wynosiła dziewięć cyfr bez żadnych innych znaków. To oznacza, że nie można podać kodu kraju. Można dodać sprawdzanie, czy długość numeru wynosi dziesięć znaków.
gChartPHP — biblioteka wykorzystująca Google Chart API Biblioteka Google Chart API jest bardzo prostą w użyciu i potężną biblioteką do generowania dynamicznych grafów i wykresów. Biblioteka gChartPHP pozwala korzystać z Google Chart API za pośrednictwem notacji obiektowej, przez co jest jeszcze łatwiejsza w użyciu i mniej podatna na błędy. Dzięki Chart API obrazy generuje Google, co odciąża Twój serwer. Możesz pobrać bibliotekę gChartPHP pod adresem http://code.google.com/p/gchartphp/. Więcej informacji na temat Google Chart API można uzyskać na stronie http://code.google.com/intl/pl/apis/chart/. Google Chart API pozwala na generowanie takich wykresów, jak: liniowy, słupkowy, kołowy, mapa, punktowy, wykres Venna, radarowy, kody QR, google-o-meter, mieszany, świecowy oraz GraphViz. Pokażemy, w jaki sposób wygenerować mapę i wykres świecowy.
215
ROZDZIAŁ 10. BIBLIOTEKI
Mapa jest podobna jak w narzędziu Google Analytics. Kraje, które chcemy zaznaczyć, wyróżnione są wartościami gradientu pomiędzy dwoma kolorami. Dane, które przypiszemy, będą decydować o tym, jaki odcień otrzyma określony kraj. Jest to użyteczne, jeżeli chcemy pokazać, które kraje mają największy udział w statystyce (listing 10.21). Listing 10.21. Wyświetlenie kolorowej mapy wybranych krajów Europy setZoomArea( 'europe' ); //obszar geograficzny //Włochy, Polska, Wielka Brytania, Hiszpania, Finlandia $mapa->setStateCodes( array( 'IT', 'PL', 'GB', 'ES', 'FI') ); $mapa->addDataSet( array( 50, 100, 24, 80, 65 ) ); //poziom natężenia koloru $mapa->setColors( 'E7E7E7', //kolor domyślny array('0077FF', '000077') //zakres kolorów ); echo "getUrl() . "\" /> Europa"; ?>
Na listingu 10.21 tworzymy nowy obiekt gMapChart i ustawiamy obszar geograficzny na Europę. Następnie dodajemy kody krajów. Lista skrótów znajduje się na stronie http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2. Jeżeli chcemy, aby wszystkie kraje miały ten sam kolor, należy ustawić wszystkie wartości takie same. W kolejnym kroku ustalamy kolory. Zakres kolorów zawiera się pomiędzy jasnoniebieskim a ciemnoniebieskim. W końcu wstawiamy wygenerowany adres obrazka do znacznika img. Rysunek 10.9 pokazuje mapę wygenerowaną za pomocą skryptu z listingu 10.21. UWAGA. Długość zapytania ograniczona jest maksymalną długością żądania GET. Google Chart API i gMapChart udostępniają metody wysyłania żądania POST. Jedną z nich jest wykorzystanie renderImage(true).
Rysunek 10.9. Mapa wygenerowana za pomocą Google Chart API
216
ROZDZIAŁ 10. BIBLIOTEKI
Drugim i ostatnim przykładem będzie wygenerowanie wykresu świecowego. Wykresy świecowe wymagają przynajmniej czterech serii danych i często wykorzystywane są do obrazowania danych giełdowych (rysunek 10.10).
Najpierw tworzymy obiekt gLineChart, następnie definiujemy zestaw danych dla wykresu liniowego oraz cztery ukryte zestawy danych, które zostaną wykorzystane do wygenerowania wykresu świecowego. Później dodajemy markery świec, a na końcu ustalamy właściwości osi i wyświetlamy wygenerowany wykres. Wynik pokazano na rysunku 10.11.
Rysunek 10.11. Wykres świecowy wygenerowany za pomocą skryptu z listingu 10.22 218
ROZDZIAŁ 10. BIBLIOTEKI
Podsumowanie W tym rozdziale przedstawiliśmy użyteczne biblioteki PHP. Pokazaliśmy, w jaki sposób można skorzystać z gotowych rozwiązań. Użyliśmy bibliotek do integracji z Mapami Google oraz Google Chart API. Sparsowaliśmy kanał RSS i pobraliśmy dane ze strony WWW. Generowaliśmy dokumenty PDF, e-maile i wiadomości SMS. Zastosowanie gotowych bibliotek pozwala na szybkie tworzenie programów na wysokim poziomie. Wadą jest to, że nie mamy kontroli nad kodem, z którego korzystamy. Musimy wierzyć, że kod nie jest szkodliwy ani błędny. Być może kusi Cię, aby napisać własną bibliotekę — może np. potrzebujesz w swojej aplikacji jakiejś funkcjonalności lub dane rozwiązanie nie spełnia Twoich oczekiwań. Takie podejście jest jednak często stratą czasu. Lepiej uczestniczyć w tworzeniu poprawek i nowych funkcjonalności oprogramowania open source.
219
ROZDZIAŁ 10. BIBLIOTEKI
220
ROZDZIAŁ 11
Bezpieczeństwo
Podczas tworzenia aplikacji internetowych ważną kwestię stanowi bezpieczeństwo. Istnieje wiele słabych punktów, które napastnik będzie chciał wykorzystać. Dobry programista musi być na bieżąco zaznajomiony z zagadnieniami bezpieczeństwa. W tym rozdziale omówimy kilka dobrych praktyk i technik służących bezpieczeństwu strony. Rozdziałowi temu towarzyszy przesłanie, że nigdy nie wolno ufać danym ani intencjom użytkowników. Dane, które musimy przefiltrować, mogą pochodzić z różnych źródeł — z adresów URL, formularzy, cookies, sesji, zmiennych serwerowych oraz zapytań Ajax. Omówimy najczęściej występujące rodzaje ataków i metody zapobiegania im: • Zapobieganie atakom typu XSS (Cross Site Scripting) przez wstawianie znaków ucieczki w wyświetlanych danych. • Zapobieganie atakom typu CSRF (Cross Site Request Forgery) przez wykorzystanie tokenów zapisanych w ukrytych polach formularza. • Zapobieganie podstawieniu sesji przez nieprzechowywanie identyfikatora sesji w pliku cookie, lecz odtwarzanie go na początku każdej strony. • Zapobieganie atakom typu SQL injection dzięki wykorzystaniu przygotowanych zapytań i PDO. • Zastosowanie rozszerzenia filtrów. Wyjaśnimy także, jak uszczelnić plik php.ini, jak zabezpieczyć ustawienia serwera, a także algorytmy haszujące hasła.
Nigdy nie ufaj danym W telewizyjnym serialu Z Archiwum X Fox Mulder wypowiedział słynne zdanie: „Nie ufaj nikomu”. Powinniśmy przestrzegać tej zasady również w odniesieniu do aplikacji internetowych. Zakładaj najgorszy scenariusz — wszystkie dane są skażone. Cookies, żądania Ajax, nagłówki, dane z formularzy (nawet przesyłane metodą POST) — mogą być sfałszowane lub zmanipulowane. Nawet jeżeli użytkownicy są całkowicie zaufani, i tak powinniśmy zadbać o to, aby wszystkie dane zostały poprawnie wprowadzone i sformatowane. W związku z tym należy filtrować wprowadzane dane oraz wstawiać znaki ucieczki w danych wyświetlanych na stronie. W dalszej części rozdziału omówimy niektóre z nowych funkcji filtrujących PHP, które znacznie ułatwiają to zadanie. Przeanalizujemy także konfigurację php.ini zwiększającą bezpieczeństwo. Jeżeli jednak piszemy kod, który będzie udostępniony innym programistom, nie możemy mieć pewności, że osoby z niego korzystające będą przestrzegały zasad bezpiecznej konfiguracji pliku php.ini. Z tego powodu trzeba zawsze programować defensywnie i zakładać, że plik php.ini nie został zabezpieczony.
ROZDZIAŁ 11. BEZPIECZEŃSTWO
register_globals Dobrą praktyką jest każdorazowe inicjowanie zmiennych. To zabezpiecza przed możliwymi atakami, jeżeli włączona jest opcja register_globals w pliku php.ini. Gdy jest ona włączona, zmienne $_POST oraz $_GET są rejestrowane jako zmienne globalne w skrypcie. Jeżeli zmodyfikujesz adres, np. ?test=3, PHP utworzy zmienną globalną o takiej nazwie: $test=3; //register_globals utworzy taką zmienną globalną
Przy włączonym register_globals wywołanie adresu http://test.pl/logowanie.php?czy_admin=true w przypadku skryptu z listingu 11.1 spowoduje nadanie uprawnień administratora. Listing 11.1. Ominięcie sprawdzania logowania — listing11_1.php
Aby atak się udał, napastnik musiałby odgadnąć poprawną nazwę zmiennej $czy_admin. Jeżeli wykorzystywana jest znana biblioteka, napastnik może łatwo poznać nazwy zmiennych po przestudiowaniu API lub pełnego kodu biblioteki. Sposobem zapobieżenia tego typu atakom jest inicjowanie wszystkich zmiennych — tak jak na listingu 11.2. To zapewnia, że register_globals nie nadpisze istniejących zmiennych. Listing 11.2. Inicjacja zmiennych w celu zapobieżenia atakom za pośrednictwem register_globals
Białe i czarne listy Nie powinniśmy wykorzystywać wartości $_POST i $_GET do wywoływania funkcji include i require, ponieważ nazwy załączanych plików nie będą dla nas znane. Napastnik mógłby spróbować obejść zabezpieczenie katalogu głównego, poprzedzając nazwy pliku prefiksem ../../. Dla zmiennych przekazywanych jako parametr funkcji include i require powinniśmy mieć białą listę akceptowanych wartości lub bezpieczne nazwy.
222
ROZDZIAŁ 11. BEZPIECZEŃSTWO
UWAGA. Biała lista jest listą akceptowanych wartości. Czarna lista jest listą niepożądanych wartości. Białe listy są bezpieczniejsze niż czarne, ponieważ określają dokładnie, jakie wartości mogą być użyte. Aby czarne listy mogły być skuteczne, muszą być bezustannie aktualizowane. Przykładami białych list mogą być listy akceptowanych adresów e-mail, nazw domen lub tagów HTML. Przykładami czarnych list mogą być listy zabronionych adresów e-mail, nazw domen czy tagów HTML.
Listing 11.3 przedstawia sposób wykorzystania białej listy dozwolonych nazw plików. Listing 11.3. Ograniczenie możliwości załączania plików do plików z listy
Funkcja basename pozwala upewnić się, że pliki używane przez nasz skrypt nie są umiejscowione poza katalogiem głównym. Dla zewnętrznych adresów URL przekazywanych przez użytkownika i wykorzystywanych za pomocą file_get_contents musimy filtrować nazwę pliku. Możemy zastosować funkcję parse_url, aby wyekstrahować adres URL, lub użyć FILTER_SANITIZE_URL i FILTER_VALIDATE_URL, aby zabezpieczyć się przed niepoprawnym adresem. Filtry zostaną omówione w dalszej części rozdziału.
Dane formularzy Zapewne zdajesz sobie sprawę z tego, że dane przekazywane z formularzy przez metodę GET mogą być modyfikowane ręcznie w pasku adresu. Z reguły jest to pożądane zachowanie. Na przykład formularz wyszukiwania strony http://stackoverflow.com może być wykorzystany poprzez modyfikację adresu (listing 11.4). Listing 11.4. Wyszukiwanie na stronie php.pl poprzez modyfikację paska adresu http://stackoverflow.com/search?q=php+xss
Kod HTML dla formularza ze strony został pokazany na listingu 11.5. Listing 11.5. Formularz wyszukiwania na stronie stackoverflow.com
Takie same rezultaty wyszukiwania można uzyskać za pomocą programu telnet. Listing 11.6 pokazuje sposób wywołania żądania GET.
223
ROZDZIAŁ 11. BEZPIECZEŃSTWO
Listing 11.6. Polecenia telnet wysyłające żądanie GET telnet stackoverflow.com 80 GET /search?q=php+xss HTTP/1.1 Host: stackoverflow.com
Częstym błędem jest mniemanie, że formularze wykorzystujące metodę POST są bezpieczniejsze. Pomimo że nie można ich bezpośrednio modyfikować poprzez zmianę adresu, nadal można przesłać żądanie POST za pośrednictwem programu telnet. Gdyby poprzedni formularz wykorzystywał metodę POST (
"; }
226
ROZDZIAŁ 11. BEZPIECZEŃSTWO
} ?>
Jeżeli w pierwszym polu wstawimy wartość: "><"
a drugie pole pozostawimy puste, formularz nie przejdzie walidacji. Pole zostanie uzupełnione wstawioną przez nas wartością zawierającą skrypt. Wygenerowany HTML będzie wyglądał jak na listingu 11.11. Listing 11.11. Skrypt XSS wstawiony do kodu HTML
Napastnikowi udało się wstawić skrypt na naszą stronę. Możemy temu zapobiec, zamieniając znaki specjalne na encje HTML: ${$pole} = htmlspecialchars( $_POST[$pole], ENT_QUOTES, "UTF-8" );
To eliminuje zagrożenie — powstaje niegroźny kod przedstawiony na listingu 11.12. Listing 11.12. Rozwiązanie problemu ze skryptem XSS przez wykorzystanie funkcji htmlspecialchars
• Zmienne zawarte w adresie URL mogą być łatwo wykorzystane do przeprowadzenia ataku XSS, jeżeli nie zostaną poprawnie zabezpieczone. Rozważmy przykładowy adres URL: http://www.test.pl?uzytkownik=
A oto kod PHP:
Zapobieganie atakom XSS Aby zapobiec atakom XSS, musimy zabezpieczyć wszystkie dane wyświetlane na stronie, do których użytkownik mógł wstawić niepożądane skrypty. Oznacza to dane pochodzące z formularza, ze zmiennej $_GET oraz wpisów w księgach gości i komentarzach, w których dozwolony jest kod HTML. Aby usunąć HTML z wyświetlanego tekstu zawartego w zmiennej $nasz_teskt, możemy zastosować funkcję: htmlspecialchars( $nasz_tekst, ENT_QUOTES, 'UTF-8' )
227
ROZDZIAŁ 11. BEZPIECZEŃSTWO
Możemy także wykorzystać filter_var( $nasz_tekst, FILTER_SANITIZE_STRING ). Funkcje filtrujące omówimy w dalszej części rozdziału. Aby zapobiec atakom XSS, ale zapewnić przy tym użytkownikom więcej swobody, możemy użyć biblioteki PHP o nazwie HTML Purifier. Bibliotekę tę możemy znaleźć pod adresem http://htmlpurifier.org/.
CSRF (Cross-Site Request Forgery) Atak CSRF jest przeciwieństwem XSS — wykorzystuje się w nim zaufanie strony do użytkownika. Atak tego typu jest związany z fałszowanym żądaniem HTTP i często ma miejsce w obrębie znacznika img.
Przykład ataku CSRF Wyobraź sobie, że użytkownik odwiedza stronę zawierającą następujący kod:
Przy odbieraniu obrazka strona otwiera adres URL zapisany w atrybucie src. Zamiast adresu obrazka odwiedzana jest strona PHP, do której przekazywane są parametry. Jeżeli użytkownik niedawno odwiedzał stronę atakowanybank.pl i nadal ma ustawione pliki cookies, zlecenie przelewu mogłoby zostać wykonane. Bardziej skomplikowane ataki wykorzystują metodę POST poprzez bezpośrednie żądania HTTP. Problemem dla stron zagrożonych atakami CSRF jest odróżnienie poprawnych żądań od niepoprawnych.
Zapobieganie atakom CSRF Najpowszechniejszą metodą zapobiegania atakom CSRF jest generowanie tokenu sesji podczas generowania identyfikatora sesji, jak pokazano na listingu 11.13. Następnie token ustawiany jest w ukrytym polu formularza. Kiedy formularz jest zatwierdzany, sprawdzamy, czy poprawny token został wysłany wraz z formularzem. Sprawdzamy także, czy formularz został wysłany w zadanym czasie. Listing 11.13. Przykład formularza z ukrytym tokenem
Następnie sprawdzamy, czy token zgadza się z wartością zapisaną w sesji oraz czy czas wysłania formularza nie przekracza dozwolonego przedziału (listing 11.14). Listing 11.14. Sprawdzenie poprawności tokenu
228
ROZDZIAŁ 11. BEZPIECZEŃSTWO
//żądanie poprawne - może zostać przetworzone } } ?>
Sesje Podmiana sesji następuje, kiedy użytkownik otrzymuje identyfikator sesji innej osoby. Częstym sposobem na podmianę sesji jest wykorzystanie XSS lub zapisanie identyfikatora w pliku cookie. Identyfikatory sesji mogą być podawane w pasku adresu (np. /index.php?PHPSESSID=1234abcd) albo być podsłuchane przez napastnika. Aby zabezpieczyć się przed podmianą sesji, możemy powtórnie generować sesję na początku każdego skryptu oraz ustawić odpowiednie dyrektywy w php.ini. W plikach PHP możemy zamieniać identyfikator sesji na nowy, zachowując przy tym dane zapisane w sesji (listing 11.15). Listing 11.15. Powtórne generowanie identyfikatora sesji na początku każdego skryptu
W pliku php.ini możemy wyłączyć wykorzystanie plików cookies do przechowywania identyfikatorów sesji. Zapobiegamy także pojawianiu się identyfikatora w adresie URL. session.use_cookies = 1 session.use_only_cookies = 1 session.use_trans_sid = 0
UWAGA. Dyrektywa session.gc_maxlifetime oparta jest na mechanizmie garbage collection. Aby uzyskać lepszą spójność, lepiej samemu śledzić czas rozpoczęcia sesji i zwalniać ją w określonym momencie.
Aby zapobiec atakom z podmianą sesji, możemy także przechowywać niektóre wartości ze zmiennej $_SERVER, konkretnie REMOTE_ADDR, HTTP_USER_AGENT i HTTP_REFERER. Następnie sprawdzamy poprawność pól na początku każdego skryptu. Jeżeli wartości przechowywane i te na serwerze się nie zgadzają, sesja została prawdopodobnie zmanipulowana i możemy ją zakończyć przy wykorzystaniu session_destroy();. Ostatecznym zabezpieczeniem jest zaszyfrowanie danych sesji po stronie serwera. Dzięki temu podsłuchane dane sesji będą bezużyteczne dla każdego, kto nie dysponuje kluczem deszyfrującym.
Zapobieganie atakom typu SQL injection Atak typu SQL injection może nastąpić, gdy dane nie są zabezpieczone przed wykorzystaniem ich w zapytaniu SQL. Wstawiony kod SQL oddziałuje na bazę w sposób niezamierzony. Klasycznym przykładem ataku SQL injection jest wstawienie kodu SQL przez pasek adresu: $sql = "SELECT * FROM KontoBankowe WHERE uzytkownik = '{$_POST['uzytkownik'] }'";
Jeżeli napastnik może odgadnąć lub ustalić (na podstawie wyświetlanych informacji o błędach bądź komunikatów debugera) nazwy pól tabel odpowiadające odpowiednim polom formularza, to atak SQL injection jest możliwy. Na przykład wstawienie do pola formularza wartości test' OR uzytkownik = 'test2 bez zabezpieczenia ciągu spowoduje zinterpretowanie zapytania jako: $sql = "SELECT * FROM KontoBankowe WHERE uzytkownik = 'test' OR uzytkownik = 'test2'";
229
ROZDZIAŁ 11. BEZPIECZEŃSTWO
Dzięki temu napastnik będzie w stanie zobaczyć informacje z dwóch różnych kont. Jeszcze gorsze byłoby wstawienie wartości test' OR uzytkownik = uzytkownik, co spowoduje zinterpretowanie zapytania jako: $sql = "SELECT * FROM KontoBankowe WHERE uzytkownik = 'test' OR uzytkownik = uzytkownik;
Ponieważ warunek uzytkownik = uzytkownik jest zawsze prawdziwy, cała klauzula WHERE zawsze będzie prawdziwa. Zapytanie zwróci wszystkie rekordy z tabeli KontoBankowe. Możliwe są także wstawienia, które mogą modyfikować lub usuwać dane. Rozważmy zapytanie: $sql = "SELECT * FROM KontoBankowe WHERE id = $_POST['id'] ";
i następującą wartość w zmiennej $_POST: $_POST['id']= "1; DROP TABLE `KontoBankowe`;"
Bez zabezpieczenia zmiennej zapytanie będzie wyglądało tak: SELECT * FROM KontoBankowe WHERE id = 1; DROP TABLE `KontoBankowe`;
Wskutek wstawienia powyższej wartości do zmiennej tabela zostanie usunięta z bazy danych. Jeżeli możesz, powinieneś używać pól zamiennych, takich jak w przypadku PHP Data Objects. Z punktu widzenia bezpieczeństwa PDO pozwala na wykorzystanie pól zamiennych, przygotowanych zapytań oraz przypisywanie danych. Rozważmy trzy powyższe warianty zapytań z zastosowaniem PDO zgodnie z listingiem 11.16. Listing 11.16. Trzy różne sposoby wykonania tego samego polecenia w PDO query( "SELECT * FROM KontoBankowe WHERE uzytkownik = ´'{$_POST['uzytkownik']}' " ); //nienazwane pola zamienne $zapytanie = $pdo_dbh->prepare( "SELECT * FROM KontoBankowe WHERE uzytkownik = ? " ); $zapytanie->execute( array( $_POST['uzytkownik'] ) ); //nazwane pola zamienne $zapytanie = $pdo_dbh->prepare( "SELECT * FROM KontoBankowe WHERE uzytkownik = :uzytkownik " ); $zapytanie->bindParam(':uzytkownik', $_POST['uzytkownik']); $zapytanie->execute( );
PDO udostępnia także funkcję quote: $bezpieczniejsze_zapytanie = $pdo_dbh->quote($niezabezpieczone_zapytanie);
Poza PDO istnieją również inne możliwości dla funkcji quote. Dla bazy MySQL można wykorzystać funkcję mysql_real_escape_string, dla bazy PostgreSQL — funkcje pg_escape_string i pg_escape_bytea. Aby użyć funkcji zabezpieczających dla MySQL lub PostgreSQL, musisz mieć włączoną odpowiednią bibliotekę w pliku php.ini. Jeżeli zastosowanie funkcji mysql_real_escape_string nie jest możliwe, wykorzystaj addslashes. Pamiętaj jednak, że funkcja mysql_real_escape_string radzi sobie lepiej z zabezpieczaniem zapytań i ze związanymi z tym problemami niż funkcja addslashes — jest bezpieczniejsza.
Wyrażenia filtrujące Wyrażenia filtrujące (zostały one udostępnione w PHP 5.2) i funkcja filter_var były wstępnie omówione w rozdziale 6., dotyczącym formularzy. Tutaj przybliżymy je wraz z opcjonalnymi flagami FILTER_FLAGS. Filtry dostępne w rozszerzeniu dotyczą albo walidacji, albo czyszczenia danych. Filtry walidujące zwracają podany ciąg w przypadku zakończenia walidacji sukcesem lub fałsz w przeciwnym razie. Filtry czyszczące usuwają niepoprawne znaki i zwracają zabezpieczony ciąg. Rozszerzenie filtrów posiada dwie dyrektywy w pliku php.ini — filter.default i filter.default_flags, których wartości domyślne to: 230
Dyrektywy te przefiltrują wszystkie zmienne superglobalne: $_GET, $_POST, $_COOKIE, $_SERVER i $_REQUEST. Filtr czyszczący unsafe_raw domyślnie nie robi nic, można jednak ustawić następujące flagi: FILTER_FLAG_STRIP_LOW FILTER_FLAG_STRIP_HIGH FILTER_FLAG_ENCODE_LOW FILTER_FLAG_ENCODE_HIGH FILTER_FLAG_ENCODE_AMP
//Usuń wszystkie znaki ASCII mniejsze niż 32 (znaki niedrukowalne). //Usuń wszystkie znaki ASC większe niż 127 (rozszerzone ASCII). //Zakoduj wartości mniejsze niż 32. //Zakoduj wartości większe niż 127. //Zakoduj znak & jako &.
Filtry walidujące to FILTER_VALIDATE_typ, gdzie typ to jedna z wartości {BOOLEAN, EMAIL, FLOAT, INT, IP, REGEXP, URL}.
Możemy sprawić, że filtry walidujące będą bardziej restrykcyjne, przez przekazanie flagi FILTER_FLAGS jako trzeciego parametru. Lista wszystkich dostępnych flag walidujących wraz z flagami opcjonalnymi jest dostępna pod adresem http://www.php.net/manual/en/filter.filters.validate.php. Flagi filtrujące są opisane w dokumentacji znajdującej się pod adresem http://www.php.net/manual/en/filter.filters.flags.php. Wraz z flagą FILTER_VALIDATE_IP możemy wykorzystać cztery flagi opcjonalne: FILTER_FLAG_IPV4 FILTER_FLAG_IPV6 FILTER_FLAG_NO_PRIV_RANGE
FILTER_FLAG_NO_RES_RANGE
//Akceptuje tylko IPv4, np. 192.0.2.128. //Akceptuje tylko IPv6, np. ::ffff:192.0.2.128. //2001:0db8:85a3:0000:0000:8a2e:0370:7334. //Przedziały prywatne nie zostaną zwalidowane. //IPv4: 10.0.0.0/8, 172.16.0.0/12 i 192.168.0.0/16 oraz //IPv6 rozpoczynające się od FD lub FC. //Przedziały zarezerwowane nie zostaną zwalidowane. //IPv4: 0.0.0.0/8, 169.254.0.0/16, //192.0.2.0/24 i 224.0.0.0/4. //IPv6: nie dotyczy.
Listing 11.17. Wykorzystanie flag filtrujących z FILTER_VALIDATE_IP
231
Filtry czyszczące to filtry o nazwie FILTER_SANITIZE_typ, gdzie typ oznacza jedną z wartości {EMAIL, ENCODED, MAGIC_QUOTES, FLOAT, INT, SPECIAL_CHARS, STRING, STRIPPED, URL, UNSAFE_RAW}. Filtr FILTER_SANITIZE_STRING usuwa tagi HTML, a FILTER_SANITIZE_STRIPPED jest jego aliasem. Jest także flaga FILTER_CALLBACK, która pozwala na ustawienie funkcji filtrującej. Funkcje czyszczące modyfikują oryginalną zmienną, ale jej nie walidują. Przeważnie najpierw używana jest funkcja czyszcząca, a po niej funkcja walidująca. Listing 11.18 prezentuje przykład wykorzystania filtra FILTER_SANITIZE_EMAIL, dotyczącego adresu e-mail. Listing 11.18. Przykład wykorzystania filtra FILTER_SANITIZE_EMAIL
Funkcja filter_var_array jest podobna do funkcji filter_var — może jednak filtrować wiele zmiennych naraz. Do filtrowania zmiennych superglobalnych należy zastosować jedną z funkcji: • filter_has_var($typ, $nazwa_zmiennej), gdzie typ oznacza jedną z wartości: INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER lub INPUT_ENV i odnosi się do odpowiedniej zmiennej superglobalnej. Zwraca informację o tym, czy zmienna istnieje.
232
ROZDZIAŁ 11. BEZPIECZEŃSTWO
• filter_input, która pobiera odpowiednią zmienną zewnętrzną i opcjonalnie ją filtruje. • filter_input_array, która pobiera zmienne zewnętrzne i opcjonalnie je filtruje. Na listingu 11.19 przedstawiono przykład wykorzystania funkcji filter_has_var. Listing 11.19. Przykład wykorzystania funkcji filter_has_var
UWAGA. Funkcja filter_has_var zwraca fałsz, chyba że wartość zmiennej $_GET zostanie zmieniona w adresie. Zwraca prawdę w przypadku, kiedy zmienna jest pusta.
Aby uzyskać metadane opisujące filtry, wykorzystaj jedną z funkcji: • filter_list, która zwraca listę dostępnych filtrów, • filter_id, która zwraca identyfikator filtra.
Plik php.ini i ustawienia serwera Centrum dobrze zabezpieczonego systemu to poprawnie skonfigurowany plik php.ini i bezpieczny serwer. Jeżeli napastnik uzyska dostęp do serwera, wszystkie dodatkowe zabezpieczenia, jakie wprowadzimy, będą bezwartościowe. Na przykład nie ma sensu filtrowanie danych i zabezpieczanie ich, jeżeli plik PHP będzie mógł być modyfikowany przez napastnika.
Środowisko serwerowe Im mniej wie potencjalny napastnik o naszym serwerze, tym lepiej. Dotyczy to konfiguracji fizycznej, tego, czy nasza strona wykorzystuje hosting współdzielony, jakie moduły są uruchomione, oraz ustawień w pliku php.ini. Znane poprawki bezpieczeństwa w nowych wersjach Apache, PHP i innych bibliotekach oznaczają, że napastnik będzie dokładnie wiedział, z jakich luk może skorzystać w przypadku starszych wersji. Z tego powodu nie należy ujawniać informacji wyświetlanych przez phpinfo() w środowisku produkcyjnym. Dalej pokażemy, jak wyłączyć ujawnianie tych informacji w pliku php.ini. Na serwerach Apache możemy wykorzystać plik .htaccess do ograniczenia widoczności plików. Możemy także dodać pliki indeksu do katalogów, tak aby ich zawartość nie była wyświetlana w przeglądarce. Ważne jest także, aby użytkownik nie miał uprawnień do zapisu plików, chyba że jest to absolutnie konieczne. Musimy zabezpieczać przed zapisem pliki i katalogi. Ustawienie uprawnień do katalogu na 755 i uprawnień do pliku na 644 sprawi, że użytkownicy niebędący właścicielami plików będą mogli te pliki jedynie odczytywać, a użytkownicy niebędący właścicielami katalogów będą mogli te katalogi jedynie odczytywać i przetwarzać ich zawartość. Nie możemy trwać w przekonaniu, że plik robots.txt zablokuje robotom możliwość odczytu danych wrażliwych. Co więcej, informacje w pliku mogą skierować robota prosto do tych danych. Z tego powodu wszystkie dane wrażliwe powinny być umieszczane poza katalogiem głównym aplikacji. Jeżeli strona działa w środowisku hostingu współdzielonego, musimy mieć pewność, że host stosuje najlepsze praktyki bezpieczeństwa, a wszystkie uaktualnienia są przeprowadzane na czas. W przeciwnym razie ataki
233
ROZDZIAŁ 11. BEZPIECZEŃSTWO
na inne strony mogą prowadzić do uzyskania dostępu do naszej aplikacji. W kolejnym podrozdziale omówimy wykorzystanie safe_mode. I ostatnia uwaga — należy okresowo przeglądać logi serwerowe i PHP w poszukiwaniu podejrzanych wpisów.
Zabezpieczanie pliku php.ini W pliku php.ini jest kilka ustawień, które możemy zmodyfikować w celu poprawienia bezpieczeństwa naszej aplikacji. Przede wszystkim trzeba sprawić, żeby nie były wyświetlane żadne informacje o błędach w aplikacji — mogłoby to ujawnić informacje przydatne dla potencjalnego napastnika. Potrzebujemy informacji o błędach, ale nie możemy ich wyświetlać. display_errors = Off display_startup_errors = Off log_errors = On
//Nie wyświetlaj błędów. //Zapisuj błędy.
Wysiłek ten pójdzie na marne, jeżeli napastnik będzie w stanie uzyskać dostęp do plików logów. Dlatego należy upewnić się, że plik logu znajduje się poza głównym katalogiem aplikacji: error_log = "/gdzies/poza/katalogiem/aplikacji/" track_errors = Off //Przechowuje informacje o ostatnim błędzie w $php_errormsg. Nie chcemy tego. html_errors = Off //Wstawia odnośniki do dokumentacji dotyczącej błędów. expose_php = Off; //Nie pozwala serwerowi dodawać PHP do nagłówka, //gdyż ujawniałoby to informację, że PHP jest wykorzystywany na serwerze.
Jak już wcześniej wspominaliśmy, register_globals może stanowić poważną lukę w bezpieczeństwie serwera — zwłaszcza kiedy zmienne nie są inicjalizowane. register_globals = Off //Rejestruje dane z formularzy jako zmienne globalne. //Opcja została wycofana od wersji PHP 5.3.0.
Ustawienie dotyczące cudzysłowów ma za zadanie wyeliminowanie próby automatycznego poprzedzania cudzysłowów znakiem ucieczki. Może to jednak prowadzić do braku spójności. Najlepiej w tym celu jawnie wywoływać funkcje bazy danych. magic_quotes_gpc = Off
//Wycofane w 5.3.0; zamiast tego wykorzystaj funkcje bazy danych.
Jak powiedziano, należy zrezygnować z zapisywania identyfikatora sesji w pliku cookie i adresie URL. session.use_cookies = 1 session.use_only_cookies = 1 session.use_trans_sid = 0
Możemy wyłączyć ryzykowne funkcje PHP i włączać je tylko w razie potrzeby. disable_functions = curl_exec, curl_multi_exec, exec, highlight_file, parse_ini_file, passthru, ´phpinfo, proc_open, popen, shell_exec, show_source, system
Istnieje także dyrektywa uniemożliwiająca używanie klas, z których skrypty nie powinny korzystać. disable_classes =
Możemy sprawić, że PHP będzie bezpiecznie operować na plikach zdalnych: allow_url_fopen = Off allow_url_include = Off file_uploads = Off
//Czy pozwalać na otwieranie plików zdalnych? //Czy pozwolić na załączanie skryptów z plików zewnętrznych? //Wyłącz tylko wtedy, gdy skrypt nie wykorzystuje wczytywania plików.
Dyrektywa open_basedir pozwala na użycie plików pochodzących ze wskazanego katalogu i jego podkatalogów.
234
ROZDZIAŁ 11. BEZPIECZEŃSTWO
open_basedir = /katalog/bazowy/ enable_dl = Off //Pozwala na ominięcie katalogu wskazanego w open_basedir.
W przypadku hostingów współdzielonych ustawienie safe_mode pozwala na wykonywanie skryptów PHP tylko przez użytkownika o odpowiednim identyfikatorze. Nie ogranicza to jednak innych języków skryptowych — takich jak Bash czy Perl. Przez to wzrost bezpieczeństwa wynikający z tego ustawienia jest mniejszy, niż moglibyśmy się spodziewać. safe_mode = On
Algorytmy haseł W tym podrozdziale omówimy algorytmy haszujące (mieszające). Do przechowywania haseł użytkowników chcemy wykorzystać format, który utrudni odczytanie haseł nawet w przypadku uzyskania dostępu do bazy danych. Nigdy nie powinniśmy przechowywać haseł zapisanych tekstem jawnym. Algorytmy haszujące zmieniają ciąg znaków na jego odpowiednik o ustalonej długości. Są to algorytmy jednokierunkowe — oznacza to, że na podstawie hasza nie możemy uzyskać ciągu, dla którego został on wygenerowany. Trzeba zawsze haszować hasło, aby porównać wynik działania algorytmu z wartością zapisaną w bazie. Funkcja crc32 zapisuje wynik zawsze jako 32-bitową liczbę binarną. Ponieważ jest więcej ciągów znaków niż możliwych haszy, wynik i źródło nie mają przełożenia jeden do jednego. Istnieją różne ciągi generujące ten sam hasz. Algorytm MD5 (Message Digest) konwertuje ciąg na 32-znakową liczbę szesnastkową lub na równoznaczną 128-bitową liczbę binarną. Mimo że haszowanie jest jednokierunkowe, dostępne są tzw. tabele tęczowe, pozwalające na sprawdzenie, jakie łańcuchy wygenerują dany hasz. Znane są tablice tęczowe dla haszy MD5. Jeżeli atakujący uzyska dostęp do bazy przechowującej dane zaszyfrowane w MD5, hasła użytkowników mogą być z łatwością odczytane. Jeżeli wykorzystujesz do szyfrowania algorytm MD5, musisz go wzmocnić. Możesz to zrobić, dodając ciąg znaków po wygenerowanym haszu, a następnie zaszyfrować go ponownie. Wygenerowany wynik można sprawdzić tylko wtedy, gdy się zna ciąg znaków dodany do pierwszego hasza. W PHP funkcja mt_rand jest nowsza i wykorzystuje szybszy algorytm niż funkcja rand. Aby wygenerować losową wartość między 1 a 100, należy wywołać: mt_rand(1, 100);
Funkcja uniqid wygeneruje unikalny identyfikator. Przyjmuje ona dwa opcjonalne parametry — pierwszy oznacza prefiks, drugi oznacza poziom entropii (poziom losowości). Korzystając z tych dwóch funkcji (mt_rand i uniqid), można wygenerować unikalny ciąg znaków i dodać go do hasza (listing 11.20). Listing 11.20. Wygenerowanie unikalnego ciągu znaków i powtórne haszowanie przy jego wykorzystaniu
Dopisany ciąg musimy przechowywać w bazie danych wraz z hasłem, aby w przyszłości móc sprawdzać poprawność hasła. Silniejszym algorytmem jest algorytm SHA1 (US Secure Hash Algorithm 1). PHP udostępnia funkcję sha1(): $silniejsze_haslo = sha1( $haslo.$ciag );
Dla PHP 5.1.2 i wersji późniejszych możesz wykorzystać jego następcę sha2, który oczywiście jest silniejszy niż sha1. Aby użyć algorytmu sha2, musimy zastosować ogólną funkcję hash, która jako pierwszy argument przyjmuje nazwę algorytmu, a ciąg znaków do przetworzenia jako drugi (listing 11.21). Aktualnie funkcja udostępnia ponad 30 algorytmów haszujących. Funkcja hash_algos zwróci nazwy wszystkich algorytmów dostępnych w Twojej wersji PHP.
235
ROZDZIAŁ 11. BEZPIECZEŃSTWO
Listing 11.21. Wykorzystanie funkcji hash i algorytmu sha2
Możemy także użyć funkcji crypt wraz z kilkoma algorytmami, takimi jak md5, sha256 i sha512. Funkcja ta wymaga jednak dodatkowych ciągów zabezpieczających i prefiksów, co powoduje, że zapamiętanie jej składni jest trudne. Podczas budowania systemu logowania dla strony warto wykorzystać istniejące rozwiązania, takie jak OpenId lub OAuth, gwarantujące odpowiedni poziom bezpieczeństwa. Jeżeli nie ma potrzeby tworzenia rozwiązań unikalnych, można sięgnąć po rozwiązania wypróbowane.
Podsumowanie W tym rozdziale omówiliśmy wagę bezpieczeństwa w skryptach PHP. Podkreślaliśmy, że nie należy ufać danym w naszym programie i że trzeba zawsze zabezpieczać dane wyświetlane na stronie. Poruszyliśmy tematykę filtrowania danych oraz zabezpieczania ich przed atakami typu XSS, CSRF i z podmianą sesji. Wspomnieliśmy o atakach SQL injection i wyjaśniliśmy, jak zabezpieczyć system plików naszej aplikacji. Na końcu pokazaliśmy, jak zabezpieczyć plik php.ini i zapewnić bezpieczeństwo dla haseł. Najważniejszą zasadą bezpieczeństwa jest brak zaufania do danych i użytkowników. Podczas tworzenia aplikacji musimy zakładać, że dane mogą zostać przechwycone i że luki w systemie mogą być wyszukiwane przez intruzów — musimy zabezpieczyć się przed taką ewentualnością.
236
ROZDZIAŁ 12
Programowanie zwinne z wykorzystaniem Zend Studio dla Eclipse, Bugzilli, Mylyn i Subversion W ostatnich latach popularność programowania zwinnego znacznie wzrosła. Jest to podejście, w którym przyjmuje się, że dwuosobowe zespoły programistyczne są wydajniejsze niż programiści pracujący samodzielnie. Koncepcja ta ma także swoich przeciwników, uznających, że dwóch programistów pracujących jednocześnie na tej samej maszynie to strata czasu. Przyjrzymy się najpierw podstawowym zasadom programowania zwinnego. Następnie omówimy wykorzystanie programów i narzędzi wspominanych zawartych w tytule tego rozdziału oraz jak mogą zostać wykorzystane do wcielenia w życie zasad programowania zwinnego.
Zasady programowania zwinnego Istnieje wiele zasad programowania zwinnego. Wprowadzenie ich do pracy zespołu programistów może zająć sporo czasu. Wcześniej należy to zaplanować i przygotować odpowiedni harmonogram. Trzeba się liczyć z tym, że nie wszyscy, od programistów po zarząd, będą wykazywać entuzjazm. Idee towarzyszące programowaniu zwinnemu wymagają dogłębnego zrozumienia. Kiedy już je sobie przyswoisz, możesz wymyślić własną terminologię. Terminologia programowania zwinnego nie jest ściśle określona. Programowanie to i zadania programistów w jego obrębie można porównać do rajdu samochodowego. W każdym samochodzie znajdują się kierowca i pilot. Rajd z reguły trwa określony czas — może to być jeden dzień (małe zadanie programistyczne) lub więcej dni, jeżeli jest to rajd wieloetapowy (duże zadanie). W jego trakcie mogą występować przerwy techniczne lub przerwy na odpoczynek. Na końcu ogłaszane są wyniki. Podobnie jest w programowaniu zwinnym. UWAGA. To porównanie jest naszym indywidualnym pomysłem. W Twoim przypadku mogą okazać się skuteczne inne analogie. Na przykład codzienne spotkanie na początku dnia pracy, podsumowujące bieżące zadania, nie byłoby dobre w naszej sytuacji. Może się jednak okazać świetne dla Twojego zespołu. Należy próbować różnych sposobów.
Trzymając się analogii rajdu samochodowego, można powiedzieć, że uczestnicy otrzymują opis lub mapę trasy przejazdu. Organizatorzy przygotowują trasę i przekazują szczegóły pilotom. Każdy pilot planuje, w jaki sposób pokona drogę ze swoim kierowcą. Na niektórych rajdach przejazdy testowe są niedozwolone, na innych organizatorzy zapewniają informacje dotyczące trasy; innymi słowy, informacje, jakimi dysponuje pilot, są zależne od rajdu. Czasami na krótko przed startem informacje te mogą ulec zmianie. Planowanie w przypadku programowania zwinnego wygląda identycznie. Problem należy rozpoznać w maksymalnym możliwym stopniu i to jest zadanie „pilota”. Wykonanie planu należy do „kierowcy” (programisty
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
prowadzącego). Musi on uważnie słuchać „pilota” i wykorzystać swój sprzęt jak najlepiej, aby dotrzeć do celu w jak najkrótszym czasie. To podstawowe role w programowaniu zwinnym. Zobaczmy teraz, jakie zadania mieszczą się w obrębie tych ról. Mamy nadzieję, że poniższy opis pomoże Ci zrozumieć, jak wygląda „rajd” od jego rozpoczęcia do zakończenia.
Rajd programowania zwinnego Organizator rajdu (menedżer projektu) ustala, dokąd i kiedy powinni dotrzeć uczestnicy wyścigu. Z naszego doświadczenia wynika, że czas rajdu nie powinien przekraczać dwóch tygodni (później wyjaśnimy dlaczego). Naturalnie czas ten jest uzależniony od pracy, jaka ma być wykonana. Trasa rajdu — zadania do wykonania — zostanie przedstawiona przez organizatora pilotom, którzy z kolei rozpoznają występujące na niej trudne fragmenty (problemy) i zaplanują, jak się z nimi uporać (jak rozwiązać problemy). Następnie pilot wybiera kierowcę (programistę) odpowiedniego do zadania. Najlepiej jest, jeżeli piloci sami mogą wybierać kierowców, ponieważ to piloci najlepiej wiedzą, co należy zrobić, i najlepiej poradzą sobie z doborem potrzebnej osoby. Kiedy rozpocznie się rajd, pilot siedzi obok kierowcy i nawiguje. Ten etap może wydawać się mało wydajny. Jeżeli jednak się nad tym zastanowisz, to przyznasz, że kiedy kierowca ma kogoś, kto mu powie, na co patrzeć i czego się wystrzegać, jego praca będzie lepsza. Gdy nic nie rozprasza uwagi kierowcy (sprawdzanie poczty, przeglądanie Facebooka czy serwisu YouTube), to poziom jego skupienia i wydajność rosną wykładniczo. Szczegółowość instrukcji, jakie pilot będzie wydawał kierowcy, jest uzależniona od tego, jakie występują różnice w doświadczeniu i umiejętnościach obu osób — jej poziom jest dynamiczny i sam musi się ustabilizować. Programista prowadzący (pilot) nie powinien jednak wskazywać koledze (kierowcy) pojedynczych klawiszy, które mają zostać naciśnięte, tak jak prawdziwy pilot nie mówi kierowcy, kiedy zmienić bieg. UWAGA. Pilot ma do odegrania większą rolę i powinien ją traktować poważnie. Musi zaplanować trasę, wyznaczyć kamienie milowe oraz punkty kontrolne, tak aby zespół nie zabłądził gdzieś po drodze. Kiedy rozpoczynaliśmy wdrażanie programowania zwinnego w naszym miejscu pracy, znaleźliśmy kilka dobrych filmów przedstawiających współpracę pilota z kierowcą podczas prawdziwych rajdów i pokazywaliśmy je pracownikom. Wyjaśnialiśmy im praktyczne aspekty omawianej koncepcji. Pokaż swojemu zespołowi kilka nagrań z dobrych i złych rajdów (wypadki), aby zademonstrować, że podczas programowania zwinnego także mogą pojawić się przeszkody.
Według niektórych szkół czas, jaki „kierowca” spędza przy klawiaturze, nie powinien przekraczać kilku godzin. Po jego upływie pracownicy powinni zamienić się rolami i (lub) zmienić zadania. To jest zależne od zespołu. Dobrym pomysłem jest robienie przerw. Kiedy organizator ustali już daty rozpoczęcia i zakończenia rajdu, należy zaplanować przerwy techniczne. Jeżeli rajd jest długi, przerwy te powinny być zaplanowane na strategiczne momenty. Zespół powinien się wtedy zbierać i podsumowywać, co zostało wykonane, co się nie udało oraz co wymaga dalszej pracy. Spotkania takie bywają nazywane scrumami. UWAGA. Gdy się stosuje programowanie zwinne, środowisko pracy powinno być dobrze przemyślane. Proponujemy przynajmniej trzy różne środowiska: lokalne środowisko pracy dla zespołów, środowisko testowe zapewniające dobrą jakość wykonanych prac oraz oczywiście środowisko produkcyjne.
Programowanie zwinne wymaga pewnej praktyki i wykorzystania odpowiednich narzędzi. Zapewne nie pojechałbyś długą limuzyną na trasę rajdową — chociaż mogłoby się to spodobać kibicom… Pamiętaj, że najpierw przeczytałeś to tutaj! Pozostała część tego rozdziału dotyczy zestawu narzędzi, które w naszej ocenie są bardzo użyteczne podczas programowania zwinnego — od etapu planowania do implementacji. Omówimy każde z tych narzędzi osobno, przedstawimy ich zalety i możliwości, a następnie wyjaśnimy, w jaki sposób można je zintegrować.
238
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
UWAGA. Z myślą o osobach chcących wprowadzić programowanie zwinne w swoim zespole i potrzebujących więcej informacji zamieściliśmy na końcu tego rozdziału kilka odnośników do wartościowych materiałów.
Wprowadzenie do programu Bugzilla Bugzilla jest webowym narzędziem open source, które pozwala użytkownikowi śledzić błędy i problemy w wielu projektach jednocześnie. Szczerze mówiąc, jego uruchomienie w środowiskach *nix może sprawiać problemy. Najlepiej poprosić doświadczonego administratora, aby mieć pewność, że wszystko zostało ustawione poprawnie. My wykorzystujemy Bugzillę we wszystkich naszych projektach, łączenie z raportowaniem stanu prac oraz udoskonalaniem produktów. Wszystko, co ma związek z projektem, może i powinno być zapisane w tym programie (nie należy ograniczać się wyłącznie do błędów). Na rysunku 12.1 pokazany jest ekran powitalny programu — darmowej wersji demo dostępnej online.
Rysunek 12.1. Strona powitalna programu Bugzilla Kiedy już zainstalujesz program, będziesz musiał utworzyć przynajmniej jeden projekt, dla którego będziesz śledził błędy i zadania. Projekty tworzy się łatwo, jednak ze względu na późniejsze użytkowanie powinny być dobrze przemyślane. Wygodne może się okazać na przykład rozbicie projektów na mniejsze fragmenty i traktowanie ich jako odrębne produkty. Taki poziom szczegółowości pomoże Ci w dłuższej perspektywie — dzięki niemu będziesz w stanie dokładnie śledzić zadania i błędy rejestrowane w programie. Po utworzeniu odpowiednich projektów będziesz mógł zacząć wprowadzać informacje o zadaniach i błędach, które chcesz śledzić za pomocą Bugzilli. Na rysunku 12.2 przedstawiono część listy zadań, które są śledzone w tej instancji programu; na rysunku 12.3 pokazano, jakie informacje mogą być wprowadzane w odniesieniu do każdego zadania. Bugzilla sama w sobie jest doskonałym narzędziem do zarządzania projektem i śledzenia zadań (niektóre z nich mogą być błędami). W programie tym można indywidualizować wyszukiwanie, modyfikować kategorie zadań i ich priorytety, są w nim nawet wbudowane mechanizmy raportowania pozwalające na śledzenie czasu wykonania zadań. Choćby z powodu tylko tych zalet powinieneś włączyć Bugzillę do swojego zestawu narzędzi pomagających w zarządzaniu projektami.
239
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Rysunek 12.2. Lista zadań i błędów śledzonych w programie Bugzilla
Rysunek 12.3. Szczegółowe informacje dotyczące błędu (zadania)
Mylyn dla Eclipse Mylyn jest narzędziem do zapisywania zadań zbudowanym dla Eclipse. Dostępny jest jako odrębny plugin dla interfejsu Eclipse, dzięki czemu może być wykorzystany z dowolnym językiem programowania obsługiwanym przez Eclipse (Java, C++, PHP itd.). Jedną z jego najlepszych funkcji jest zapisywanie kontekstu zadania, które jest aktualnie wykonywane — w dalszej części powiemy na ten temat nieco więcej. W świecie Zend Studio dla Eclipse Mylyn wbudowany jest bezpośrednio w widok listy zadań i może być wykorzystywany od razu. To świetne rozwiązanie, jeżeli jesteś jedynym programistą w projekcie, ponieważ nie musisz dzielić z nikim kodu. Rysunek 12.4 pokazuje widok listy zadań wraz z sekcjami Uncategorized oraz Test pochodzącą z testowego serwera Bugzilli. Sekcja Uncategorized wykorzystywana jest do samodzielnej pracy. Rysunek 12.5 pokazuje szczegóły jednego zadania. Możemy tutaj zobaczyć aktualny status zadania, harmonogramowanie dotyczące zadania, a także komentarze i notatki. To wszystko jest świetne samo w sobie, zwłaszcza dla samodzielnego programisty. Jednak połączenie Mylyn i Bugzilli daje o wiele większe możliwości. Połączenie z serwerem Bugzilla jest bardzo proste. Wszystko, co należy zrobić, to założenie nowego wpisu w widoku repozytoriów zadań (Task Repositories). Zakładanie nowego repozytorium pokazane jest na rysunku 12.6. Gdy nawiążesz połączenie, podając poprawne dane, musisz założyć zapytanie. To zapytanie może działać jak filtr dla wszystkich produktów i zadań, jakie funkcjonują w Bugzilli — abyś podczas pracy nad konkretnym zadaniem mógł się skupić tylko na nim, bez konieczności przeglądania zadań z nim niezwiązanych.
240
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Rysunek 12.4. Przykładowy widok zadań z serwera w programie Zend Studio
Rysunek 12.5. Szczegóły zadania dostępne z poziomu Zend Studio
Rysunek 12.6. Przykładowe ustawienia repozytorium zadań
241
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Bugzilla i Mylyn w połączeniu z Eclipse Programiści współpracują najczęściej w ramach zespołu. W tym przypadku Mylyn i Bugzilla mogą połączyć siły i stworzyć wspaniałe możliwości. Przygotowane zadania możesz przypisać do członków zespołu za pomocą Bugzilli lub za pośrednictwem interfejsu Zend Studio. Jest to zadanie organizatora rajdu. Można wykonać znacznie więcej operacji na zadaniach Bugzilli — można dodawać załączniki (dokumenty, obrazy itp.), zmieniać statusy zadań, modyfikować ich atrybuty (jakiego systemu operacyjnego dotyczy zadanie, priorytet itd.), można także zmieniać osobę, do której przypisane jest zadanie. Wszystkie te rzeczy można wykonać za pośrednictwem interfejsu Zend Studio, nie musisz więc ciągle przeskakiwać między oknami, aby wykonać jakąś czynność. Po nawiązaniu połączenia z serwerem Bugzilla utwórz przynajmniej jedno zapytanie do niego, tak abyś mógł się skupić na zadaniach wymagających Twojej uwagi. Możesz utworzyć wiele zapytań — po jednym dla każdego projektu, jeżeli tak jest dla Ciebie wygodnie, i przełączać się między nimi w miarę potrzeby. Jak widać na rysunku 12.7, strona budowania zapytania jest dość złożona i pozwala na dokładne filtrowanie. W tym przykładzie modyfikuje zapytanie tak, aby pokazywało jedynie zadania z projektu Produkt testowy. Jeżeli wynik jest bardzo duży, można jeszcze przeprowadzić wyszukiwanie według słów kluczowych za pomocą filtra w widoku zadań.
Rysunek 12.7. Okno tworzenia zapytania do repozytorium zadań
242
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Skoro informacje o błędach i zadaniach są już wyświetlane w Zend Studio dla Eclipse, czas zacząć śledzić postępy w ich rozwiązywaniu. Na rysunku 12.4 możesz zobaczyć kolumnę złożoną z bladych kółek. Powiedziano wcześniej, że śledzenie kontekstu zadania lub błędu jest jedną z najważniejszych cech Mylyn. Teraz przyjrzymy się szczegółom tej funkcjonalności. Gdy wybierzesz zadanie, nad którym chcesz pracować, kliknij kółko, zanim zaczniesz wykonywać jakiekolwiek związane z nim czynności. Zadanie zostanie aktywowane i Mylyn zacznie zapisywać kontekst trwających prac. Program przechowuje informacje o wszystkich plikach otwartych podczas aktywności zadania i kiedy później wrócisz do tego zadania, wszystkie pliki, które miałeś otwarte w trakcie pracy nad nim, zostaną powtórnie otwarte. Rysunek 12.8 pokazuje kontekst błędu oraz informacje o tym, jakie pliki są z nim związane — w tym przypadku są to plik DemoDebugowania.php i zawierający go projekt. Mylyn śledzi nie tylko pliki otwarte w kontekście zadania, ale także te, z którymi pracowałeś i które zostały zamknięte.
Rysunek 12.8. Szczegółowe śledzenie kontekstu zadania (błędu) Kolejną zaletą strony kontekstu jest to, że jeżeli plik jest zamknięty (ale nadal jest częścią kontekstu zadania), wystarczy dwukrotnie kliknąć nazwę pliku i zostanie on otwarty do edycji. To pozwala zaoszczędzić czas potrzebny na szukanie plików w obrębie dużych projektów. Czynności te można wykonywać, kiedy zadanie jest aktywne, więc jedyną trudnością jest pamiętanie o włączaniu i wyłączaniu kontekstu zadania, nad którym aktualnie pracujemy. Kontekst zadania jest przenoszony na listę plików projektu. Kiedy zadanie jest aktywne, dzięki filtrowaniu wyświetlane są na liście tylko te pliki, które są wykorzystywane w kontekście danego zadania lub błędu. Lista dużego projektu jest zatem znacznie zmniejszana, co pozwala na szybkie wyszukiwanie odpowiednich plików. Filtrowanie plików można włączać i wyłączać za pomocą ikony przedstawiającej trzy kule armatnie i dymek z tekstem focus on active task. Kolejną świetną cechą Bugzilli jest możliwość zarządzania w niej załącznikami przypisanymi do zadania lub błędu z poziomu Zend Studio. Jest to bardzo pomocne, gdy się zarządza dokumentacją towarzyszącą, taką jak na przykład pełna dokumentacja błędu, specyfikacja, plan testów lub zrzut ekranowy wystąpienia błędu. Rysunek 12.9 pokazuje obraz JPG przypisany do zadania.
Rysunek 12.9. Błąd wyświetlony w Zend Studio wraz z załączonym plikiem A jeżeli niektórzy z członków Twojego zespołu nie używają programu Zend Studio (o ile to w ogóle możliwe)? To definitywnie zmniejszyłoby wszystkie zyski płynące z wykorzystania Mylyn z poziomu interfejsu. Na szczęście większość danych będzie przekazywana do serwera Bugzilla, kiedy połączenie zostanie nawiązane za pośrednictwem połączenia repozytoriów. Jeżeli natomiast jakieś zmiany zostaną wprowadzone bezpośrednio na serwerze, będą pobrane do połączonych z nim IDE. Istnieje ciągłe dwukierunkowe połączenie, które pozwala na utrzymanie synchronizacji. Częstotliwość synchronizacji może być sterowana z poziomu okna ustawień listy zadań, ponadto synchronizacja może być wyzwolona przez kliknięcie myszą ikony z niebieskim cylindrem z dwoma strzałkami. 243
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Aby ustawić czas automatycznej synchronizacji, wystarczy otworzyć ustawienia przez kliknięcie ikony menu na pasku narzędzi widoku (rysunek 12.10). Dostępnych jest mnóstwo dodatkowych opcji pozwalających na kontrolowanie listy zadań. Nie omówimy tutaj ich wszystkich; zauważ jednak, że możesz śledzić ilość czasu poświęconego na realizację każdego z zadań i zliczać czas ich aktywności.
Rysunek 12.10. Okno ustawień aktualizacji repozytorium zadań Zadania na liście możemy dodatkowo przeglądać podzielone na kategorie (domyślnie — projekty) lub posegregowane zgodnie z terminem planowanego wykonania. Dzięki temu widać, które zadania wymagają uwagi, a które mogą trochę poczekać. Na pasku narzędzi dostępny jest przełącznik zmieniający widok z kategorii na termin wykonania (druga ikona od prawej strony); jest tam także kilka opcji pozwalających na kontrolowanie listy zadań. Trzecia ikona od prawej, z linią przechodzącą przez ikonę zaznaczonego pola wyboru, jest kolejnym przełącznikiem — pozwala na filtrowanie zadań zakończonych, dzięki czemu z widoku znikają niepotrzebne elementy. Kolejna ikona umożliwia zwijanie i rozwijanie elementów na liście zadań, następna (z trzema kulami armatnimi) pozwala na wyświetlenie jedynie zadań przewidzianych na bieżący tydzień, dzięki czemu możemy jeszcze bardziej zmniejszyć liczbę zadań. Ostatnia jest ikona menu widoku (wspomniana już wcześniej); na pewno zauważyłeś, że w menu znajdują się jeszcze inne opcje. Szczególnie warta uwagi jest opcja Show UI Legend. Po jej kliknięciu pokaże się lista elementów (rysunek 12.11). Jest to, krótko mówiąc, legenda wszystkich ikon, które zobaczysz na liście zadań, wraz z krótkim wyjaśnieniem, co oznacza każda z nich.
244
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Rysunek 12.11. Legenda ikon Mylyn zawartych w Zend Studio dla Eclipse
Maksymalizowanie korzyści Dowiedziałeś się już, jakie są korzyści wynikające z integracji Bugzilli i Mylyn. Teraz omówimy zalety innych obszarów Zend Studio. Można przeprowadzać kolejne integracje, które mogą okazać się przydatne dla Ciebie i Twojego zespołu. Pierwsza z nich widoczna jest w obszarze kontekstu, w oknie zadania. Jeżeli posiadasz integrację z repozytorium kodu (a kto w obecnych czasach jej nie posiada?), taką jak Subversion (SVN) lub CVS, możesz zarządzać całym repozytorium z poziomu widoku filtrowania (kontekstu) danego zadania. Na rynku dostępne są inne, podobne narzędzia do zarządzania repozytoriami, takie jak Git czy Mercurial, ale ze względu na możliwości integracji z Zend Studio najbardziej interesujący jest SVN. Jak widać na rysunku 12.12, otwarte okno kontekstu pokazuje plik, który został zmodyfikowany i zapisany lokalnie (plik oznaczony znakiem <). Dzięki temu o wiele wyraźniej widać, jakie pliki związane z zadaniem muszą być zapisane w repozytorium. Pamiętaj jednak, że śledzenie plików możliwe jest tylko wtedy, gdy zadanie jest aktywne. Kolejne przydatne usprawnienie można zobaczyć, przeglądając zmiany w kodzie podczas audytu. Znowu z poziomu kontekstu zadania mamy dostęp do listy — kliknięcie prawym przyciskiem myszy na projekcie (lub na pojedynczym pliku) pozwoli Ci na porównanie wyniku Twojej pracy z poprzednią wersją pliku. Możesz
245
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Rysunek 12.12. Okno kontekstowe zadania w Zend Studio także sprawdzać poprzednie wersje pliku, przeglądając jego historię. Rysunek 12.13 pokazuje możliwość porównania historii zatwierdzanych plików w obrębie całego projektu. Takie porównanie możliwe jest także dla historii lokalnych plików. Wybór opcji, z której należy skorzystać (historii lokalnej lub repozytorium), należy do Ciebie, ale może jednak zależeć od częstotliwości zatwierdzania zmian w repozytorium.
Rysunek 12.13. Porównanie historii pliku lokalnego i pliku z repozytorium SVN Na rysunku 12.13 widać listę numerów rewizji SVN, po których wyświetlone są szczegółowe informacje o tym, kiedy dane zmiany zostały zatwierdzone. Sekcja poniżej pokazuje listę plików zmienionych w danej rewizji — czyli do czasu ostatniego zatwierdzenia (nie będą uwzględnione pliki, które zmodyfikowano, ale zmiany nie zostały jeszcze zatwierdzone). Kolejna sekcja zawiera pliki zmienione przez nas podczas pracy, które nie zostały jeszcze zatwierdzone, a następna — zmiany w konkretnych liniach kodu oraz ich porównanie z wersją pliku znajdującą się na serwerze.
Podsumowanie Poznałeś świetne narzędzia i metody zarządzania nimi z poziomu Zend Studio. Zaznajomiłeś się z informacjami wystarczającymi do tego, aby tych narzędzi z powodzeniem używać i mieć z tego jak największe korzyści dla swojego zespołu. Nie wiemy, jakie byłyby dzisiaj nasze zdolności zarządzania zadaniami i projektami, gdybyśmy nie wypróbowali tego podejścia do programowania. Oczywiście, zbudowanie omówionego zestawu narzędzi zajęło nam kilka lat. Najpierw korzystaliśmy z Zend Studio w połączeniu z SVN, następnie odkryliśmy Bugzillę i w końcu możliwości integracji między tymi narzędziami. Ty natomiast zaoszczędziłeś czas potrzebny na odkrywanie własnej drogi i możesz od razu zacząć pracę z nimi. Jeżeli właśnie poznajesz zalety programowania zwinnego w połączeniu z programowaniem ekstremalnym i częstymi publikacjami kodu itp., to powinieneś sprawdzić, jak możesz wykorzystać przedstawiony tutaj zestaw narzędzi.
246
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
Jak na pewno zauważyłeś, nie omówiliśmy wszystkich możliwości i opcji tych programów. Możesz wiele się nauczyć, odkrywając nowe opcje samodzielnie i w ten sposób zdobywając doświadczenie. Własne spostrzeżenia i pomysły przynoszą najwięcej satysfakcji. Na końcu zamieszczamy kilka przydatnych odnośników do źródeł, z których możesz czerpać dalszą wiedzę dotyczącą omówionych narzędzi. • Strona Zend Studio dla Eclipse: http://www.zend.com/en/products/studio/ • Strona Bugzilli: http://www.bugzilla.org/ • Strona Mylyn dla Eclipse: http://www.eclipse.org/mylyn/ • Strona Mylyn: http://tasktop.com/mylyn/ • Dr Mik Kersten, „Redefining the »I« of IDE” (wideo): http://tasktop.com/resources/videos/w-jax/kersten-keynote.html
247
ROZDZIAŁ 12. PROGRAMOWANIE ZWINNE Z WYKORZYSTANIEM ZEND STUDIO DLA ECLIPSE, BUGZILLI, MYLYN I SUBVERSION
248
ROZDZIAŁ 13
Refaktoryzacja, testy jednostkowe i ciągła integracja Naszym — programistów — celem jest osiągnięcie stabilnego kodu o wysokiej jakości. Gdy mamy taki kod, zmniejsza się ilość czasu przeznaczanego na poszukiwanie błędów i łatwiejsze są częste publikacje. W tym rozdziale omówimy trzy techniki poprawiające jakość kodu i czyniące go łatwiejszym w modyfikacji. Te techniki to refaktoryzacja, testy jednostkowe oraz ciągła integracja. Refaktoryzacja jest sposobem modyfikowania struktury kodu w celu poprawienia jego jakości. Podczas refaktoryzacji nie próbujemy modyfikować ani dodawać funkcjonalności. Refaktoryzacja jest niezbędną i naturalną częścią programowania. Pomimo naszych najlepszych intencji, aby funkcjonalność nie została zmieniona podczas refaktoryzacji, należy sobie zdawać sprawę, że bardzo łatwo zrobić to niechcący. Błędy, które mogą w ten sposób powstać, są trudne do znalezienia i mogą być długo niezauważone. Testy jednostkowe pozwalają upewnić się, że błędy powstałe podczas refaktoryzacji będą natychmiast zauważone. Kiedy test jednostkowy zakończy się niepowodzeniem, natychmiast możemy zbadać, dlaczego tak się stało. Jeżeli spodziewamy się, że wynik testu będzie negatywny wskutek tego, że funkcjonalność została zmodyfikowana celowo, po prostu modyfikujemy test, dopóki nie zakończy się powodzeniem. Jednakże jeżeli negatywny wynik testu jest niezamierzony, musimy poprawić błąd. Ciągła integracja (ang. continuous integration — CI) dokonuje sprawdzenia jakości (ang. quality assurance — QA) w obrębie projektu. Członkowie zespołu propagują zmiany w kodzie do repozytorium projektu codziennie (lub częściej). Serwer CI wysyła do repozytorium zapytanie o zmiany w kodzie i automatycznie buduje kod, jeżeli je wykryje — operacja ta wykonywana jest okresowo lub na żądanie. Podczas budowy uruchamiane są testy jednostkowe oraz analiza statyczna. Częstotliwość i automatyzacja ciągłej integracji pozwala na szybkie wykrycie zmian, które „coś zepsuły”. Oznacza to, że napotkany został element, który nie funkcjonuje poprawnie po zatwierdzeniu w repozytorium zmian wprowadzonych przez jednego z członków zespołu. W językach niekompilowalnych, takich jak PHP, oznacza to z reguły, że jeden z testów jednostkowych zakończył się niepowodzeniem. Wykorzystanie CI zwiększa nasze zaufanie do kodu, co z kolei prowadzi do częstszych i stabilniejszych publikacji. CI pozwala nam na uruchomienie zbudowanych skryptów oraz uruchamianie sekwencji poleceń i zadań, które w przeciwnym wypadku mogłyby być nudne, czasochłonne i (lub) podatne na błędy.
Refaktoryzacja Poniżej znajduje się lista kilku przykładów refaktoryzacji: • Eliminacja nadmiarowego kodu poprzez utworzenie nowej funkcji i jej wywołanie. • Zamiana skomplikowanego wyrażenia logicznego na jego uproszczoną wersję lub przeniesienie go do funkcji z opisową nazwą, poprawiającą czytelność kodu. • Przeniesienie metod z dużej klasy do mniejszych lub odpowiedniejszych.
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
• Zmniejszenie poziomu zagnieżdżenia struktur kontrolnych (if/else, for, foreach, while, switch). • Zmiana modelu obiektowego, np. rozszerzenie klasy bazowej albo wykorzystanie wzorca projektowego takiego jak wzorzec budowniczego lub singleton. Istnieje więcej modyfikacji pasujących do kategorii refaktoryzacji. Pionierem tego obszaru programowania był Martin Fowler — sklasyfikował on kilka złych praktyk oraz podał sposoby ich eliminowania. Warte polecenia książki na ten temat to między innymi: • Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts (Helion 2011). • Pro PHP Refactoring, Francesco Trucchia, Jacopo Romei (Apress 2010). Powtórzenia w kodzie są znakiem, że powinien on być poddany refaktoryzacji. Enkapsulowanie logicznych części kodu w funkcjach jest podstawową zasadą programistyczną. Jeżeli kod będzie rozrzucony w wielu miejscach, pomijamy tę zasadę i znacznie zwiększamy prawdopodobieństwo wystąpienia błędów. Wyobraź sobie, że mamy blok kodu, który skopiowany jest w pięciu miejscach. Po jakimś czasie, kiedy chcemy zmienić funkcjonalność tego kodu, jest mało prawdopodobne, że będziemy pamiętać o zmianie kodu we wszystkich pięciu miejscach. Gdybyśmy ten blok kodu umieścili w funkcji, musielibyśmy wprowadzić modyfikacje tylko w niej. Przy dodawaniu testów potrzebne byłoby przetestowanie tylko tej funkcji. Ukrytym niebezpieczeństwem występującym podczas refaktoryzacji jest możliwość wprowadzenia nieprzewidzianych zmian w zachowaniu kodu. Takie zmiany zachowania często pojawiają się w miejscach, nad którymi nie pracujemy i które mogą być bardzo trudne do znalezienia — dlatego właśnie refaktoryzacja i testy jednostkowe są nierozłączne. Podczas refaktoryzacji całkowita objętość kodu może ulec zwiększeniu, ale to w niczym nie przeszkadza. Celem refaktoryzacji nie jest zmniejszenie ilości kodu. Wiele dodanych linii to po prostu białe znaki, które poprawiają czytelność kodu.
Niewielka refaktoryzacja Przykład skryptu potrzebującego refaktoryzacji przedstawiony jest na listingu 13.1. Listing 13.1. Skrypt sprawdzający, czy powinniśmy wybrać się na spacer
Nasza pierwsza refaktoryzacja (listing 13.2) przenosi opcje konfiguracyjne do osobnego pliku (listing 13.3). Listing 13.2. Mała refaktoryzacja polegająca na utworzeniu pliku konfiguracyjnego
250
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
if ( (POSIADASZ_PSA && (!JESTES_ZMECZONY || NIE_SPACEROWALES_OD_DAWNA)) || (LADNA_POGODA && ´!JESTES_ZMECZONY) || JESTES_ZNUDZONY ) { idzNaSpacer(); } function idzNaSpacer() { echo "Idę na spacer."; } ?>
Listing 13.3. Plik konfiguracyjny walkConfig.php
Długie logiczne wyrażenie, takie jak na listingu 13.1, może być przeniesione do funkcji w celu poprawienia czytelności kodu zgodnie z listingiem 13.4. Listing 13.4. Poprawianie czytelności — przeniesienie warunku logicznego do funkcji
Na pierwszy rzut oka może się wydawać, że po prostu zmieniliśmy miejsce umieszczenia warunku. To prawda. Jednak dzięki zmianie sposób działania głównego programu jest teraz łatwiejszy w interpretacji. Jeżeli ponadto logika zostanie powtórzona, możemy teraz wykorzystać funkcję. Możemy także poprawić czytelność nowej funkcji poprzez rozdzielenie logiki — tak jak pokazano na listingu 13.5. Listing 13.5. Rozdzielenie funkcji zawierającej logikę na dwie mniejsze
251
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
function czasWyprowadzicPsa(){ return (POSIADASZ_PSA && (!JESTES_ZMECZONY || NIE_SPACEROWALES_OD_DAWNA)); } function ochotaNaSpacer() { return ((LADNA_POGODA && !JESTES_ZMECZONY) || JESTES_ZNUDZONY); } function idzNaSpacer() { echo "Ide na spacer"; } ?>
Skrypty z listingów 13.1 i 13.5 są równoznaczne funkcjonalnie, ale skrypt z listingu 13.5 jest o wiele czytelniejszy, daje możliwość ponownego wykorzystania funkcji i jest łatwiejszy w testowaniu. Refaktoryzacja kolejnego przykładu, przedstawionego na listingu 13.6, będzie bardziej złożona — wprowadza parametry w funkcjach rozszerzających. Skrypt po refaktoryzacji pokazano na listingu 13.7. Listing 13.6. Skrypt z powtórzeniem 5) { $mnoznik = 2; $wynik = $wartosc; $wynik *= $mnoznik; $wynik += (10 - $wartosc); print "do widzenia "; print "wartość początkowa to $wartosc "; print "wynik to $wynik "; } else { $mnoznik = 7; $wynik = $wartosc; $wynik *= $mnoznik; $wynik += (10 - $wartosc); print "dzień dobry! "; print "wartość początkowa to $wartosc "; print "wynik to $wynik "; } ?>
Listing 13.7. Skrypt z listingu 13.6 po refaktoryzacji w celu usunięcia powtórzeń 5) { $wynik = zamienWartosc($wartosc, 2); wyswietlWiadomosc("do widzenia", $wartosc, $wynik); } else { $wynik = zamienWartosc($wartosc, 7); wyswietlWiadomosc("dzień dobry!", $wartosc, $wynik); } function zamienWartosc($wartosc, $mnoznik){ $wynik = $wartosc * $mnoznik; $wynik += (10 - $wartosc); return $wynik;
252
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
} function wyswietlWiadomosc($powitanie, $wartosc, $wynik){ print "$powitanie "; print "wartość początkowa to $wartosc "; print "wynik to $wynik "; } ?>
Podczas zmiany kodu możesz zadać sobie pytanie, skąd masz wiedzieć, czy zmiany nie spowodują efektów ubocznych. Odpowiedź jest prosta — bez testów nie ma pewności.
Większy przykład Poddamy refaktoryzacji duży skrypt (listing 13.8) — stanie się łatwiejszy w interpretacji. Oblicza on optymalną trasę przejazdu i ustala czas podróży po podaniu lokalizacji początkowej i końcowej. Na potrzeby przykładu zakładamy uproszczone warunki: zawsze możemy dotrzeć do celu w linii prostej, a w samochodzie nigdy nie kończy się paliwo. Listing 13.8. Skrypt do refaktoryzacji — travel_original.php x = $x; $this->y = $y; } public function toString() { return "(" . round ( $this->x, 2 ) . ", " . round ( $this->y, 2 ) . ")"; } } function podroz(Lokalizacja $start, Lokalizacja $cel) { //obliczenie wektora kierunku $odleglosc_y = $cel->y - $start->y; $odleglosc_x = $cel->x - $start->x; $kat = null;
253
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
if ($odleglosc_x) { if ($odleglosc_y) { $kat = atan($odleglosc_y / $odleglosc_x); } else { if ($odleglosc_x > 0) { $kat = 0.0; //w prawo } else { $kat = 180.0; //w lewo } } } else { if ($odleglosc_y) { if ($odleglosc_y < 0) { $kat = - 90.0; //w dół } else { $kat = 90.0; //w górę } } } $kat_w_radianach = deg2rad ( $kat ); $odleglosc = 0.0; //obliczenie odległości w linii prostej if ($cel->y == $start->y) { $odleglosc = $cel->x - $start->x; } else if ($cel->x == $start->x) { $odleglosc = $cel->y - $start->y; } else { $odleglosc = sqrt ( ($odleglosc_x * $odleglosc_x) + ($odleglosc_y * $odleglosc_y) ); } print "Podróż z " . $start->toString () . " do " . $cel->toString () . " \n"; if (SPIESZYSZ_SIE) { print "Spieszysz się! \n"; } print "Odległość to " . $odleglosc . " pod kątem " . $kat . " "; $czas = 0.0; $ma_opcje = false; if (MASZ_SAMOCHOD || (MASZ_PIENIADZE && NA_TRASIE_AUTOBUSU) || MASZ_ROWER) { $ma_opcje = true; } if ($ma_opcje) { if (ZLA_POGODA) { if (MASZ_SAMOCHOD) { //jazda samochodem while ( abs ( $start->x - $cel->x ) > KROK_SAMOCHOD || ´abs ( $start->y - $cel->y ) > KROK_SAMOCHOD ) { $start->x += (KROK_SAMOCHOD * cos ( $kat_w_radianach )); $start->y += (KROK_SAMOCHOD * sin ( $kat_w_radianach )); ++ $czas; print "jazda samochodem... aktualna pozycja to (" . round ( $start->x, 2 ) . ", " . round ( $start->y, 2 ) . ") \n"; } print "Dotarłeś do celu samochodem. ";
254
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
W stanie początkowym skrypt (listing 13.8) jest jedną, bardzo dużą funkcją wykonującą wiele zadań. Nie zostały w nim uwzględnione żadne testy. Gdybyśmy mieli program polegający na bezawaryjnym działaniu tego skryptu, nie mielibyśmy pewności, że możemy mu ufać. Co więcej, gdyby zaszła potrzeba dodania nowych funkcjonalności, musielibyśmy postępować ostrożnie, aby nic nie zepsuć. Ten kod nie jest typowym kodem zastanym — może być jednak jeszcze gorzej — w przykładzie nie ma zmiennych globalnych, z którymi musielibyśmy walczyć. Funkcja podroz ma zbyt wiele poziomów zagnieżdżenia. Dodanie testów w tej formie byłoby bardzo trudne. Funkcja ta ma także za dużo zadań. Oblicza odległość między punktami, determinuje tryb podróży oraz wyświetla komunikaty. Jest przepełniona komentarzami, które nie byłyby konieczne, gdyby została rozbita na mniejsze części o bardziej opisowych nazwach. Łatwo zauważyć, że w skrypcie znajduje się wiele nadmiarowego kodu.
256
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Możemy wykonać kilka przykładowych wywołań funkcji, aby intuicyjnie sprawdzić, czy działa ona poprawnie. Takie sprawdzenie nie jest jednak w żadnym wypadku wystarczające. Musimy poddać kod refaktoryzacji i zaimplementować testy jednostkowe. Podczas refaktoryzacji musimy sobie zadać następujące pytania: • Co można łatwo zmodyfikować (bez zależności)? • Czy występują powtórzenia (czy powinniśmy utworzyć funkcję)? • Czy możemy uprościć kod lub poprawić jego czytelność? • Czy kod jest już na tyle prosty, że możemy dodać testy? Nie ma jednego sposobu refaktoryzacji. Różnice w zmianach i kolejności ich wprowadzenia zależą od programisty (lub nawet od czasu, w którym programista wykonuje refaktoryzację). Zyskując doświadczenie, programista coraz lepiej dostrzega, co powinno zostać zmienione. To, w którym momencie dodawane są testy podczas refaktoryzacji, też jest niezwykle istotne. Byłoby idealnie, gdybyśmy dodawali nowy test po każdej zmianie, w praktyce jednak nie zawsze jest to możliwe. UWAGA. Ponieważ celem tej książki nie jest szczegółowe omówienie technik refaktoryzacji i brakuje na to miejsca, nie przedstawimy w tym rozdziale procesu refaktoryzacji krok po kroku. Zamiast tego pokażemy wynik kilku refaktoryzacji. Aby przeanalizować proces krok po kroku, pobierz odpowiednie kody pod adresem ftp://ftp.helion.pl/przyklady/phpzap.zip.
W pierwszym etapie refaktoryzacji przeniesiemy polecenia define, znajdujące się na początku skryptu, do odrębnego pliku — config.php (listing 13.9). Dzięki temu konfiguracja będzie mogła być wykorzystana w innych skryptach. Jeżeli będziemy jej potrzebować, wystarczy załączyć plik. Listing 13.9. Plik z ustawieniami — config.php
( ( ( ( ( ( ( ( ( ( ( ( (
'KROK_SPACER', 0.25 ); //kroki o długości ćwierci metra 'KROK_ROWER', 3.00 ); //kroki o długości trzech metrów 'KROK_AUTOBUS', 30.00 ); //kroki autobusu 'OPOZNIENIE_AUTOBUSU', 300 ); //czas oczekiwania na autobus 'KROK_SAMOCHOD', 50.00 ); //kroki samochodu 'OPOZNIENIE_SAMOCHODU', 20 ); //czas rozpędzania samochodu 'MASZ_SAMOCHOD', true ); 'MASZ_PIENIADZE', true ); 'SPIESZYSZ_SIE', true ); 'NA_TRASIE_AUTOBUSU', true ); 'MASZ_ROWER', false ); 'ZLA_POGODA', false ); 'MAKSYMALNA_ODLEGLOSC_SPACERU', 2500 );
Klasę Lokalizacja przeniesiemy do odrębnego pliku— location.php (listing 13.10). Zmienimy nazwę funkcji podroz na wykonaj i umieścimy ją wewnątrz klasy Podroz (listing 13.12). Następnie blok znajdujący się na górze funkcji wykonaj, który wyświetla informacje o tym, dokąd chcemy się udać, umieścimy w klasie pomocniczej PodrozWidok (listing 13.11). Przeniesiemy blok kodu sprawdzający, jakim pojazdem odbędziemy podróż, do osobnej funkcji. Nasza nadal nieprzetestowana, ale już bardziej uporządkowana klasa główna pokazana została na listingu 13.12. Listing 13.10. Plik z klasą Lokalizacja — location.php
257
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
public function __construct($x, $y) { $this->x = $x; $this->y = $y; } public function toString() { return "(" . round ( $this->x, 2 ) . ", " . round ( $this->y, 2 ) . ")"; } } ?>
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
} else { if (ZLA_POGODA) { print "Błąd: Burza "; } else if ($odleglosc < MAKSYMALNA_ODLEGLOSC_SPACERU) { //spacer while ( abs ( $start->x - $cel->x ) > KROK_SPACER || abs ( $start->y - $cel->y ) > ´KROK_SPACER ) { $start->x += (KROK_SPACER * cos ( $kat_w_radianach )); $start->y += (KROK_SPACER * sin ( $kat_w_radianach )); ++ $czas; print "spacer... aktualna pozycja to (" . round ( $start->x, 2 ) . ", " . round ( $start->y, 2 ) . ") \n"; } print "Dotarłeś do celu spacerem.br/>"; } else { print "Błąd: Zbyt daleko na spacer "; } } print "Czas podróży: " . date ( "i:s", $czas ); } private function czyMamyOpcje(){ $ma_opcje = false; if (MASZ_SAMOCHOD || (MASZ_PIENIADZE && NA_TRASIE_AUTOBUSU) || MASZ_ROWER) { $ma_opcje = true; } return $ma_opcje; } } ?>
Wykonamy jeszcze jeden zestaw refaktoryzacji, a następnie dodamy testy. Przeniesiemy kod dotyczący jazdy samochodem, jazdy autobusem, jazdy rowerem i spaceru do osobnych funkcji. Także część obliczeń matematycznych zostanie przeniesiona do osobnej funkcji — podrozObliczenia.php (listing 13.13). Zanim zaczniemy, zauważ, że kod dotyczący jazdy autobusem nie jest w obu przypadkach taki sam. Jedna z instancji ustawia czas opóźnienia autobusu, podczas gdy druga nie. Jako programista zmagający się z kodem zastanym na pewno zadasz sobie pytanie: „Czy powinienem zawsze uwzględniać opóźnienie, czy nigdy go nie uwzględniać, czy może uwzględniać je tylko czasami? Czy to zamierzone działanie, czy może błąd?”. Najprawdopodobniej jest to błąd. Tego typu sytuacji możemy uniknąć, jeżeli zamiast przeklejania kodu z jednego miejsca w inne zastosujemy funkcje. Listing 13.13. Klasa obliczeniowa — travelMath.php y == $start->y) { $odleglosc = $cel->x - $start->x; } else if ($cel->x == $start->x) { $odleglosc = $cel->y - $start->y;
261
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
print "Dotarłeś do celu rowerem. "; } private function spacer() { //spacer while ( abs ( $this->start->x - $this->cel->x ) > KROK_SPACER || ´abs ( $this->start->y - $this->cel->y ) > KROK_SPACER ) { $this->start->x += (KROK_SPACER * cos ( $this->kat_w_radianach )); $this->start->y += (KROK_SPACER * sin ( $this->kat_w_radianach )); ++ $this->czas; print "spacer... aktualna pozycja to (" . round ( $this->start->x, 2 ) . ", " . round ( $this->start->y, 2 ) . ") \n"; } print "Dotarłeś do celu spacerem. "; } }
Po zaledwie kilku refaktoryzacjach nasz kod jest łatwiejszy do zrozumienia, odczytywania, modyfikowania i możemy dodać do niego testy. Wyeliminowaliśmy mnóstwo powtórzeń. Nadal można jednak wprowadzić wiele ulepszeń. W kolejnym podrozdziale dodamy do kodu testy. Zaczniemy od klasy PodrozObliczenia, która zawiera funkcje nieposiadające żadnych zależności.
Testy jednostkowe Aby mieć pewność, że nasz kod działa poprawnie, musimy go przetestować. Chcemy, aby nasz kod był podzielony na krótkie bloki z niewieloma zależnościami, tak abyśmy mogli wyizolować i przetestować jednostki funkcjonalne. W tym celu musimy zadbać, by zależności pomiędzy komponentami były jak najmniejsze (loose coupling), i powinniśmy stosować wzorzec wstrzykiwania zależności (dependency injection). Powinniśmy także sprawić, aby funkcje były krótkie i przyjmowały niewiele parametrów. Do jakiej długości powinny być refaktoryzowane funkcje? Tak jak klasa reprezentuje jeden obiekt, tak funkcja powinna wykonywać jedno zadanie. Jeżeli funkcja wykonuje wiele zadań, oznacza to, że powinna zostać rozbita na mniejsze funkcje. Funkcje zazwyczaj powinny mieć od 5 do 15 linii kodu. Im mniejsza jest funkcja, tym łatwiej ją zrozumieć. Także wystąpienie błędów jest wtedy mniej prawdopodobne. Dobrą książką dotyczącą optymalnej długości funkcji oraz długości klas jest Czysty kod. Podręcznik dobrego programisty autorstwa Roberta C. Martina (Helion 2010). Dwoma szeroko stosowanymi bibliotekami wykorzystywanymi do testów jednostkowych są PHPUnit i Simpletest. W tym rozdziale zastosujemy PHPUnit. PHPUnit jest portem xUnit napisanego przez Sebastiana Bergmanna. Jako że PHPUnit należy do rodziny xUnit, osoby znające jUnit lub NUnit nie będą miały z nim problemów. UWAGA. Biblioteki są dostępne pod adresami:
https://github.com/sebastianbergmann/phpunit/ http://www.simpletest.org/ Dokumentacja do PHPUnit znajduje się pod adresem http://www.phpunit.de/manual/current/en/index.html.
Aby zainstalować PHPUnit za pomocą PEAR, użyj następujących poleceń: pear pear pear pear
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Podczas pisania testów jednostkowych powinniśmy starać się tworzyć szybkie, czytelne testy izolujące funkcjonalność małych bloków kodu. Może to oznaczać konieczność wykorzystania zaawansowanych technik, takich jak wstrzykiwanie zależności czy obiekty makiety. Zarówno PHPUnit i Simpletest obsługują obiekty makiety. Obiekty makiety używane są do izolowania tej części kodu, którą chcemy przetestować. Pomagają także usprawnić testy, zwracają symulowane rezultaty, zamiast odwoływać się do wolnych (w kontekście testów jednostkowych) źródeł, takich jak baza danych, pliki lub lokacje sieciowe. W dalszej części rozdziału dodamy testy do naszej klasy Podroz. Najpierw jednak wrócimy do przykładu z listingu 13.5, sprawdzającego, czy powinniśmy wybrać się na spacer, i dodamy do niego testy. Kiedy już szkielet testów zostanie ustalony, możemy dodać testy sprawdzające, czy przy zadanych parametrach funkcja zwraca spodziewane rezultaty. Skrypt z listingu 13.15 przedstawia obiektową wersję skryptu z listingu 13.5. Listing 13.15. Klasa Spacer, walk.php klucze_opcji as $klucz) { $this->opcje[$klucz] = true; } } public function ruszSie() { if ($this->czyIscNaSpacer()) { $this->idzNaSpacer(); } } public function czyIscNaSpacer() { return ($this->czasWyprowadzicPsa() || $this->ochotaNaSpacer()); } public function czasWyprowadzicPsa() { return ($this->opcje['posiadaszPsa'] && (!$this->opcje['jestesZmeczony'] || ´$this->opcje['nieSpacerowalesOdDawna'])); } public function ochotaNaSpacer() { return (($this->opcje['ladnaPogoda'] && !$this->opcje['jestesZmeczony']) || ´$this->opcje['jestesZnudzony']); } public function __set($nazwa, $wartosc) { if (in_array($nazwa, $this->klucze_opcji)) { $this->opcje[$nazwa] = $wartosc; }
266
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
} private function idzNaSpacer() { echo "Idę na spacer."; } } //$spacer = new Spacer(); //$spacer->ruszSie(); ?>
Większość narzędzi programistycznych, takich jak Netbeans czy Eclipse, potrafi wygenerować pliki szkieletów testów, aby nam pomóc. Programy z reguły wyświetlają rezultaty testów jako kolorowe — czerwone lub zielone — paski obrazujące sukces lub porażkę. Możesz jednak uruchamiać PHPUnit i Simpletest bezpośrednio z wiersza poleceń. Pozwalają one także wygenerować raporty pokrycia kodu, pokazujące procent przetestowanego kodu oraz które linie kodu nie zostały przetestowane. Utworzymy naszą pierwszą klasę PHPUnit (listing 13.16), która na razie nie zawiera testów. Listing 13.16. Szkielet testów jednostkowych dla klasy Spacer — listing13_16.php obiekt = new Spacer; } /** * Usuwa parametry, np. zamyka połączenie sieciowe. * Ta metoda uruchamiana jest po zakończeniu testu. */ protected function tearDown() { } } ?>
Klasa z listingu 13.16 dziedziczy po klasie PHPUnit_Framework_TestCase. Funkcja setUp tworzy instancję klasy testowanej i zapisuje ją w zmiennej $obiekt. Wewnątrz funkcji tearDown zwalnialibyśmy zasoby oraz usuwalibyśmy obiekty po zakończeniu testu. Uruchomienie testu w programie Netbeans bez zdefiniowanych testów spowoduje wyświetlenie wyniku pokazanego na rysunku 13.1. Na listingu 13.17 zademonstrowaliśmy test, który zakończy się niepowodzeniem. Wynik działania testu pokazany jest na rysunku 13.2.
267
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Rysunek 13.1. Żaden test nie został wykonany w Netbeans Listing 13.17. Dodanie pierwszego testu, który zakończy się niepowodzeniem obiekt = new Spacer; } protected function tearDown() { } public function testCzasWyprowadzicPsa_domyslny() { print "testCzasWyprowadzicPsa_domyslny"; $this->assertTrue(!$this->obiekt->czasWyprowadzicPsa()); } } ?>
Rysunek 13.2. Nieudany test wyświetlony w Netbeans Nasze domyślne opcje to zmienne posiadaszPsa i nieSpacerowalesOdDawna ustawione na true. A więc rezultatem wywołania $this->obiekt->czasWyprowadzicPsa() powinna być wartość true. Na listingu 13.18 test został zmodyfikowany tak, aby kończył się powodzeniem. Dodany został także drugi test. Listing 13.18. Naprawienie pierwszego testu i dodanie drugiego
268
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
$this->obiekt = new Spacer; } protected function tearDown() { } public function testCzasWyprowadzicPsa_domyslny() { print "testCzasWyprowadzicPsa_domyslny"; $this->assertTrue($this->obiekt->czasWyprowadzicPsa()); } public function testCzasWyprowadzicPsa_nieMaszPsa_powinnoZwracacFalse() { print "testCzasWyprowadzicPsa_domyslny"; $this->obiekt->posiadaszPsa = false; $this->assertTrue(!$this->obiekt->czasWyprowadzicPsa()); } } ?>
W drugim teście opcję posiadaszPsa ustawiliśmy na wartość false. Oczywiście teraz wywołanie this->obiekt->czasWyprowadzicPsa() także zwraca wartość false. Pozytywny wynik obu testów pokazany jest na rysunku 13.3.
Rysunek 13.3. Dwa pozytywnie zweryfikowane testy Pokrycie kodu jest procentem kodu, który został przetestowany. Na rysunku 13.4 klasa Spacer po naszych testach została pokryta testami w 55,56%. Większość programów ma wbudowaną funkcjonalność lub wtyczkę pozwalającą na podświetlenie pokrycia linii kodu. Należy pamiętać, że pokrycie kodu testami jednostkowymi może wynieść 100%, a program i tak może działać niepoprawnie. Dzieje się tak dlatego, że poszczególne części będą działać poprawnie, ale program jako całość nie. Możesz sobie wyobrazić każdą jednostkę jako część samochodową, a program jako cały samochód. Pomimo że wszystkie części są nowe i działają poprawnie, mogą być niepoprawnie połączone — więc samochód nie będzie działał. Aby przetestować cały program, potrzebne są testy funkcjonalne. Testy jednostkowe mają poinformować nas o zmianach w naszym programie — zarówno o tych znanych, jak i tych, które pojawiły się jako efekt uboczny refaktoryzacji. Błędy powstałe w wyniku refaktoryzacji mogą pozostawać niezauważone przez długi okres. Poprawienie takich błędów, wykrytych po tygodniu lub miesiącu, może być bardzo trudne i czasochłonne. Lepiej odpowiedzialność przenieść na testy jednostkowe. Bez względu na to, czy zostały napisane pięć minut, czy dwa lata wcześniej, zawsze poinformują nas o zmianach. Testy jednostkowe i testy funkcjonalne są typami testów regresyjnych. Testy regresyjne uruchamiane są co jakiś czas w celu sprawdzenia, czy nowe błędy nie pojawiły się po wprowadzeniu nowych funkcjonalności, poprawek, czy po zmianach w konfiguracji. Wyniki wyświetlane przez PHPUnit będą zależne od tego, czy korzystamy z interfejsu graficznego, przeglądarki, czy wiersza poleceń — porównaj rysunki 13.3, 13.5 i 13.6.
269
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Rysunek 13.4. Pokrycie linii kodu testami w programie Netbeans
Rysunek 13.5. Przykład wyświetlania wyników w Zend Studio
Rysunek 13.6. Przykładowy wynik działania PHPUnit w wierszu poleceń Statystyki pokrycia kodem pozwalają sprawdzić, w jakim procencie kod poszczególnych plików pokryty jest testami jednostkowymi. Rysunek 13.7 pokazuje pokrycie testami dla programu wyznaczającego trasę podróży. Po zaimplementowaniu testów jednostkowych dla klasy PodrozObliczenia (listing 13.20) wykryte zostały błędy pokazane na rysunku 13.8.
270
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Rysunek 13.7. Pokrycie testami plików programu wyznaczającego trasę podróży Listing 13.20. Pełne testy jednostkowe dla klasy PodrozObliczenia, TravelMathTest.php assertEquals($oczekiwane, $otrzymane); } public function testObliczOdleglosc_bez_roznic_na_y() { $start = new Lokalizacja(5, 7); $cel = new Lokalizacja(3, 7); $oczekiwane = 2; $otrzymane = PodrozObliczenia::obliczOdleglosc($start, $cel); $this->assertEquals($oczekiwane, $otrzymane); }
271
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Rysunek 13.8. Kilka niespodziewanych błędów public function testObliczOdleglosc_bez_roznic_na_x() { $start = new Lokalizacja(3, 10); $cel = new Lokalizacja(3, 7); $oczekiwane = 3; $otrzymane = PodrozObliczenia::obliczOdleglosc($start, $cel); $this->assertEquals($oczekiwane, $otrzymane); } public function testObliczOdleglosc_roznica_na_x_y() { $start = new Lokalizacja(6, 7); $cel = new Lokalizacja(3, 11); $oczekiwane = 5; $otrzymane = PodrozObliczenia::obliczOdleglosc($start, $cel); $this->assertEquals($oczekiwane, $otrzymane, '', 0.01); } public function testObliczKatWStopniach_bez_ruszania() { $start = new Lokalizacja(3, 7); $oczekiwane = null; $otrzymane = PodrozObliczenia::obliczKatWStopniach($start, $start); $this->assertEquals($oczekiwane, $otrzymane); } public function testObliczKatWStopniach_ruch_w_gore() { $start = new Lokalizacja(3, 7); $cel = new Lokalizacja(3, 12); $oczekiwane = 90.0; $otrzymane = PodrozObliczenia::obliczKatWStopniach($start, $cel); $this->assertEquals($oczekiwane, $otrzymane); } public function testObliczKatWStopniach_ruch_w_dol() { $start = new Lokalizacja(3, 12); $cel = new Lokalizacja(3, 7);
272
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
$oczekiwane = -90.0; $otrzymane = PodrozObliczenia::obliczKatWStopniach($start, $cel); $this->assertEquals($oczekiwane, $otrzymane); } public function testObliczKatWStopniach_ruch_w_lewo() { $start = new Lokalizacja(6, 7); $cel = new Lokalizacja(3, 7); $oczekiwane = 180.0; $otrzymane = PodrozObliczenia::obliczKatWStopniach($start, $cel); $this->assertEquals($oczekiwane, $otrzymane); } public function testObliczKatWStopniach_ruch_w_prawo() { $start = new Lokalizacja(3, 7); $cel = new Lokalizacja(6, 7); $oczekiwane = 0.0; $otrzymane = PodrozObliczenia::obliczKatWStopniach($start, $cel); $this->assertEquals($oczekiwane, $otrzymane); } public function testObliczKatWStopniach_ruch_polnocny_wschod() { //losowe wartości w obu przypadkach powinny być $x2 != $x1 i $y2 != $y1 $x1 = rand(-25, 15); $y1 = rand(-25, 25); $x2 = rand(-25, 25); $y2 = rand(-25, 25); while ($x2 == $x1) { $x2 = rand(-25, 25); } while ($y2 == $y1) { $y2 = rand(-25, 25); } $start = new Lokalizacja($x1, $y1); $cel = new Lokalizacja($x2, $y2); $oczekiwane = rad2deg(atan(($y2 - $y1) / ($x2 - $x1))); $otrzymane = PodrozObliczenia::obliczKatWStopniach($start, $cel); $this->assertEquals($oczekiwane, $otrzymane, '', 0.01); } public function testCzyBliskoCelu_x_daleko_blad() { $start = new Lokalizacja(3, 9); $cel = new Lokalizacja(3.5, 7); $krok = 1.0; $oczekiwane = false; $otrzymane = PodrozObliczenia::czyBliskoCelu($start, $cel, $krok); $this->assertEquals($oczekiwane, $otrzymane); } public function testCzyBliskoCelu_y_daleko_blad() { $start = new Lokalizacja(4.5, 7.5); $cel = new Lokalizacja(3.5, 7); $krok = 1.0;
273
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Po sprawdzeniu metod, dla których testy zakończyły się porażką, okazuje się, że dwa pierwsze błędy powodowane są brakiem wartości absolutnej różnicy odległości jednowymiarowych. Możemy to naprawić, zmieniając: if ($cel->y == $start->y) { $odleglosc = $cel->x - $start->x; } else if ($cel->x == $start->x) { $odleglosc = $cel->y - $start->y;
Trzeci błąd powodowany jest tym, że funkcja atan zwraca wynik w radianach. Spodziewaliśmy się stopni. Możemy to naprawić dzięki funkcji rad2deg, zamieniając: $kat = atan($odleglosc_y / $odleglosc_x);
Po wprowadzeniu poprawek i ponownym uruchomieniu testów widzimy, że błędy zostały poprawione. Zobacz rysunek 13.9.
Rysunek 13.9. Nasz kod teraz przechodzi wszystkie testy Testy jednostkowe już pokazały swoją wartość, wykrywając błędy w zastanym programie. Kontynuujemy refaktoryzację, a Tobie pozostawiamy dodawanie dalszych testów. W ostatnim kroku do klasy PodrozWidok zostaną przeniesione kolejne polecenia wyświetlające informacje, wykorzystana będzie też metoda PodrozObliczenia::czyBliskoCelu — zobacz listing 13.21. Na listingu 13.22 pokazano kolejną refaktoryzację klasy Widok.
274
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Listing 13.21. Ostateczna postać klasy PodrozWidok toString () . " do " . $cel->toString () . " \n"; if (SPIESZYSZ_SIE) { print "Spieszysz się! \n"; } print "Odległość to " . $odleglosc . " pod kątem " . $kat . " "; } public static function wyswietlPodsumowanie($czas) { print "Czas podróży: " . date ( "i:s", $czas ); } public static function wyswietlBlad($blad){ print "Błąd: ".$blad. " "; } public static function wyswietlStatusAktualnejLokalizacji($metoda, $x, $y){ print $metoda . "… aktualna pozycja to (" . round($x, 2). " " . round($y, 2). ") \n"; } public static function wyswietlZakonczenie($wiadomosc){ print "Dotarłeś do celu - " . strtolower($wiadomosc). " "; } } ?>
Listing 13.22. Być może ostatnia refaktoryzacja klasy Podroz odleglosc = new Lokalizacja(0, 0); }
275
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
public function wykonaj(Lokalizacja $start, Lokalizacja $cel) { $this->start = $start; $this->cel = $cel; $this->obliczKatIOdleglosc(); PodrozWidok::wyswietlPlanowanaTrase( $this->kat, $this->odleglosc, $this->start, ´$this->cel); if ($this->czyMamyOpcje()) { $this->wybierzNajlepszaOpcje(); } else { $this->sprobujSpacerem(); } PodrozWidok::wyswietlPodsumowanie($this->czas); } public function obliczKatIOdleglosc() { $this->kat = PodrozObliczenia::obliczKatWStopniach($this->start, $this->cel); $this->kat_w_radianach = deg2rad($this->kat); $this->odleglosc = PodrozObliczenia::obliczOdleglosc($this->start, $this->cel); } public function sprobujSpacerem() { if (ZLA_POGODA) { PodrozWidok::wyswietlBlad("Burza"); } else if ($this->odleglosc < MAKSYMALNA_ODLEGLOSC_SPACERU) { $this->spacer (); } else { PodrozWidok::wyswietlBlad("Zbyt daleko na spacer"); } } public function wybierzNajlepszaOpcje() { if (ZLA_POGODA) { $this->wybierzNajszybszyPojazd(); } else { if ($this->odleglosc < MAKSYMALNA_ODLEGLOSC_SPACERU && !SPIESZYSZ_SIE) { $this->spacer(); } else { $this->wybierzNajszybszyPojazd(); } } } private function wybierzNajszybszyPojazd() { if (MASZ_SAMOCHOD) { $this->jazdaSamochodem(); } else if (MASZ_PIENIADZE && NA_TRASIE_AUTOBUSU) { $this->jazdaAutobusem(); } else { $this->jazdaRowerem(); } } private function czyMamyOpcje(){ $ma_opcje = false; if (MASZ_SAMOCHOD || (MASZ_PIENIADZE && NA_TRASIE_AUTOBUSU) || MASZ_ROWER) { $ma_opcje = true; }
276
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Porównaj zmodyfikowaną klasę z listingu 13.22 z kodem początkowym z listingu 13.8. Gdybyśmy mieli dodać testy dla klasy Podroz, moglibyśmy wywołać wszystkie testy naraz, łącząc je w pakiet (ang. suite) — zobacz listing 13.23. Listing 13.23. Pakiet testów — AllTests.php addTestSuite('PodrozTest'); $suite->addTestSuite('PodrozObliczeniaTest'); return $suite; } } ?>
277
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Możemy teraz zademonstrować przykład wykorzystania zmodyfikowanego kodu (listing 13.24) z dużo większą dozą zaufania do jego poprawnego funkcjonowania. Listing 13.24. Wywołanie skryptu execute(new Lokalizacja(1, 3), new Lokalizacja(4,7)); ?>
Wynik wykonania skryptu z listing 13.24 (z flagą SPIESZYSZ_SIE ustawioną na wartość false) pokazany jest poniżej. Podróż z (1, 3) do (4, 7) Odległość to 5 pod kątem 53.130102354156 Spacer… aktualna pozycja to (1.15 3.2) Spacer… aktualna pozycja to (1.3 3.4) Spacer… aktualna pozycja to (1.45 3.6) Spacer… aktualna pozycja to (1.6 3.8) Spacer… aktualna pozycja to (1.75 4) Spacer… aktualna pozycja to (1.9 4.2) Spacer… aktualna pozycja to (2.05 4.4) Spacer… aktualna pozycja to (2.2 4.6) Spacer… aktualna pozycja to (2.35 4.8) Spacer… aktualna pozycja to (2.5 5) Spacer… aktualna pozycja to (2.65 5.2) Spacer… aktualna pozycja to (2.8 5.4) Spacer… aktualna pozycja to (2.95 5.6) Spacer… aktualna pozycja to (3.1 5.8) Spacer… aktualna pozycja to (3.25 6) Spacer… aktualna pozycja to (3.4 6.2) Spacer… aktualna pozycja to (3.55 6.4) Spacer… aktualna pozycja to (3.7 6.6) Spacer… aktualna pozycja to (3.85 6.8) Dotarłeś do celu - spacer Czas podróży: 00:19
Testy jednostkowe i refaktoryzacja świetnie współdziałają. Metodyka projektowa oparta na testach (ang. Test Driven Development) idzie o krok dalej. Zgodnie z nią kod piszemy dopiero po napisaniu dla niego testów jednostkowych. Podstawowe zasady TDD to: 1. Napisać test. 2. Test kończy się niepowodzeniem z powodu braku kodu. 3. Zaimplementuj minimum funkcjonalności, która pozwoli zakończyć test powodzeniem. 4. Powtórz. TDD jest świetną metodologią, gdy posiadamy rozbudowaną siatkę testów jednostkowych dbających o bezpieczeństwo podczas pracy nad nowym kodem lub refaktoryzacją. Częściej jednak musimy pracować z kodem zastanym, ale nie jest to dobra wymówka i przy jego modyfikowaniu nie ma się czego bać. Istnieje szansa, że usuwanie zależności i refaktoryzacja mogą prowadzić do nieprzewidzianych rezultatów. Im dłużej zwlekasz z refaktoryzacją, tym większe jest niebezpieczeństwo. Najlepiej prowadzić częste refaktoryzacje wprowadzające małe zmiany.
278
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Nawet mała liczba testów jednostkowych jest lepsza niż żadna. Kod posiadający 10% pokrycia w testach naprawdę jest o wiele bezpieczniejszy niż kod, dla którego nie wprowadzono żadnych testów. Ta dodatkowa praca nad stabilnością kodu z reguły zwiększa tempo dalszych refaktoryzacji oraz powstawania nowych testów. Kiedy usuwamy zależności, aby wprowadzenie testów jednostkowych było możliwe, poprawia się także jakość kodu. To z kolei pozwala na usuwanie zależności z miejsc, gdzie było ich bardzo dużo. Przy dodawaniu testów stan projektu dzielimy na dwa różne typy: 1. Rozpoczynanie nowego projektu lub dodawanie elementów do projektu, który ma 100% kodu pokrytego testami. Możemy wtedy bezpiecznie wykorzystać TDD (jeżeli zechcemy) oraz kontynuować testy i refaktoryzację. 2. Modyfikacja kodu zastanego. Może to być nieprzetestowany projekt typu open source, kod firmowy, który odziedziczyłeś, lub Twój własny kod, który nie był wcześniej testowany. W doskonałej książce Michaela Feathersa, Working Effectively with Legacy Code (Prentice Hall 2004), kod zastany określany jest jako dowolny kod niezawierający testów. Jako programista PHP spotkasz się z obydwoma typami (najczęściej z drugim). Na szczęście programiści coraz częściej tworzą kod w miarę dobry i poprawny. Między innymi implementują standardy dotyczące tworzenia i testowania oprogramowania. UWAGA. Większość cytowanych i polecanych książek dotyczących refaktoryzacji i testów jednostkowych nie była pisana z myślą o PHP. Znajomość mocno typowanych języków, takich jak Java, C++ czy C#, nie jest niezbędna, ale przydaje się w pełnym zrozumieniu prezentowanych technik.
Ciągła integracja Trzeba dążyć do tego, aby testy naszego kodu były wykonywane często i aby były zautomatyzowane. Dzięki temu możliwe jest ustalenie szybkich i stabilnych cykli publikacji. Serwer ciągłej integracji wykonuje serie predefiniowanych zadań, takich jak wdrożenie kodu, testy lub tworzenie analiz. Te zadania wykonywane są za każdym razem, kiedy repozytorium zostanie zmienione przez zatwierdzenie kodu albo w wyznaczonych interwałach, np. co godzinę czy na żądanie. Ciągła integracja (CI) pozwala na ustawienie powtarzalnych zadań, które komputer może wykonać automatycznie. Mogą to być zadania monotonne, nudne, składać się z wielu kroków, być skomplikowane lub podatne na błędy. Poniżej pokazane są przykłady wielokrokowych zadań, które możemy ustawić jako automatyczne: 1. Pobranie aktualnej wersji kodu z repozytorium. 2. Pobranie najnowszej wersji bibliotek zewnętrznych ze strony internetowej. 3. Wykonanie statycznej analizy programu. 4. Wykonanie testów jednostkowych dla kodu programu. Załóżmy, że chcemy opublikować najnowszą wersję naszego programu. Dzięki CI możemy po poprawnym przeprowadzeniu testów jednostkowych wykonać dodatkowe kroki: 1. Zamaskować kod PHP. 2. Wygenerować plik WAR. 3. Odpytać system wersjonowania o numer aktualnej rewizji. 4. Odczytać aktualną wersję publikacji z bazy danych. 5. Przygotować aktualizację poprzedniej wersji do wersji aktualnej. 6. Oznaczyć wersję jako opublikowaną. 7. Wstawić nowy rekord do bazy danych zawierającej publikacje lub zaktualizować plik przechowujący wersje. 8. Opublikować plik WAR na ogólnie dostępnym serwerze.
279
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Wyobraź sobie, że każde z powyższych zadań musisz wykonać ręcznie, po czym uświadomisz sobie, że w kodzie znalazł się drobny błąd lub brakuje pliku. Opublikowanie poprawnej wersji wymaga powtórzenia wszystkich kroków. Ręczne powtarzanie tych czynności zabiera więcej czasu, niż chcielibyśmy na to przeznaczyć, jest podatne na błędy i generalnie mało ciekawe. Dzięki CI wszystkie czynności mogą być wykonywane automatycznie po każdym zatwierdzeniu kodu w repozytorium. Możemy także oznaczyć tylko wybrane wersje, dla których zostanie wykonanych osiem powyższych kroków.
Serwer ciągłej integracji Dwa najlepsze darmowe serwery CI dla PHP to Jenkins i phpUnderControl. Jenkins, który jest odgałęzieniem projektu Huston, jest jednym z najczęściej wykorzystywanych systemów CI na świecie. Serwer phpUnderControl integruje się z frameworkiem CI o nazwie CruiseControl. Zarówno Jenkins, jak i CruiseControl są napisane w Javie, wspierają wiele systemów budowania oraz różne języki. Oba oferują także wiele dodatkowych wtyczek. W tym rozdziale będziemy korzystać z Jenkinsa. UWAGA. Jenkins, phpUnderControl i CruiseControl dostępne są pod adresami:
http://jenkins-ci.org/ http://phpundercontrol.org/ http://sourceforge.net/projects/cruisecontrol/files/ Serwery CI wykorzystują następujące narzędzia: 1. Kontrola wersji 2. Testy jednostkowe i pokrycie kodu 3. Analiza statyczna 4. Automatyzacja budowania
System kontroli wersji Wracając do poprzedniego rozdziału — systemy kontroli wersji bywają również nazywane systemami kontroli źródła. Są niezbędnym narzędziem każdego programisty — wykorzystującego programowanie zwinne lub nie. Systemy kontroli wersji są jak cyfrowy magnetofon i mikser w jednym. Możemy odtworzyć istniejący kontent, dodawać nowy, przewinąć do danego miejsca, rozgałęzić ścieżkę lub łączyć części, które odpowiadają nam najbardziej. Czasami jednak części kolidują i musimy wykonać pewne modyfikacje, aby poszczególne fragmenty znowu zaczęły ze sobą współpracować. Jest to potężne narzędzie. Jednym z najpopularniejszych systemów kontroli wersji jest Subversion (SVN), który został omówiony w rozdziale 12. Nowa fala rozproszonych systemów kontroli wersji takich jak Git czy Mercurial także zdobywa coraz większe uznanie w środowisku programistycznym. UWAGA. Dokumentacja powyższych systemów kontroli wersji dostępna jest między innymi pod adresami:
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Analiza statyczna Narzędzia analizy statycznej wykorzystują metrykę do sprawdzenia kodu i mogą ujawnić użyteczne informacje, np.: • Poziomy złożoności obliczeniowej (im wyższy tym gorzej) • Zależności (im mniej tym lepiej) • Rady dotyczące dobrych praktyk • Przestrzeganie stylu kodowania • Problematyczny kod i potencjalne błędy • Zduplikowany kod • Tworzenie dokumentacji Narzędzia analizy statycznej dla PHP w większości dostępne są w formie wtyczek dla narzędzi programistycznych lub jako pakiety PEAR. Niektóre istniejące narzędzia zostały wymienione poniżej z podziałem na kategorie. Przestrzeganie konwencji programistycznych PhpCheckstyle http://code.google.com/p/phpcheckstyle/ PHP Code Sniffer http://pear.php.net/package/PHP_CodeSniffer/ Generowanie API PHP Documentor http://www.phpdoc.org/ Metryki jakości kodu PHP Lines of Code Metryki dotyczące ilości kodu w funkcjach, klasach itp. https://github.com/sebastianbergmann/phploc pdepend Zależności klasowe i funkcyjne http://pdepend.org/ Sugestie dotyczące jakości kodu PHP Copy/Paste Detector https://github.com/sebastianbergmann/phpcpd phpcpd (php copy/paste detector) — wykrywa zduplikowany kod phpmd PHP — wykrywa nieuporządkowany kod http://phpmd.org/ PHp ANalzer for Type Mismatches — analizator niepoprawnych typów PHP jest słabo typowanym językiem — phantm pomaga znaleźć potencjalne błędy wynikające z błędnego typowania https://github.com/colder/phantm padawan Złe praktyki w kodzie https://github.com/mayflowergmbh/padawan Podświetlanie phpcb Przeglądarka kodu PHP — wykorzystywana z PHPUnit i Code Snifferem https://github.com/mayflowergmbh/PHP_CodeBrowser
281
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Budowanie automatyzacji Aby zautomatyzować powtarzalne zadania, trzeba wiedzieć, jak korzystać z systemu budowania, takiego jak Apache Ant, Maven lub Phing. Systemy te są oparte na plikach XML. XML jest omówiony w rozdziale 14. Typowy plik budowania zawiera jeden cel bądź większą ich liczbę — każdy z podzadaniami. Zadania mogą być wykorzystane do dodawania lub usuwania plików, pobierania plików z repozytorium, uruchamiania testów jednostkowych, analizy statycznej, tworzenia dokumentacji itd. UWAGA. Więcej informacji dotyczących systemów budowania można znaleźć na ich stronach internetowych:
W tym przykładzie zdefiniowany został jeden cel — testAutomatyzacji. Wyświetlany jest komunikat oraz tworzony jest katalog. Pliki mogą zawierać wiele celów i być bardzo skomplikowane. Programiście, który nigdy nie korzystał z serwera CI, może być trudno dostrzec zalety dodatkowej pracy, która musi być wykonana w celu ustawienia środowiska CI. Po jakimś czasie, zwiększeniu liczby budowanych aplikacji oraz po uruchomieniu testów osiągniemy pełne korzyści.
Uruchomienie serwera Jenkins W tym podrozdziale zademonstrujemy uruchomienie serwera Jenkins z PHP (rysunek 13.10). Strona internetowa Jenkinsa jest intuicyjna. To bardzo popularny serwer CI skupiający wokół siebie dużą społeczność. Jest rozbudowany, ale prosty w użyciu. Posiada dobrze napisane GUI oraz możliwość zarządzania z wiersza poleceń. Instrukcja instalacji Jenkinsa z PHP znajduje się pod adresem http://jenkins-php.org/; została napisana przez Sebastiana Bergmanna (autora PHPUnit). Podstawowe kroki są następujące: 1. Pobranie i instalacja Jenkinsa 2. Konfiguracja wtyczek Jenkinsa 3. Aktualizacja pakietów pear PHP 4. Utworzenie strony budowania 5. Utworzenie nowego zadania Jenkinsa
282
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Rysunek 13.10. Strona internetowa Jenkinsa Pobierz Jenkinsa ze strony http://jenkins-ci.org/. Proces instalacji jest zależny od systemu operacyjnego i wersji Jenkinsa. Pomoc dotycząca rozszerzeń jest dostępna pod adresem https://wiki.jenkinsci.org/display/JENKINS/Home. Domyślnie używany jest port 8080, a kokpit dostępny jest pod adresem http://localhost:8080/dashboard. Aby skonfigurować wtyczki, możemy skorzystać z interfejsu WWW lub z wiersza poleceń (listing 13.25) — zobacz rysunek 13.11. Listing 13.25. Instalowanie wtyczek za pośrednictwem wiersza poleceń wget java java java java java java java
Rysunek 13.11. Zarządzanie Jenkinsem za pośrednictwem GUI
283
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Być może będziesz musiał zmodyfikować zainstalowane moduły pear lub zainstalować nowe. Wszystkie błędy zgłoszone podczas instalacji pear muszą być naprawione, aby automatyzacja budowania działała poprawnie. pear pear pear pear pear
UWAGA. Jeżeli pojawią się komunikaty o błędach, być może będziesz zmuszony zmienić konfigurację pear. Na przykład katalog data_dir może być niepoprawnie ustawiony. http://pear.php.net/manual/en/guide.users.commandline.config.php ERROR: failed to mkdir C:\php\pear\data\PHP_PMD\resources\rulesets pear config-get data_dir "C:\php5" #incorrect pear config-set data_dir "C:\xampp\php\pear\data" pear config-set doc_dir "C:\xampp\php\pear\docs" pear config-set test_dir "C:\xampp\php\pear\tests"
Bergmann napisał kilka skryptów, które mogą posłużyć jako dobry punkt wyjścia bądź szablony dla Twojego serwera CI. Na pewno będziesz musiał dostosować ścieżki i (lub) zmodyfikować cele. Skrypty można pobrać za pośrednictwem menedżera pakietów pear poleceniem: pear install phpunit/ppw
lub pod adresem https://github.com/sebastianbergmann/php-project-wizard. Rysunek 13.12 pokazuje główne menu Jenkinsa, gdzie możemy utworzyć nowe zadanie.
Rysunek 13.12. Główne menu Jenkinsa
284
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
Podsumowanie W tym rozdziale omówiliśmy trzy metody, które mogą w znaczny sposób poprawić jakość kodu PHP. Te metody to refaktoryzacja, testy jednostkowe oraz ciągła integracja. Wszystkie te obszary intensywnie się rozwijają; powstały doskonałe książki na ich temat. Większość kodu, z jakim będziesz miał do czynienia, będzie kodem zastanym, więc jako programista musisz znać sposoby uzyskiwania stabilności w początkowo niepoprawnym kodzie. Refaktoryzacja to kluczowa umiejętność; z czasem nabierzesz wprawy. Nieraz jednak nawet znawcy tematu i eksperci mogą niechcący wprowadzić subtelne zmiany w zachowaniu kodu. Zmiany te mogą przez długi czas pozostawać niezauważone i być bardzo trudne w późniejszym zlokalizowaniu. Testy jednostkowe pomagają śledzić oczekiwane wyniki działania funkcji i wykrywać nieplanowane zmiany wprowadzone w strukturze kodu. Aby nasze testy były regularnie uruchamiane, możemy wykorzystać ciągłą integrację. Systemy CI pomagają wyeliminować nudne i powtarzalne (a co za tym idzie, podatne na błędy) zadania. Komputery są świetne w wykonywaniu zadań powtarzalnych i nigdy nie narzekają, że praca, którą wykonują, jest nudna. Powinniśmy starać się, aby poziom sprawdzania jakości kodu był zawsze wysoki, a także zawsze poszukiwać nowych technik i narzędzi.
285
ROZDZIAŁ 13. REFAKTORYZACJA, TESTY JEDNOSTKOWE I CIĄGŁA INTEGRACJA
286
ROZDZIAŁ 14
XML
XML (ang. Extensible Markup Language) jest potężnym narzędziem do przechowywania i przesyłania danych. Dokument zapisany w XML-u może być łatwo rozumiany i wymieniany przez wiele programów. Nie można nie doceniać wagi faktu, że XML jest międzynarodowym standardem. Wykorzystywany jest w nowoczesnych edytorach tekstu, usługach sieciowych SOAP i REST, kanałach RSS oraz dokumentach XHTML. W tym rozdziale zajmiemy się głównie rozszerzeniem PHP — SimpleXML, które pozwala na łatwą manipulację dokumentami XML. Omówimy także z grubsza drzewo DOM (ang. Document Object Model) oraz rozszerzenia XMLReader. DOM gwarantuje, że dokument będzie wyświetlany zawsze w ten sam sposób, niezależnie od języka programowania, za którego pośrednictwem z niego korzystamy. Istnieje wiele rozszerzeń XML — różnią się one łatwością użycia, poziomem funkcjonalności oraz sposobem, w jaki wykonywane są operacje na dokumencie XML.
Podstawy XML XML pozwala na tworzenie dokumentów wykorzystujących dowolne znaczniki i atrybuty. Oglądając dokument XML w edytorze tekstu, możesz zauważyć, że bardzo przypomina HTML. Jest tak dlatego, że podobnie jak HTML (ang. HyperText Markup Language), XML jest językiem znaczników — przechowuje kolekcję oznaczonej zawartości uporządkowanej hierarchicznie. Struktura hierarchiczna przyjmuje formę drzewa mającego jeden element główny (root) — korzeń — elementy potomne odchodzące od elementu głównego oraz elementy potomne odchodzące od ich elementów nadrzędnych. Dokument XML można przeglądać również jako serię odrębnych zdarzeń. Zauważ, że przeglądanie elementów w kolejności nie wymaga żadnej wiedzy o tym, co zawiera cały dokument; utrudnia to jednocześnie jego przeszukiwanie. Specyficznym przykładem zastosowania XML-a jest XHTML. Podobieństwo XHTML-u i HTML-u polega na tym, że wykorzystane są te same znaczniki. Jednakże XHTML stosuje się do standardów XML i w związku z tym jest bardziej rygorystyczny. XHTML posiada następujące dodatkowe wymagania: • W nazwach znaczników rozróżnia się wielkie i małe litery. W dokumencie XHTML wszystkie znaczniki muszą być pisane małymi literami. • Pojedyncze elementy, takie jak , muszą być domknięte — w tym przypadku jest to: . • Encje &, <, >, ', " muszą być zapisywane w kolejności: &, <, >, ' i ". • Atrybuty muszą być zapisane pomiędzy znakami cudzysłowu. Na przykład nie jest poprawne, podczas gdy już tak. Aby sparsować dokument XML, mamy do wyboru dwa podejścia — oparte na strukturze drzewiastej oraz oparte na zdarzeniach. Modele drzewiaste, jak te w SimpleXML i DOM, prezentują dokumenty HTML i XML
ROZDZIAŁ 14. XML
jako drzewo elementów — w tym przypadku cała struktura wczytywana jest do pamięci. Każdy element, poza elementem głównym, posiada element nadrzędny. Elementy mogą zawierać atrybuty i wartości. Modele oparte na zdarzeniach, takie jak Simple API for XML (SAX), wczytują do pamięci tylko część dokumentu. W przypadku dużych dokumentów SAX jest szybszy; w przypadku bardzo dużych może być jedyną opcją. Modele drzewiaste są z reguły bardziej intuicyjne, a niektóre dokumenty muszą być wczytywane w całości. Podstawowy dokument XML może wyglądać następująco: piesreksio
Elementem głównym jest element , który posiada dwa elementy potomne: oraz . Wartością elementu jest pies, natomiast wartością elementu jest reksio. Element posiada jeden atrybut — id — którego wartość wynosi 9. Co więcej, każdy znacznik otwierający ma pasujący znacznik zamykający, a wartość atrybutu zawarta jest w cudzysłowach.
Schematy Schematy wprowadzają dodatkowe warunki do dokumentów XML. Mogą to być na przykład elementy opcjonalne lub takie, które muszą być zawarte w dokumencie, dozwolone wartości i atrybuty elementu oraz dopuszczalne miejsca, w których element może być umiejscowiony. Bez schematu możliwe jest wprowadzanie w dokumencie nonsensownych danych, jak na listingu 14.1. Listing 14.1. Przykład pokazujący konieczność stosowania schematów czarnyreksiobrązowytabby beagle cross misiu
Dla ludzi ten dokument nie ma żadnego sensu. Element kot nie może być częścią elementu pies. Elementy kolor i imie nie są zwierzętami i powinny być umieszczone wewnątrz elementów kot lub pies. Jednakże z perspektywy maszyny jest to całkowicie poprawny dokument. Musimy podać jej powody, dla których dokument nie jest poprawny. Schemat pozwala poinformować maszynę, w jaki sposób dane powinny być ułożone. Dzięki temu może być zapewniona integralność danych w dokumencie. Schemat pozwala określić, że elementy nie mogą być zawarte w elementach oraz że elementy i mogą być umieszczone tylko wewnątrz elementów i . Trzy najpopularniejsze języki tworzenia schematów to DTD (ang. Document Type Definition), XML Schema i RELAX NG (ang. REgular LAnguage for XML Next Generation). Ponieważ książka dotyczy PHP, nie będziemy się zagłębiać w szczegóły tworzenia schematów; nadmienimy tylko, że schemat deklarowany jest na początku dokumentu (listing 14.2).
288
ROZDZIAŁ 14. XML
Listing 14.2. Fragment kodu pokazujący wykorzystanie schematu xhtml1-transitional
SimpleXML Dzięki SimpleXML możemy łatwo przechować XML jako obiekty PHP i odwrotnie. SimpleXML upraszcza poruszanie się po strukturze dokumentu XML oraz znajdowanie konkretnych informacji. Rozszerzenie SimpleXML wymaga PHP5 lub nowszego i jest domyślnie włączone.
Parsowanie XML z tekstu Zacznijmy od razu od przykładu. Wczytamy dokument XML zapisany jako tekst do obiektu SimpleXMLElement, a następnie przeiterujemy po jego elementach. Listing 14.3. Pierwszy przykład — listing14_3.php piesreksio XML; //Aby wczytać dokument XML do obiektu SimpleXMLElement, wystarczy jedna linia kodu. $xml_obiekt = simplexml_load_string($xml); foreach ($xml_obiekt as $element => $wartosc) { print $element . ": " . $wartosc . " "; } ?>
Po wczytaniu dokumentu z listingu 14.3 zmienna $xml_obiekt reprezentuje element główny dokumentu — . Dokument jest reprezentowany przez obiekt SimpleXMLElement, dzięki czemu możemy przeiterować po elementach potomnych w pętli foreach. Wynik działania skryptu z listingu 14.3 jest następujący: typ: pies imie: reksio
Listing 14.4 prezentuje przykład operacji na bardziej złożonym dokumencie XML. Listing 14.4. Bardziej złożony przykład — listing14_4.php
289
ROZDZIAŁ 14. XML
reksiobrązowybeagle crossbzikbrązowytabbyazorczarnylab cross XML; $xml_obiekt = simplexml_load_string($xml); //wyświetlenie imion wszystkich psów foreach($xml_obiekt->pies as $pies){ print $pies->imie." "; } ?>
Wynik działania skryptu jest następujący: reksio azor
Większość kodu z listingu 14.4 to wykorzystanie składni heredoc do zapisania dokumentu XML w czytelny sposób. Właściwy kod wyszukujący odpowiednie wartości to zaledwie parę linii. SimpleXML jest wystarczająco sprytne, aby przeiterować po wszystkich elementach pies, nawet pomimo elementu kot wstawionego pomiędzy nie.
Parsowanie XML z pliku Podczas wczytywania XML-a PHP zgłosi błąd wraz z pomocnym komunikatem, jeżeli dokument jest niepoprawny. Wiadomość może informować, że musisz zamknąć tag lub zamienić encję, wskaże także numer linii, w której występuje błąd (listing 14.5). Listing 14.5. Prosty komunikat PHP informujący o błędzie w dokumencie Warning: simplexml_load_string() [function.simplexml-load-string]: Entity: line 1: parser error : ´attributes construct error in E:\xampp\htdocs\xml\zwierzeta.php on line 29
Kolejne dwa przykłady dotyczą wczytania pliku XML widocznego na listingu 14.6. Niektóre z elementów XML mają atrybuty. Na listingu 14.7 pokażemy, w jaki sposób znajdować wartości atrybutów przy użyciu kolejnych wywołań SimpleXML. Następnie zaprezentujemy na listingu 14.10, jak znaleźć wartość atrybutów przy wykorzystaniu XPath, którego zadaniem jest uproszczenie wyszukiwania. Listing 14.6. Prosty dokument XHTML — template.xhtml
290
ROZDZIAŁ 14. XML
tutaj byłby nagłówek
tutaj byłoby menu
lewa kolumna
główna zawartość
prawa kolumna
tutaj byłaby stopka
Pierwsze dwie linie na listingu 14.6 definiują wykorzystaną wersję XML-a oraz DOCTYPE i nie są wczytywane do obiektu SimpleXMLElement jako część drzewa. Elementem głównym jest element . Skrypt 14.7 przedstawia zastosowanie metod SimpleXML do znalezienia elementu
mającego atrybut id="glowna_srodek". Listing 14.7. Znalezienie atrybutu o zadanej wartości body->div as $divy) { if (!empty($divy->div)) { foreach ($divy->div as $wewnetrzne_divy) { if (czyElementMaId($wewnetrzne_divy, $id)) { break 2; } } } else { if (czyElementMaId($divy, $id)) { break; } } } } function czyElementMaId($element, $id) { $id_elementu = (String) $element->attributes()->id; if ($id_elementu == $id) { $wartosc = trim((String) $element); print "wartość elementu #$id to: $wartosc"; return true; }
291
ROZDZIAŁ 14. XML
return false; } ?>
Skrypt z listingu 14.7 znajdzie wszystkie elementy
zawarte wewnątrz elementu oraz elementy
bezpośrednio im podległe. Dla każdego elementu wartość atrybutu id porównywana jest z wartością szukaną glowna_srodek. Jeżeli są równe, wyświetlamy wartość elementu i kończymy działanie pętli. Wynik działania skryptu jest następujący: wartość elementu #glowna_srodek to: główna zawartość
Nie możemy po prostu wyświetlić zmiennej $element wewnątrz funkcji czyElementMaId, ponieważ wtedy wyświetlony zostałby cały obiekt SimpleXMLElement. object(SimpleXMLElement)[10] public '@attributes' => array 'id' => string 'glowna_srodek' (length=13) 'class' => string 'test' (length=4) string ' główna zawartość ' (length=50)
Musimy więc rzutować zwracaną wartość z typu Object na String (pamiętaj, że rzutowanie konwertuje zmienną z jednego typu na inny). Zwróć uwagę, że zwracane są także białe znaki, więc musimy wykorzystać funkcję trim(). Do pobrania atrybutów możemy zastosować funkcję SimpleXML, attributes(), która zwraca obiekt z atrybutami. var_dump($element->attributes()); object(SimpleXMLElement)[9] public '@attributes' => array 'id' => string 'glowna_srodek' (length=13) 'class' => string 'test' (length=4)
Także w przypadku wartości $element->attributes()->id musimy wykonać rzutowanie — w przeciwnym razie znowu otrzymamy obiekt klasy SimpleXMLElement. Kod z listingu 14.7 nie jest do końca poprawny. Jeżeli zmianie ulegnie struktura dokumentu lub poziom zagłębień będzie wyższy niż dwa, identyfikator nie zostanie znaleziony. Mogłeś zauważyć, że dokumenty XHTML zachowują obiektowy model dokumentu (DOM) znany z HTML-u. Istniejące parsery i oprogramowanie pozwalające poruszać się po drzewie, takie jak XPath czy XQuery, znacznie ułatwiają znajdowanie zagnieżdżonych elementów. XPath jest składnikiem biblioteki SimpleXML, ale także biblioteki PHP DOM. Przy wykorzystaniu SimpleXML zapytanie XPath wywoływane jest za pomocą funkcji $simple_xml_object->xpath(). W bibliotece DOM zapytanie XPath wykonuje się za pomocą tworzonego obiektu DOMXPath, wywołując jego metodę query. Na listingu 14.10 pokazano, w jaki sposób znaleźć szukany atrybut przy wykorzystaniu XPath. Najpierw pokażemy jednak, jak znaleźć elementy wybierane na listingach 14.3 i 14.4 — zobacz listing 14.8. Listing 14.8. Wyszukiwanie elementu przy użyciu XPath piesreksio
Wynik działania skryptu: pies typ: pies imie: reksio
W pierwszej części skryptu z listingu 14.8 wybieramy element , będący potomkiem elementu , wykorzystując selektor XPath typ. Zwrócona zostanie tablica pasujących do zapytania obiektów typu SimpleXMLElement. W drugiej części wybieramy wszystkie elementy potomne elementu przy użyciu selektora /zwierze/* — gwiazdka oznacza dziką kartę. Kiedy obiekty SimpleXMLElement zostaną zwrócone z funkcji xpath(), możemy wypisać ich nazwy przy zastosowaniu metody getName(). UWAGA. Pełna specyfikacja selektorów XPath dostępna jest pod adresem http://www.w3.org/TR/xpath/.
Na listingu 14.9 pokazano, jak znaleźć dany element potomny niezależnie od jego elementu nadrzędnego. Listing demonstruje także, jak znaleźć element nadrzędny elementu w obiekcie SimpleXMLElement. Listing 14.9. Wyszukiwanie elementów potomnych i nadrzędnych przy wykorzystaniu XPath reksiobrązowybeagle crossbzikbrązowytabbyazorczarnylab cross
Wynik działania skryptu jest następujący: reksio (pies) bzik (kot) azor (pies)
Za pomocą zapytania XPath — */imie — dopasowaliśmy element , niezależnie od tego, czy zawarty był wewnątrz elementu , czy . Aby pobrać element nadrzędny elementu z obiektu SimpleXMLElement, zastosowaliśmy zapytanie ... Zamiast tego mogliśmy wykorzystać zapytanie parent::*. Listing 14.10. Wyszukiwanie atrybutu o zadanej wartości przy zastosowaniu XPath xpath("//*[@id='glowna_srodek']"); print (String)$zawartosc[0]; ?>
Na listingu 14.10 użyliśmy zapytania //*[@id='glowna_srodek'], aby znaleźć element o wartości atrybutu id równej glowna_srodek. Aby wyszukiwać wartości parametrów w XPath, wykorzystujemy znak @. Zauważ, jak prosty jest skrypt z listingu 14.10 w porównaniu ze skryptem z listingu 14.7.
Przestrzenie nazw Przestrzenie nazw XML określają, do jakiej kolekcji należą elementy — zapobiegają tym samym dwuznaczności danych. Dwuznaczność może się pojawić, jeżeli masz dwa różne typy węzłów zawierające elementy o takich samych nazwach. Możesz na przykład zdefiniować odrębne przestrzenie nazw dla elementów i , aby zapewnić, że ich wewnętrzne elementy będą miały unikalne nazwy — zostało to zademonstrowane na listingach 14.11 i 14.12. Aby uzyskać więcej informacji na temat przestrzeni nazw, odwołaj się do rozdziału 5., „Nowości technologiczne”. Pierwszą rzeczą, jaką należy wykonać, jest zadeklarowanie przestrzeni nazw za pomocą xmlns:your_namespace:
Następnie nazwy elementów należy poprzedzić odpowiednim prefiksem. Szukając imion psów, możesz szukać elementów — dzięki temu znajdziesz wyłącznie imiona psów. Listing 14.11 demonstruje sposób postępowania z przestrzeniami nazw w dokumentach XML. Wywołanie tego skryptu nie spowoduje wyświetlenia jakiegokolwiek komunikatu. Podczas wykonywania XPath musimy zarejestrować przestrzenie nazw. Zobacz listing 14.12.
294
ROZDZIAŁ 14. XML
Listing 14.11. Brak możliwości znalezienia danych w dokumencie bez zdefiniowanych przestrzeni nazw reksiobrązowybeagle crossbzikbrązowytabbyazorczarnylab cross XML; $xml_obiekt = simplexml_load_string($xml); $imiona = $xml_obiekt->xpath("imie"); foreach ($imiona as $imie) { print $imie . " "; } ?>
Listing 14.12. Wyszukanie danych w dokumencie po zarejestrowaniu przestrzeni nazw reksiobrązowybeagle crossbzikbrązowytabbyazorczarnylab cross XML; $xml_obiekt = simplexml_load_string($xml); $xml_obiekt->registerXPathNamespace('kot', 'http://test.pl/kot'); $xml_obiekt->registerXPathNamespace('pies', 'http://test.pl/pies'); $imiona = $xml_obiekt->xpath("pies:imie"); foreach ($imiona as $imie) { print $imie . " "; } ?>
Wynik jest następujący: reksio azor
295
ROZDZIAŁ 14. XML
W skrypcie z listingu 14.12 po zarejestrowaniu przestrzeni nazw musieliśmy także wykorzystać odpowiedni prefiks w zapytaniu XPath. Na listingu 14.13 zastosujemy XPath do znalezienia elementu o zadanej wartości. Następnie odczytamy wartość atrybutu tego elementu. Listing 14.13. Sprawdzenie wartości atrybutu elementu o zadanej wartości reksiobrązowybeagle crossbzikbrązowytabbyazorczarnylab cross XML; $xml_obiekt = simplexml_load_string($xml); $wynik = $xml_obiekt->xpath("pies/imie[contains(., 'azor')]"); print (String)$wynik[0]->attributes()->id; ?>
Na listingu 14.13 wykorzystaliśmy funkcję XPath — contains — przyjmującą dwa parametry: pierwszy oznacza miejsce wyszukiwania (. oznacza aktualny węzeł), a drugi szukaną wartość. Funkcja ma format parametrów (miejsce_wyszukiwania, szukana_wartość). Po wykonaniu zapytania otrzymujemy pasujący obiekt SimpleXMLObject, dla którego wyświetlamy wartość atrybutu id. XPath daje ogromne możliwości. Każdy, kto zna wysokopoziomowy język JavaScript, np. jQuery, zna już dużą część składni XPath. Gdy nauczysz się XPath i DOM, będziesz mógł zaoszczędzić czas, a Twoim skryptom będzie można ufać w większym stopniu.
RSS Really Simple Syndication (RSS) jest prostą metodą publikowania i subskrybowania informacji jako kanałów. Zasady są jednakowe dla wszystkich kanałów RSS — przyjrzyjmy się jednak przykładowi z magazynu „Wired”. Kanał dostępny jest pod adresem http://feeds.wired.com/wired/index?format=xml. Kod wiadomości z kanału wygląda następująco:
296
ROZDZIAŁ 14. XML
Wired Top Stories http://www.wired.com/rss/index.xml Top Storiesen-usCopyright 2007 CondeNet Inc. All rights reserved.Sun, 27 Feb 2011 16:07:00 GMTWired.com2011-02-27T16:07:00Zen-usCopyright 2007 CondeNet Inc. All rights reserved.Peers Or Not? Comcast And Level 3 Slug It Out At FCC's Doorstep http://feeds.wired.com/~r/wired/index/~3/QJQ4vgGV4qM/ pierwszy opisSun, 27 Feb 2011 16:07:00 GMThttp://www.wired.com/epicenter/2011/02/comcast-level-fcc/Matthew Lasar2011-02-27T16:07:00Zhttp://www.wired.com/epicenter/2011/02/comcast-level-fcc/360 Cams, AutoCAD and Miles of Fiber: Building an Oscars Broadcast http://feeds.wired.com/~r/wired/index/~3/vFb527zZQ0U/ drugi opisSun, 27 Feb 2011 00:19:00 GMThttp://www.wired.com/underwire/2011/02/oscars-broadcast/Terrence Russell2011-02-27T00:19:00Zhttp://www.wired.com/underwire/2011/02/oscars-broadcast/ … … …
Aby zmniejszyć objętość dokumentu, zawartość elementu description została zamieniona. Jak widać, dokument RSS to po prostu XML. Istnieje wiele bibliotek pozwalających na parsowanie wiadomości XML — w rozdziale 10. pokazaliśmy, w jaki sposób wykorzystać do tego bibliotekę SimplePie. Przy Twojej znajomości XML-a możesz jednak z łatwością sparsować go samodzielnie. Skrypt z listingu 14.14 buduje tabelę zawierającą podstawowe informacje z kanału RSS: tytuł przekierowujący do pełnego artykułu, imię i nazwisko twórcy dokumentu oraz datę publikacji. Zauważ, że element creator należy do przestrzeni nazw — pobieramy go za pomocą XPath. Wynik pokazany jest na rysunku 14.1. Listing 14.14. Parsowanie kanału RSS ze strony magazynu „Wired”
"; //równoważne pobranie imienia i nazwiska autora przy wykorzystaniu funkcji children zamiast xpath //$autor_przestrzen_nazw = $pozycja->children('http://purl.org/dc/elements/1.1/')->creator; //print "
".(String)$autor_przestrzen_nazw[0]."
"; } ?>
Rysunek 14.1. Wynik działania parsera RSS z listingu 14.14 Na listingu 14.14 wykorzystaliśmy XPath w celu uzyskania informacji o autorze zawartych w elemencie creator należącym do przestrzeni nazw dc. Mogliśmy także pobrać element potomny elementu $pozycja należący do zadanej przestrzeni nazw. Jest to proces dwuetapowy. Najpierw musimy znaleźć informację o tym, co oznacza dc.
Następnie musimy przekazać adres przestrzeni nazw do parametru funkcji children. //$autor_przestrzen_nazw = $pozycja->children('http://purl.org/dc/elements/1.1/')->creator;
Generowanie dokumentów XML za pomocą SimpleXML Do tej pory używaliśmy SimpleXML wyłącznie do parsowania dokumentów. Rozszerzenie to może być także wykorzystane do generowania dokumentów XML na podstawie istniejących danych. Dane mogą być w formie tablicy, obiektu lub bazy danych. Aby programistycznie utworzyć dokument XML, musimy utworzyć nowy obiekt klasy SimpleXMLElement, który będzie stanowił element główny naszego dokumentu. Następnie są dodawane do niego elementy potomne, a do nich kolejne (listing 14.15). Listing 14.15. Tworzenie podstawowego dokumentu XML przy wykorzystaniu SimpleXML '); $zwierzeta->{0} = 'Witaj, świecie'; $zwierzeta->asXML('zwierzeta.xml');
298
ROZDZIAŁ 14. XML
//sprawdzenie potencjalnych błędów dla nowo utworzonego pliku var_dump(simplexml_load_file('zwierzeta.xml')); ?>
Wyświetli się komunikat: object(SimpleXMLElement)[2] string 'Witaj, świecie' (length=14)
Utworzony zostanie plik zawierający: Witaj, świecie
Skrypt z listingu 14.15 tworzy element główny — zwierzeta — przypisuje do niego wartość oraz wywołuje metodę asXML zapisującą plik. Aby przetestować poprawność skryptu, wczytujemy zawartość zapisanego pliku, a następnie ją wyświetlamy. Upewnij się, że posiadasz uprawnienia do zapisu w lokalizacji, w której chcesz utworzyć plik. Na listingu 14.16, który jest nawiązaniem do skryptu z listingu 14.4, mamy dane o zwierzętach zapisane w tablicach; na ich podstawie chcemy utworzyć dokument XML. Listing 14.16. Tworzenie podstawowego dokumentu XML przy wykorzystaniu SimpleXML "reksio", "kolor" => "brązowy", "rasa" => "beagle cross" ), array("imie" => "azor", "kolor" => "czarny", "rasa" => "lab cross" ), ); $koty_tablica = array( array("imie" => "bzik", "kolor" => "brązowy", "rasa" => "tabby" ), ); //Generowanie XML-a rozpoczynamy od elementu głównego. $zwierzeta = new SimpleXMLElement(''); $koty_xml = $zwierzeta->addChild('koty'); $psy_xml = $zwierzeta->addChild('psy'); foreach ($koty_tablica as $k) { $kot = $koty_xml->addChild('kot'); foreach ($k as $klucz => $wartosc) { $tmp = $kot->addChild($klucz); $tmp->{0} = $wartosc; }
'; //sprawdzenie nowego pliku var_dump(simplexml_load_file('zwierzeta.xml')); ?>
Na listingu 14.16 tworzymy element główny za pomocą polecenia new SimpleXMLElement('');. Aby zasilić nasz dokument od elementu głównego w dół, tworzymy elementy potomne, wywołując metodę addChild i przechowując referencję do nowego elementu. Korzystając z referencji, możemy dodać elementy potomne. Powtarzając ten proces, możemy utworzyć całe drzewo węzłów. Niestety, funkcja asXML nie formatuje w żaden sposób wynikowego XML-a — cała zawartość pojawia się w jednej linii. Aby to obejść i wyświetlić XML w ładniejszej formie, możemy wykorzystać klasę DOMDocument, którą omówimy w dalszej części rozdziału. $zwierzeta_dom = new DOMDocument('1.0'); $zwierzeta_dom->preserveWhiteSpace = false; $zwierzeta_dom->formatOutput = true; //zwraca DOMElement $zwierzeta_dom_xml = dom_import_simplexml($zwierzeta); $zwierzeta_dom_xml = $zwierzeta_dom->importNode($zwierzeta_dom_xml, true); $zwierzeta_dom_xml = $zwierzeta_dom->appendChild($zwierzeta_dom_xml); $zwierzeta_dom->save('zwierzeta_sformatowane.xml');
Ten kod tworzy nowy obiekt klasy DOMDocument oraz ustawia go tak, aby formatował dokument. Następnie importujemy element SimpleXMLElement do nowego obiektu DOMElement. Rekurencyjnie wstawiamy węzły do dokumentu, a następnie zapisujemy sformatowany plik. Zamiana powyższego kodu w miejscu wywołania funkcji asXML z listingu 14.16 spowoduje powstanie czystego, zagnieżdżonego dokumentu: bzikbrązowytabbyreksiobrązowybeagle crossazorczarny
300
ROZDZIAŁ 14. XML
lab cross
UWAGA. SimpleXML może także importować obiekty DOM za pomocą funkcji simplexml_import_dom. Brian"); $simple_xml = simplexml_import_dom($dom_xml); print $simple_xml->imie; // brian ?>
Skrypt z listingu 14.17 wygeneruje przykładową wiadomość RSS z przestrzeniami nazw i atrybutami. Naszym celem jest uzyskanie dokumentu posiadającego następującą strukturę: Kanał RSS BrianaNajnowsze wpisy na blogu Briana http://www.briandanchilla.com/wezel/feed Fri, 04 Feb 2011 00:11:08 +0000 Fri, 04 Feb 2011 08:25:00 +0000 Udawany tematUdawany opis http://www.briandanchilla.com/udawany-link/ unikalnie generowany łańcuch znakówFri, 04 Feb 2011 08:25:00 +0000
Najważniejsze elementy powyższego kodu to ustawienie przestrzeni nazw w elemencie głównym, $rss_xml = new SimpleXMLElement(''), pobranie przestrzeni nazw, jeżeli nazwa klucza to pubDate, oraz wygenerowanie daty w formacie RFC 2822 dla klucza lastBuildDate.
Zawartość pliku po uruchomieniu skryptu z listingu 14.17 będzie zbliżona do poniższej: ab c dFri, 23 Dec 2011 16:35:14 +0100ea2b2 c2 d2Fri, 23 Dec 2011 16:35:14 +0100e2
302
ROZDZIAŁ 14. XML
UWAGA. Więcej informacji dotyczących SimpleXML można znaleźć pod adresem http://php.net/manual/en/book. simplexml.php. Aby sprawdzić błędy w niepoprawnym pliku XML, możesz wykorzystać walidator dostępny online pod adresem http://validator.w3.org/check.
DOMDocument Jak wspomniano na początku tego rozdziału, SimpleXML nie jest jedyną opcją, jeżeli chodzi o manipulowanie dokumentami XML w PHP. Innym popularnym rozszerzeniem jest DOM. Podczas formatowania dokumentu wynikowego mogliśmy zauważyć, że ma on kilka opcji bardziej zaawansowanych niż SimpleXML. DOMDocument ma większe możliwości, ale jak można się spodziewać, nie jest tak prosty w wykorzystaniu. W większości przypadków prawdopodobnie wybierzesz SimpleXML zamiast DOM. DOM ma jednak następujące zalety: • Stosuje się go do W3C DOM API — jeżeli znasz JavaScript, łatwiej nauczysz się DOM. • Wspiera parsowanie HTML. • Różnorodne typy węzłów XML umożliwiają lepszą kontrolę nad dokumentem. • Może dopisywać XML do istniejących dokumentów XML. • Ułatwia wprowadzanie zmian w istniejących dokumentach poprzez modyfikowanie lub usuwanie węzłów. • Daje większą kontrolę nad zawartością CDATA i nad komentarzami. W SimpleXML wszystkie węzły są jednakowe. Element wykorzystuje więc ten sam obiekt jako argument. DOM ma odrębne typy węzłów. Są to XML_ELEMENT_NODE, XML_ATTRIBUTE_NODE i XML_TEXT_NODE. W zależności od rodzaju węzła odpowiednie właściwości to tagName dla elementów, name i value dla atrybutów oraz nodeName i nodeValue dla tekstu. //tworzenie obiektu DOMDocument $dom_xml = new DOMDocument(); DOMDocument może wczytać XML z ciągu znaków lub pliku albo zaimportować obiekt SimpleXML. //z ciągu znaków $dom_xml->loadXML('ciąg znaków zawierający XML'); //z pliku $dom_xml->load('animals.xml'); // import obiektu SimpleXML $dom_element = dom_import_simplexml($simplexml); $dom_element = $dom_xml->importNode($dom_element, true); $dom_element = $dom_xml->appendChild($dom_element);
Aby poruszać się w obrębie obiektu DOM, należy wywoływać metody obiektu takie jak: $dom_xml->item(0)->firstChild->nodeValue $dom_xml->childNodes $dom_xml->parentNode $dom_xml->getElementsByTagname('div');
Dostępnych jest kilka funkcji zapisujących: save, saveHTML, saveHTMLFile i saveXML. DOMDokument udostępnia funkcję walidującą, pozwalającą na sprawdzenie poprawności dokumentu. Aby wykorzystać XPath, konieczne jest utworzenie nowego obiektu klasy DOMXPath. $xpath = new DOMXPath($dom_xml);
303
ROZDZIAŁ 14. XML
Aby lepiej zilustrować różnicę pomiędzy rozszerzeniami SimpleXML i DOM, następne dwa przykłady wykorzystujące DOM są równoważne z przykładami demonstrującymi SimpleXML, pokazanymi wcześniej. Skrypt z listingu 14.18 wyświetla wszystkie imiona zwierząt wraz z typem zwierzęcia zapisanym w nawiasie. Jest alternatywą dla przykładu z listingu 14.9, który wykorzystywał SimpleXML. Listing 14.18. Wyszukiwanie elementów za pomocą DOM reksiobrązowybeagle crossbzikbrązowytabbyazorczarnylab cross XML; $xml_obiekt = new DOMDocument(); $xml_obiekt->loadXML($xml); $xpath = new DOMXPath($xml_obiekt); $imiona = $xpath->query("*/imie"); foreach ($imiona as $element) { $typ_rodzica = $element->parentNode->nodeName; echo "$element->nodeValue ($typ_rodzica) "; } ?>
Zauważ, że na listingu 14.18 musieliśmy utworzyć obiekt klasy DOMXPath, a następnie wywołać jego metodę query. Na listingu 14.9 możemy bezpośrednio odwołać się do elementu nadrzędnego. Zauważ także, że na listingu 14.18 odwołujemy się do wartości i nazw węzłów poprzez parametry, natomiast na listingu 14.9 poprzez metody. Listing 14.19 pokazuje, w jaki sposób znaleźć element o określonej wartości, a następnie odwołać się do jego atrybutu. Skrypt jest odpowiednikiem skryptu z listingu 14.13. Listing 14.19. Wyszukiwanie elementu i wartości atrybutu za pomocą DOM reksiobrązowybeagle cross
304
ROZDZIAŁ 14. XML
bzikbrązowytabbyazorczarnylab cross XML; $xml_obiekt = new DOMDocument(); $xml_obiekt->loadXML($xml); $xpath = new DOMXPath($xml_obiekt); $wyniki = $xpath->query("pies/imie[contains(., 'azor')]"); foreach ($wyniki as $element) { print $element->attributes->getNamedItem("id")->nodeValue; } ?>
Najważniejszą rzeczą, jaką należy zauważyć, jest zastosowanie attributes->getNamedItem("id")->nodeValue do pobrania wartości atrybutu id. W przypadku SimpleXML na listingu 14.13 wykorzystaliśmy attributes()->id.
XMLReader i XMLWriter Rozszerzenia XMLReader i XMLWriter stosuje się łącznie. Są trudniejsze w wykorzystaniu niż SimpleXML i DOM. XMLReader i XMLWriter to dobre rozwiązanie (często jedyne) dla bardzo dużych dokumentów, które jest oparte na zdarzeniach i nie wymaga wczytywania całego dokumentu do pamięci. Jednym z wymogów jest jednak to, aby przed rozpoczęciem pracy znany był cały schemat dokumentu. Za pomocą XMLReader możesz uzyskać większość wartości, powtarzając wywołanie metody read(), sprawdzając typ węzła oraz pobierając wartość. Listing 14.20 jest odpowiednikiem skryptu z listingu 14.4, wykorzystującego SimpleXML. Listing 14.20. Wyszukiwanie elementów za pomocą XML Reader reksiobrązowybeagle crossbzikbrązowytabbyazorczarny
305
ROZDZIAŁ 14. XML
lab cross XML; $xml_obiekt = new XMLReader(); $xml_obiekt->XML($xml); $pies_rodzic = false; while ($xml_obiekt->read()) { if ($xml_obiekt->nodeType == XMLREADER::ELEMENT) { if ($xml_obiekt->name == "kot") { $pies_rodzic = false; } else if ($xml_obiekt->name == "pies") { $pies_rodzic = true; } else if ($xml_obiekt->name == "imie" && $pies_rodzic) { $xml_obiekt->read(); if ($xml_obiekt->nodeType == XMLReader::TEXT) { print $xml_obiekt->value . " "; $pies_rodzic = false; } } } } ?>
Zauważ, że skrypt z listingu 14.20 nie zawiera elementów z przestrzeni nazw ani wywołań XPath, a mimo to jest kompleksowy. Użyteczna funkcja expand() zwróci kopię aktualnego węzła jako obiekt DOMNode. To oznacza, że masz dostęp do przeszukiwania poddrzewa według nazwy znacznika. $poddrzewo = $xml_reader->expand(); $rasy = $subtree->getElementsByTagName('rasa');
Czynność ta ma oczywiście sens dla poddrzew, które nie są bardzo duże. XMLReader i XMLWriter są o wiele bardziej złożone niż rozszerzenia oparte na modelu drzewa — samych typów węzłów mają około dwudziestu. Poziom skomplikowania rozszerzeń XMLReader i XMLWriter w porównaniu z SimpleXML i DOM powoduje, że są one wykorzystywane jedynie w razie konieczności.
Podsumowanie XML jest bardzo użytecznym narzędziem komunikacji i przechowywania danych. Niezależność od języka i platformy powoduje, że jest idealny dla wielu aplikacji. Dokumenty XML mogą być zarówno proste, jak i skomplikowane, z rozbudowanymi schematami i wieloma przestrzeniami nazw. W tym rozdziale pokazaliśmy podstawy XML-a. Następnie omówiliśmy parsowanie i tworzenie dokumentów za pomocą rozszerzenia SimpleXML. Rozszerzenie to sprawia, że praca z dokumentami XML jest łatwa i zarazem daje duże możliwości. Wyjaśniliśmy, jak znaleźć wartości elementów i atrybutów oraz jak radzić sobie z przestrzeniami nazw. SimpleXML jest najlepszym rozwiązaniem w odniesieniu do większości dokumentów. W niektórych przypadkach powinny być jednak stosowane rozszerzenia takie jak DOM i XMLReader. Dla dokumentów XHTML odpowiedni będzie DOM, a dla dużych dokumentów jedyną możliwością mogą być XMLReader i XMLWriter. Znajomość wielu parserów XML zawsze może się przydać.
306
ROZDZIAŁ 15
JSON i Ajax
W ostatnich latach strony WWW próbują naśladować funkcjonalności znane z komputerów osobistych. Użytkowanie stron jest coraz łatwiejsze (chociaż są one coraz bardziej skomplikowane programistycznie). Lepiej reagują one na działania użytkownika i są ładniejsze. Dzięki szybszym odpowiedziom, podpowiedziom, autouzupełnianiu oraz mniejszej liczbie koniecznych przeładowań przeglądanie stron jest bardziej intuicyjne i przyjemniejsze. Wszystko to jest możliwe dzięki żądaniom asynchronicznym wysyłanym przez przeglądarkę do serwera i odbieranym z powrotem. Żądania są asynchroniczne, ponieważ wykonywane są przez osobny wątek, który nie blokuje wykonywania skryptu z głównego wątku. Informacje przekazywane są w obie strony w formie dokumentów JSON (JavaScript Object Notation), XML (eXtensible Markup Language) lub zwykłego tekstu. Te asynchroniczne żądania nie wymagają przeładowania całej strony i są znane pod nazwą Ajax. UWAGA. Termin Ajax został po raz pierwszy użyty w 2005 roku przez Jessego Garretta i oznaczał asynchroniczny JavaScript i XML (ang. Asynchronous JavaScript and XML). Od tamtej pory możliwa jest asynchroniczna komunikacja z innymi językami programowania i formatami danych, takimi jak VBScript i JSON. Ajax może też oznaczać po prostu technikę asynchronicznej komunikacji.
Ajax nie jest jednolitą technologią — łączy w sobie kilka współpracujących narzędzi. Te komponenty to: • Warstwa prezentacji: HTML (HyperText Markup Language) lub XHTML (eXtensible HTML) oraz CSS (Cascading Style Sheets) i DOM (Document Object Model). • Wymiana danych: XML, JSON, HTML lub czysty tekst. • Asynchroniczna komunikacja: obiekt JavaScript XMLHttpRequest. XML i DOM zostały omówione w rozdziale 14. Zakładamy, że użytkownik zna podstawy HTML, CSS i JavaScript. Najpierw przyjrzymy się formatowi JSON i użyjemy go z poziomu PHP. Omówimy obiekt XMLHttpRequest oraz jego wykorzystanie. Pokażemy, w jaki sposób wysłać żądanie Ajax na adres URL i przesłać dane w odpowiedzi. Zademonstrujemy, jak API wyższego poziomu, jQuery, może znacznie ułatwić pracę z Ajaksem. Pod koniec rozdziału zbudujemy przykład łączący wszystkie zdobyte informacje. Będzie to siatka rysowana przy wykorzystaniu tabeli, którą będziemy mogli modyfikować, edytować i zapisywać. Do zmiany koloru tła komórek zastosujemy jQuery, do zapisywania danych w pliku i ponownego ich wczytywania przy kolejnych odwiedzinach strony użyjemy żądania Ajax wraz z PHP.
ROZDZIAŁ 15. JSON I AJAX
JSON Podobnie jak XML, który został omówiony w rozdziale 14., JSON jest po prostu formatem zapisu danych. Udostępnia on siedem typów danych: string, object, array, number, true, false i null. Ciągi znaków muszą znajdować się w cudzysłowach i mogą zawierać znaki ucieczki, takie jak: \n, \t i \". Obiekty JSON są otoczone klamrami i przechowują pary klucz-wartość oddzielone przecinkami. Klucze zawsze są ciągami znaków, natomiast wartość może przyjąć dowolny z siedmiu dostępnych typów — łącznie z obiektami i tablicami. Przykład obiektu JSON wygląda następująco: {"imie":"Brian", "wiek":29}
W tym przykładzie klucz imie przypisany jest do wartości Brian, natomiast klucz wiek do wartości 29. Tablice są otoczone nawiasami kwadratowymi i zawierają wartości oddzielone przecinkami. Przykład tablicy wygląda tak: ["Brian", 29]
Obiekty i tablice JSON mogą być zagnieżdżane. Oto przykład obiektu JSON reprezentującego obraz: { "wymiary": { "szerokosc":800, "wysokosc":600 }, "format":"jpg", "kanal_alfa": false, "nazwa_pliku":"chmury.jpg" }
Wartość dla klucza wymiary jest obiektem — ten zagnieżdżony obiekt zawiera pary klucz-wartość reprezentujące jego szerokość i wysokość. Poniżej pokazano zagnieżdżenie wielu obiektów JSON wewnątrz tablicy: [ { "dimensions": { "width":800, "height":600 }, "format":"jpg", "alpha_channel": false, "filename":"clouds.jpg" }, { "dimensions": { "width":40, "height":40 }, "format":" png", "alpha_channel":true, "filename":"icon.jpg" } ]
Obiekt JSON zawierający tablice reprezentujące oddzielne kanały czerwony, zielony i niebieski (RGB) jednego koloru pokazano poniżej: { "red": [128,128,255,255,255,128,128,0,0], "green": [0, 0, 0, 0, 0, 0, 0,0,0], "blue": [128,128,255,255,255,128,128,0,0] }
Oto te same dane dotyczące kolorów zobrazowane jako trójki zagnieżdżonych tablic:
PHP i JSON Na szczęście tablice w PHP są bardzo podobne do obiektów JSON, a PHP ma wbudowane funkcje do kodowania i dekodowania JSON. Te funkcje to json_encode i json_decode. UWAGA. Jedynym typem danych, który nie może być zakodowany jako JSON, jest resource, czyli np. baza danych lub uchwyt do pliku. Inaczej niż jest w PHP, JSON nie odróżnia liczb całkowitych od zmiennoprzecinkowych. Jedne i drugie reprezentowane są jako liczba.
Funkcje json_encode i json_decode działają tylko na danych zakodowanych w UTF-8. Drugi opcjonalny parametr funkcji json_decode — $assoc — przyjmuje wartość logiczną (domyślnie false). Jeżeli zostanie ustawiony na true, obiekty JSON dekodowane są na tablice asocjacyjne. Podczas rozpoznawania problemów z funkcją json_decode należy pamiętać, że „funkcja zwraca wartość NULL, jeżeli JSON nie będzie mógł być zdekodowany lub zostanie osiągnięty limit poziomu rekurencji”. Opisano to w dokumentacji pod adresem http://www.php.net/manual/en/function.json-decode.php. Trzecią funkcją jest json_last_error — zwraca ona liczbę całkowitą reprezentującą kod błędu. Zwracane błędy to: JSON_ERROR_NONE JSON_ERROR_DEPTH JSON_ERROR_CTRL_CHAR JSON_ERROR_STATE_MISMATCH JSON_ERROR_SYNTAX JSON_ERROR_UTF8
Nie wystąpił żaden błąd Maksymalna wielkość stosu została przekroczona Błędny znak kontroli, prawdopodobnie błędne kodowanie Niepoprawny lub zdeformowany JSON Błąd składni Niepoprawne znaki UTF-8, prawdopodobnie błędne kodowanie
Listing 15.1 przedstawia przykład kodowania różnych typów danych PHP na format JSON i z powrotem na PHP. Listing 15.1. Kodowanie danych PHP na JSON i z powrotem na PHP
Reprezentacja w JSON:
Reprezentacja w PHP:
309
ROZDZIAŁ 15. JSON I AJAX
Wywołanie skryptu spowoduje wyświetlenie: Reprezentacja w JSON: string '[4.1,3,null,true,false,"cześć",{},[]]' (length=47) Reprezentacja w PHP: array 0 => float 4.1 1 => int 3 2 => null 3 => boolean true 4 => boolean false 5 => string 'cześć' (length=7) 6 => object(stdClass)[2] 7 => array empty
Skrypt z listingu 15.2 koduje zagnieżdżoną tablicę PHP zawierającą książki na format JSON, a następnie rozkodowuje z powrotem na PHP. JSON zostanie zakodowany jako tablica obiektów. Listing 15.2. Zagnieżdżona tablica zakodowana na format JSON, a następnie rozkodowana na PHP "Lewis Carroll", "tytuł" => "Alicja w krainie czarów", "rok" => 1865), array("autor" => "Yann Martel", "tytuł" => "Życie Pi", "rok" => 2001), array("autor" =>"Junot Diaz", "tytuł" => "Krótki i niezwykły żywot Oscara Wao", "rok" => 2007), array("autor" => "Joseph Heller", "tytuł" => "Paragraf 22", "rok" => 1961), array("autor" => "Timothy Findley", "tytuł" => "Pielgrzym", "rok" => 1999), array("autor" => "Fiodor Dostojewski", "tytuł" => "Bracia Karamazow", "rok" => 1880), ); $json_ksiazki = json_encode($ksiazki); $zdekodowane_json_ksiazki = json_decode($json_ksiazki); ?>
Skrypt najpierw wyświetla reprezentację tablicy PHP w formacie JSON, która przyjmuje formę tablicy obiektów. Dane będą wyświetlone w jednej linii, przełamania zostały dodane w celu poprawienia czytelności:
310
ROZDZIAŁ 15. JSON I AJAX
string '[ {"autor":"Lewis Carroll","tytuł":"Alicja w krainie czarów","rok":1865}, {"autor":"Yann Martel","tytuł":"Życie Pi","rok":2001}, {"autor":"Junot Diaz","tytuł":"Krótki i niezwykły żywot Oscara Wao","rok":2007}, {"autor":"Joseph Heller","tytuł":"Paragraf 22","rok":1961}, {"autor":"Timothy Findley","tytuł":"Pielgrzym","rok":1999}, {"autor":"Fiodor Dostojewski","tytuł":"Bracia Karamazow","rok":1880} ]' (length=449)
Następnie skrypt z listingu 15.2 wyświetla wersję zdekodowaną na PHP, która także jest reprezentowana jako tablica obiektów: array 0 => object(stdClass)[1] public 'autor' => string public 'tytuł' => string public 'rok' => int 1865 1 => object(stdClass)[2] public 'autor' => string public 'tytuł' => string public 'rok' => int 2001 2 => object(stdClass)[3] public 'autor' => string public 'tytuł' => string public 'rok' => int 2007 3 => object(stdClass)[4] public 'autor' => string public 'tytuł' => string public 'rok' => int 1961 4 => object(stdClass)[5] public 'autor' => string public 'tytuł' => string public 'rok' => int 1999 5 => object(stdClass)[6] public 'autor' => string public 'tytuł' => string public 'rok' => int 1880
'Lewis Carroll' (length=13) 'Alicja w krainie czarów' (length=24)
'Yann Martel' (length=11) 'Życie Pi' (length=9)
'Junot Diaz' (length=10) 'Krótki i niezwykły żywot Oscara Wao' (length=38)
Warto zauważyć, że JSON pomija numeryczne klucze w poszczególnych tablicach książek. Kiedy zmienimy jeden z klucz na asocjacyjny, wszystkie klucze, także te numeryczne, zostaną uwzględnione w obiekcie JSON. Zmodyfikowanie początkowej części listingu 15.2 z: $ksiazki = array( array("autor" => "Lewis Carroll", "tytuł" => "Alicja w krainie czarów", "rok" => 1865),
tak aby przechowywała klucz asocjacyjny, spowoduje wygenerowanie obiektu zawierającego obiekty, zarówno w formacie JSON, jak i w PHP: string '{ "przykładowa_książka": {"autor":"Lewis Carroll","tytuł":"Alicja w krainie czarów","rok":1865}, "0":{"autor":"Yann Martel","tytuł":"Życie Pi","rok":2001}, "1":{"autor":"Junot Diaz","tytuł":"Krótki i niezwykły żywot Oscara Wao","rok":2007}, "2":{"autor":"Joseph Heller","tytuł":"Paragraf 22","rok":1961}, "3":{"autor":"Timothy Findley","tytuł":"Pielgrzym","rok":1999}, "4":{"autor":"Fiodor Dostojewski","tytuł":"Bracia Karamazow","rok":1880} }' (length=505) object(stdClass)[1] public 'przykładowa_książka' object(stdClass)[2] public 'autor' => string public 'tytuł' => string public 'rok' => int 1865 public '0' => object(stdClass)[3] public 'autor' => string public 'tytuł' => string public 'rok' => int 2001 public '1' => object(stdClass)[4] public 'autor' => string public 'tytuł' => string public 'rok' => int 2007 public '2' => object(stdClass)[5] public 'autor' => string public 'tytuł' => string public 'rok' => int 1961 public '3' => object(stdClass)[6] public 'autor' => string public 'tytuł' => string public 'rok' => int 1999 public '4' => object(stdClass)[7] public 'autor' => string public 'tytuł' => string public 'rok' => int 1880
=> 'Lewis Carroll' (length=13) 'Alicja w krainie czarów' (length=24)
'Yann Martel' (length=11) 'Życie Pi' (length=9)
'Junot Diaz' (length=10) 'Krótki i niezwykły żywot Oscara Wao' (length=38)
Ajax Ajax pozwala na częściowe przeładowanie wyrenderowanej zawartością oraz manipulowanie nią bez konieczności przeładowywania całej strony. Żądania Ajax mogą być synchroniczne, ale często są asynchronicznie wykonywane w tle, tak aby przepływ danych nie przeszkadzał w działaniu głównego wątku programu. Jak wspomniano, Ajax nie jest jedną technologią, lecz składa się z kilku współpracujących elementów. Oto kilka wad Ajaksa: • Przycisk przeglądarki powrotu do poprzedniej strony nie śledzi odwołań Ajaksa. • Dynamicznie generowana zawartość nie jest łatwo indeksowalna przez przeglądarki.
312
ROZDZIAŁ 15. JSON I AJAX
• Dla użytkowników niemających dostępu do obsługi JavaScriptu wymagane jest zapewnienie zawartości zastępczej, co wiąże się z dodatkową pracą. • Urządzenia czytające zawartość ekranu mają problemy z dostępem do niej. Responsywność i dynamika Ajaksa z reguły przeważa nad negatywnymi aspektami. Aplikacje takie jak Gmail, Google Docs i Facebook pokazują jego możliwości.
Tradycyjny model WWW W uproszczonym modelu klasycznego WWW (rysunek 15.1) klient przesyła żądania HTTP do serwera, od którego otrzymuje odpowiedź. Jeżeli przeglądarka chce zaktualizować wyświetlane dane, nawet gdy zmianie uległ tylko jeden element
lub obrazek , albo należy zwalidować formularz, to do serwera musi zostać wysłane pełne żądanie. Przy każdym żądaniu przeglądarka czeka na odpowiedź.
Rysunek 15.1. Tradycyjny model WWW 20 lat temu, gdy sieć WWW zaczęła zyskiwać popularność, oczekiwanie 30 sekund lub więcej na zatwierdzenie formularza było akceptowalne. Internet był jeszcze młodą technologią, a połączenia były wolne. Niemniej jednak elektroniczne przesyłanie informacji było o wiele szybsze niż tradycyjne wysyłanie listu pocztą lub osobiste składanie formularza. Gdy ludzie przyzwyczaili się do szybszych łączy i krótszych czasów odpowiedzi, tolerancja dotycząca długiego oczekiwania znacznie się zmniejszyła. Pojawiła się konieczność komunikowania się z serwerem bez przerywania pracy użytkownika.
Model Ajax W modelu Ajax (pokazanym na rysunkach 15.2 i 15.3) występuje pośrednik — w postaci silnika Ajax — umieszczony pomiędzy klientem a serwerem. Klient przesyła zdarzenia do silnika Ajax. Zależnie od rodzaju zdarzenia modyfikowana jest warstwa prezentacji (HTML i CSS) lub przesyłane jest do serwera asynchroniczne zdarzenie. W drugim przypadku serwer kieruje odpowiedź do silnika Ajax, który z kolei modyfikuje klienta. Brak konieczności wykonywania przez klienta bezpośrednich odwołań do serwera pozwala na komunikację bez przeładowań strony, które przerywałyby ciąg działań użytkownika.
313
ROZDZIAŁ 15. JSON I AJAX
Rysunek 15.2. Model Ajax — proste zdarzenie, angażujące jedynie klienta i silnik Ajax Dzięki modelowi Ajax zdarzenia takie jak modyfikowanie wyświetlanych danych czy walidacja formularza mogą się odbywać bez kontaktu z serwerem. Odwołanie do serwera jest wykonywane, kiedy musimy zapisać lub odczytać dane. Na rysunku 15.3 widać, że niektóre zdarzenia, takie jak wyświetlenie zmian, nie wymagają przesłania żądania do serwera lub odbierania od niego danych.
Rysunek 15.3. Model Ajax — bardziej skomplikowane zdarzenie angażujące klienta, silnik Ajax oraz serwer Inne zdarzenia wymagają przesyłania żądań i odpowiedzi pomiędzy serwerem i silnikiem Ajax, jak pokazano na rysunku 15.3.
314
ROZDZIAŁ 15. JSON I AJAX
Zdarzenia synchroniczne kontra asynchroniczne Załóżmy, że mamy trzy zdarzenia — żądania HTTP A, B i C. W modelu synchronicznym przed wysłaniem żądania B musimy poczekać, aż otrzymamy odpowiedź na żądanie A. Następnie musimy poczekać na odpowiedź na żądanie B, zanim będziemy mogli przesłać żądanie C. Zdarzenia są sekwencyjne, więc zdarzenie A blokuje zdarzenia B i C, a zdarzenie B blokuje zdarzenie C (rysunek 15.4).
Rysunek 15.4. Sekwencyjne, synchroniczne zdarzenia HTTP Dzięki zdarzeniom asynchronicznym kolejne żądania nie muszą czekać. Wykonywane są równolegle. Nawet jeżeli zdarzenie A nadal czeka na odpowiedź, nowe zdarzenia B i C mogą przesłać żądania HTTP natychmiast. Jak wynika z porównania rysunków 15.4 i 15.5 — zdarzenia asynchroniczne skracają ogólny czas przetwarzania zdarzeń.
Obiekt XMLHttpRequest Obiekt XMLHttpRequest, określany skrótowo jako XHR, został opracowany przez Microsoft w 2000 roku. Jest to API często implementowane jako JavaScript, pozwalające na przesłanie żądania od klienta do serwera i na odebranie odpowiedzi bez konieczności przeładowywania strony. Nazwy obiektu nie należy rozumieć dosłownie. Poszczególne części są tylko wskazówkami — na przykład: • XML — może oznaczać XML, JSON, HTML lub zwykły tekst, • HTTP — może oznaczać HTTP lub HTTPS, • Request — oznacza żądania i odpowiedzi na nie. Niektóre przeglądarki nie wspierają obiektu XMLHttpRequest, zamiast niego udostępniają obiekt XDomainRequest lub metodę window.createRequest(). W tym rozdziale nie będziemy się przejmować przestarzałymi i niestandardowymi przeglądarkami. Aby utworzyć obiekt XMLHttpRequest, wystarczy jedna linia kodu (listing 15.3). Listing 15.3. Tworzenie obiektu XMLHttpRequest w JavaScripcie
Aby ustawić parametry żądania, wykorzystujemy funkcję open(). Przyjmuje ona następujące parametry: • Rodzaj żądania — przyjmuje jedną z wartości {"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS"}. • URL — adres URL prowadzący do pliku PHP, JavaScript, HTML, tekstowego lub innego. • Asynchroniczność (opcjonalne) — domyślnie wartość true — oznaczająca żądanie nieblokujące innych. • Nazwa użytkownika (opcjonalnie) — jeżeli serwer, do którego wysyłane jest żądanie, wykorzystuje autentykację. • Hasło (opcjonalnie) — jeżeli serwer, do którego wysyłane jest żądanie, wykorzystuje autentykację. Odwołania asynchroniczne udostępniają nasłuchującą funkcję zwrotną — onreadystatechange — dzięki której główny skrypt może kontynuować działanie. Synchroniczne odwołania nie mają funkcji nasłuchującej, muszą więc blokować główny skrypt aż do momentu otrzymania odpowiedzi. Jeżeli przesyłamy asynchroniczne odwołanie, funkcja onreadystatechange ustawi właściwość readyState obiektu żądania. Aby ustawić właściwości obiektu bez wysyłania żądania, wykorzystalibyśmy następujący kod:
Domyślnie nagłówkiem wysyłanym wraz z żądaniem jest application/xml;charset=_zestaw_znakow, gdzie _zestaw_znakow oznacza kodowanie, np. UTF-8. Aby ustawić te wartości, musimy użyć funkcji setRequestHeader(String nazwa_naglowka, String wartość_naglowka). Jeżeli wykorzystywany jest serwer proxy, obiekt automatycznie ustawi i prześle nagłówki Proxy-Authorization. Zanim wyślemy żądanie, musimy zdefiniować funkcję zwrotną. Do tego celu zastosujemy funkcję anonimową (nienazwaną), która będzie sprawdzała, czy właściwość readyState przyjęła wartość 4 — oznacza ona zakończone żądanie: xhr.onreadystatechange=function(){ if (xhr.readyState == 4){ //stan = 4 oznacza zakończone żądanie if (xhr.status==200){ //sukces
316
ROZDZIAŁ 15. JSON I AJAX
Możliwe wartości właściwości readyState to: 0 Uninitialized — niezainicjowany; funkcja open() nie została jeszcze wywołana. 1 Loading — ładowanie; funkcja send() nie została jeszcze wywołana. 2 Loaded — załadowano; funkcja send() została wywołana, nagłówki i status są dostępne. 3 Interactive — pobieranie; właściwość responseText przechowuje niekompletne dane. 4 Completed — zakończono wszystkie operacje. Stany od 0 do 3 są niespójne pomiędzy przeglądarkami. Głównie interesuje nas stan 4. Teraz, kiedy obiekt jest już zainicjalizowany, a funkcja zwrotna zdefiniowana, możemy przesłać żądanie: xhr.send("nasza zawartość");
Wysłanie żądania Ajax za pomocą obiektu XMLHttpRequest wygląda jak na listingu 15.4. Listing 15.4. Prosty przykład wykorzystania obiektu XMLHttpRequest
Wykorzystanie obiektu XMLHttpRequest W pierwszym przykładzie zastosowania obiektu XMLHttpRequest (listing 15.5) zamienimy zawartość znacznika
. Listing 15.5. Modyfikacja elementu na stronie przy wykorzystaniu obiektu XMLHttpRequest
Oryginalna zawartość
Adresem URL, do którego odwołujemy się na listingu 15.5, jest aktualna strona, dostępna w zmiennej JavaScriptu window.location.pathname. W żądaniu nie przysyłamy żadnych danych — xhr.send(null). Skrypt JavaScript umieszczony jest po elemencie HTML, na którym wykonujemy operacje — aby JavaScript mógł znaleźć interesujący nas element, całe drzewo DOM musi być wczytane. Frameworki wyższego poziomu, takie jak jQuery, udostępniają funkcję sprawdzające, czy dokument został już załadowany — dzięki temu skrypt może być umieszczony w dowolnym miejscu na stronie. Zależnie od czasu odpowiedzi Twojego komputera możesz zaobserwować zmianę zawartości elementu ze stanu początkowego — Oryginalna zawartość — na stan końcowy — Ajax załadował zawartość. Skrypt z listingu 15.6 pobierze tekst z zewnętrznego pliku XML (zawartość pliku przedstawiono na listingu 15.7) i wstawi go w naszym dokumencie po załadowaniu strony. Załadowana będzie tylko wartość elementu XML — nazwa i atrybuty elementu zostaną pominięte. Listing 15.6. Pobranie zawartości pliku XML przy wykorzystaniu obiektu XMLHttpRequest i wyświetlenie jej jako zwykłego tekstu Przykład XHR
Wynik działania skryptu z listingu 15.6 pokazany jest na rysunku 15.6.
Rysunek 15.6. Wynik działania skryptu z listingu 15.6, wykorzystującego Ajax do odczytania tekstu z pliku XML
319
ROZDZIAŁ 15. JSON I AJAX
Najważniejszą linią na listingu 15.6 jest ta, w której przypisujemy tekst wynikowy w przypadku pomyślnego pobrania pliku: if (xhr.status==200){ //sukces //pobranie rezultatu jako zwykłego tekstu wiadomosc = "
" + xhr.responseText + "
"; }
i wstawienie go jako innerHTML elementu
o id równym wygenerowana_zawartosc: document.getElementById("wygenerowana_zawartosc").innerHTML = wiadomosc;
Pobieramy zawartość pliku jako XML i parsujemy wartości odpowiednich elementów, tak aby otrzymać tylko imiona zwierząt (listing 15.8). Listing 15.8. Pobranie zawartości dokumentu XML XMLHttpRequest oraz odpowiednich wartości Przykład XHR
Ajax pobrał tekst:
Na listingu 15.8 wykorzystujemy JavaScript, aby pobrać dane XML zwrócone przez żądanie Ajax — xhr.responseXML — następnie parsujemy je, wyszukując wartości elementów . Wynik pokazany został na rysunku 15.7.
Rysunek 15.7. Wynik działania skryptu z listingu 15.8, wykorzystującego Ajax do parsowania XML-a Jeżeli odwołamy się do pliku zapisanego jako HTML, wykorzystanie responseText zachowuje strukturę HTML (listing 15.9). Wynik działania skryptu pokazany został na rysunku 15.8. Listing 15.9. Pobranie HTML-u za pomocą obiektu XMLHttpRequest Przykład XHR — zwykły tekst zawierający HTML
Ajax pobrał tekst zawierający HTML:
321
ROZDZIAŁ 15. JSON I AJAX
Zawartość pliku sample_table.html jest następująca:
testowa
kolumna
a
1
b
2
c
3
Rysunek 15.8. Wynik działania skryptu 15.9, wykorzystującego Ajax do pobrania zawartości HTML
API JavaScript wyższego poziomu API wyższego poziomu, takie jak jQuery, Prototype i YUI, zyskały ostatnio dużą popularność — częściowo dlatego, że ukrywają część szczegółów implementacji i sprawiają, że wykorzystanie skomplikowanych obiektów — takich jak XMLHttpRequest — staje się łatwiejsze. Oznacza to, że użytkownik biblioteki nie musi znać szczegółów bezpośredniego użycia obiektu XMLHttpRequest. Obiekt XMLHttpRequest jest jednak kluczowy w zrozumieniu tego, co się dzieje „pod maską”. Inne zalety tych bibliotek to znacznie łatwiejsza obsługa drzewa DOM i lepsza kompatybilność między przeglądarkami. Możemy wybierać z kilku bibliotek. Danchilla jest zwolennikiem biblioteki jQuery, która jest bez wątpienia najczęściej używaną biblioteką JavaScript. Jest ona wykorzystywana przez Google, Amazon, Twitter, wewnątrz Microsoft Visual Studio, przez IBM, w Drupal CMS (system zarządzania treścią), a także przez wiele innych stron i bibliotek — zobacz http://docs.jquery.com/Sites_Using_jQuery. Jeżeli nie spodoba Ci się jQuery, do dyspozycji masz jeszcze Dojo, YUI, Prototype, MooTools oraz script.aculo.us. Omówienie szczegółów powyższych API wykracza poza zakres tej książki. Wyjaśnimy jednak znaczenie wszystkich funkcji wykorzystywanych w przykładach.
Przykłady jQuery W skrypcie z listingu 15.10 zawartość elementu
po załadowaniu strony modyfikowana jest przy wykorzystaniu jQuery — skrypt jest równoważny z listingiem 15.5. Listing 15.10. Modyfikacja elementu
po załadowaniu strony za pomocą jQuery
Pierwszy przykład jQuery
Oryginalna zawartość
Na listingu 15.10 linia:
ładuje bibliotekę jQuery z sieci CDN Google (Content Delivery Network). Można także podłączyć lokalną kopię biblioteki. W systemach produkcyjnych CDN-y są z reguły szybsze i bardziej niezawodne. Większość przeglądarek ma ograniczoną liczbę plików, które mogą być jednocześnie pobierane z domeny. Wykorzystanie zewnętrznego CDN-u usuwa jeden plik z kolejki ładowania strony. Zwiększa to wydajność i skraca czas ładowania strony. Zwróć uwagę na nazwę pliku — jquery.min.js — jest to wersja produkcyjna. W środowisku testowym podczas wyszukiwania błędów w kodzie lepiej wykorzystać wersję czytelną dla człowieka — jquery.js. Wywołanie funkcji $(document).ready jest standardowe dla skryptów jQuery. Zmienna $(document) reprezentuje cały dokument DOM i w dalszej części skryptu została skrócona do $(). Odwołanie do .ready powoduje wykonanie skryptu po załadowaniu dokumentu DOM. Dzięki temu możemy umieścić skrypt przed dokumentem, do którego odwołujemy się w skrypcie. Parametry Ajaksa są inicjalizowane i ustawiane za pomocą wywołania $.ajax(). Funkcja jako parametry przyjmuje sposób wykonania żądania (GET lub POST), adres URL oraz typ odpowiedzi. Definiuje także funkcje zwrotne wywoływane w przypadku sukcesu i niepowodzenia. Linia document.getElementsByTagName("p")[0].innerHTML z oryginalnego skryptu została zastąpiona linią $("p").html("jakieś dane"). Pierwsza część polecenia znajduje odpowiedni element
za pomocą selektorów CSS, druga ustawia zawartość elementu. UWAGA. Zapis $("p") odpowiada wszystkim elementom
w dokumencie. Jeżeli chcemy wybrać tylko pierwszą instancję elementu, tak jak na listingu 15.5, moglibyśmy zastosować funkcje wbudowane $("p").first(). Moglibyśmy także użyć selektorów CSS, np.: $("p:first") lub $("p:eq(0)").
Wersja skryptu używająca jQuery jest krótsza niż wersja oryginalna, wykorzystująca obiekt XMLHttpRequest. W przypadku bardziej skomplikowanych skryptów wartość API wyższego poziomu, takich jak jQuery, uwydatnia się w jeszcze większym stopniu. Skrypt z listingu 15.11 jest równoważny ze skryptem z listingu 15.6, ładującym tekst z pliku XML. Listing 15.11. Wykorzystanie jQuery do załadowania tekstu z pliku XML
Ładowanie tekstu z jQuery
Ajax pobrał tekst:
Gdybyśmy nie troszczyli się o potencjalne błędy, moglibyśmy przepisać kod z listingu 15.11 zwięźlej (listing 15.12). Listing 15.12. Zwięzła wersja kodu ładującego dane z pliku XML Ładowanie tekstu z jQuery
Ajax pobrał tekst:
324
ROZDZIAŁ 15. JSON I AJAX
Funkcja jQuery load()na listingu 15.12 wykorzystuje żądanie GET oraz „inteligentne zgadywanie” do zwrócenia tekstu. Następnie wstawia tekst do wybranego elementu. Funkcja jQuery wrap otacza zawartość elementu znacznikami. To pozwala nam na otoczenie załadowanego XML-a znacznikami
..
. Oprócz funkcji $.ajax, jQuery udostępnia funkcje $.get i $.post, pozwalające na wysłanie żądań GET i POST. Dzięki tym funkcjom jQuery próbuje odgadnąć odpowiedni wynik. Jeżeli odgaduje niepoprawnie, typ wyniku możemy określić sami. Dokładniejsze informacje możesz znaleźć w dokumentacji jQuery dostępnej pod adresem http://api.jquery.com/jQuery.get/. Zobacz listing 15.13. Listing 15.13. Wykorzystanie funkcji jQuery $.get i pobranie danych XML Ładowanie XML-a z jQuery
Ajax pobrał text:
Na listingu 15.13 funkcja $.get przyjmuje trzy parametry. Pierwszy z nich to żądany plik, drugi to funkcja zwrotna, w której przetwarzane są uzyskane dane, a trzeci to oczekiwany typ danych. Bez określenia xml jQuery wybrałby zwykły tekst. Do tej pory pokazaliśmy, jak wykorzystać obiekt XMLHttpRequest oraz w jaki sposób API wyższego poziomu, takie jak jQuery, ukrywają szczegóły implementacji i ułatwiają nam pracę. Teraz pokażemy przykład zastosowania formatu JSON (listing 15.14). Listing 15.14. Wyświetlenie danych JSON z tabeli PHP — json_example.php array("goryl", "żyrafa", "słoń"), "azja" => array("panda"), "północna ameryka" => array("niedźwiedź", "ryś", "orka"), ); print json_encode($zwierzeta); ?>
325
ROZDZIAŁ 15. JSON I AJAX
Skrypt z listingu 15.15 wykorzystuje jQuery do uzyskania wartości JSON z pliku PHP (listing 15.14) za pomocą żądania Ajax. Listing 15.15. Wykorzystanie $.getJSON i $.each Ładowanie danych JSON z jQuery
JSON sparsowany przez Ajax:
Wynik działania skryptu z listingu 15.15 znajduje się poniżej: JSON sparsowany przez Ajax: afryka goryl, żyrafa, słoń azja panda północna ameryka niedźwiedź, ryś, orka
Na listingu 15.15 wykorzystaliśmy funkcję skrótową $.getJSON. Mogliśmy także zastosować $.get z opcją json jako trzecim argumentem. Użyliśmy również funkcji $.each, aby przeiterować po zwróconych obiektach JSON. Aby przypisać wartości klucz-wartość do zmiennych kontynent i zwierzęta, zdefiniowaliśmy funkcję zwrotną następująco: $.each(dane, function(kontynent, zwierzeta){
326
ROZDZIAŁ 15. JSON I AJAX
Przesyłanie danych z Ajaksa do skryptu PHP W kolejnym przykładzie (listing 15.16) na stronie znajdują się dwa przyciski — Myśliwy i Ofiara. Po kliknięciu dowolnego z nich do skryptu PHP zostanie przesłane żądanie Ajax zawierające parametr ?typ=mysliwy lub ?typ=ofiara. Kiedy skrypt PHP otrzyma żądanie, wykorzysta przekazaną wartość do zwrócenia odpowiedniego zwierzęcia w formacie JSON. Listing 15.16. Plik PHP wybierający i wysyłający odpowiednie zwierzę w formacie JSON — predator_prey.php
Na listingu 15.17 obsługujemy zdarzenie .click obydwóch przycisków poprzez wywołanie żądania Ajax .load. Plik predator_prey.php odbiera żądanie wraz z parametrem typ, po czym odsyła odpowiedź, którą wstawiamy do dokumentu. Wykorzystaliśmy funkcję array_rand do wygenerowania losowego indeksu wybranej tablicy, a następnie funkcję json_encode, aby zakodować wynik w formacie JSON. Listing 15.17. Plik HTML ładujący wynik żądania Ajax Przykład Myśliwy/Ofiara
Odpowiedź z PHP:
Wynik działania skryptu z listingu 15.17 pokazany jest na rysunku 15.9.
Rysunek 15.9. Wynik działania skryptu z listingu 15.17
Prosty program graficzny Na listingu 15.18 zbudujemy prostą aplikację pozwalającą na rysowanie siatki komórek tabeli HTML i jQuery przy wykorzystaniu palety kolorów. Następnie dodamy możliwość zapisywania i wczytywania rysunku przy zastosowaniu PHP i Ajaksa. Listing 15.18. Aplikacja pozwalająca manipulować kolorami tła komórek tabeli Przykład z rysowaniem po siatce
Paleta kolorów:
Rysuj!
Konsola debugowania:
329
ROZDZIAŁ 15. JSON I AJAX
Kod CSS na listingu 15.18 zawiera polecenie margin-collapse: collapse dla tabeli z siatką — dzięki temu wewnętrzne obramowanie ma taką samą grubość jak zewnętrzne. Tworzymy paletę kolorów jako tabelę HTML. Mimo że podajemy wymiary, znak pomaga upewnić się, że przeglądarka narysuje obramowania komórek. Bez modyfikacji DOM nasza siatka jest pusta. W funkcji jQuery .ready wykorzystujemy pętlę do dodania dziesięciu kolumn i dziesięciu wierszy do tabeli. Następnie definiujemy akcję wykonywaną po kliknięciu komórki palety: $("#paleta td").each( function( index ){ //przypisanie zdarzenia onClick $( this ).bind ( click", function(){
W ciele funkcji zmieniamy zmienną aktywny_kolor oraz wskazujemy jej wartość w sekcji debugowania: function(){ aktywny_kolor = $(this).css("background-color"); $("#debug_kolor_palety").html("aktywny kolor palety to: " + "" + aktywny_kolor + ""); }
Do komórek siatki przypisujemy zdarzenie, tak aby po każdym kliknięciu wartość właściwości background-color została zmieniona na zgodną z wartością zapisaną w zmiennej aktywny_kolor: $("#siatka td").each( function( index ){ //przypisanie zdarzenia onClick $( this ).bind ( "click", function(){ $(this).css("background-color", aktywny_kolor); } );
Wynik pokazany został na rysunku 15.10. Nasz program działa, nie możemy jednak zapisać utworzonego obrazu. Jeśli wyjdziemy ze strony i do niej powrócimy, zawsze otrzymamy czystą siatkę. Zajmiemy się teraz tym problemem. UWAGA. Kolory tła w jQuery są zapisywane w nowszej formie, rgb(255, 0, 0), a nie w wartościach szesnastkowych, np. #ff0000. Nowy format jest częścią specyfikacji CSS3 i zawiera także kanał alfa — rgba. Wartości alfa pozwalają na łatwe regulowanie przezroczystości i niedługo będą wspierane przez większość przeglądarek.
Utrzymanie stanu Aby zapisać zmiany wprowadzone za pośrednictwem Ajaksa, możemy dzięki PHP zapisać dane w bazie, zmiennej $_SESSION lub pliku. Kiedy przeładujemy stronę, będziemy mogli ustawić kolory siatki zgodnie z zapisanymi danymi. W naszym przykładzie wykorzystamy plik fizyczny. Możesz rozszerzyć przykład tak, aby zapisywał dane dla unikalnej sesji lub nazwy użytkownika, my jednak przechowamy tylko jeden zestaw danych.
330
ROZDZIAŁ 15. JSON I AJAX
Rysunek 15.10. Siatka rysowania z listingu 15.18 Nie chcemy zapisywać rezultatu po każdej zmianie piksela — byłoby to bardzo wolne i pochłaniałoby wiele zasobów. Zamiast tego dodamy przycisk pozwalający na zapisanie danych w dowolnym momencie. Można także śledzić liczbę zmian pomiędzy poszczególnymi zapisami. Dzięki temu moglibyśmy utworzyć mechanizm automatycznego zapisu co każde 100 zmian pracujący w tle. To pomogłoby w zabezpieczeniu danych użytkownika bez konieczności wykonywania przez niego zapisów. Możemy również dodać przycisk czyszczący siatkę do stanu sprzed modyfikacji oraz usuwający zapisane dane (listing 15.19). Na listingach 15.20 i 15.21 zostały przedstawione odpowiednio: skrypt zapisujący odebrane dane do pliku XML i skrypt odczytujący zapisane dane. Listing 15.19. Plik HTML wyświetlający siatkę i wykonujący odwołania Ajax do skryptu PHP Przykład z rysowaniem po siatce
Paleta kolorów:
Rysuj!
Konsola debugowania:
Listing 15.20. Skrypt PHP zapisujący przekazaną zmienną $_POST, zawierającą dane w formacie JSON — save_drawing.php
333
ROZDZIAŁ 15. JSON I AJAX
Listing 15.21. Skrypt PHP ładujący zapisane dane — load_drawing.php
Nasz nowy skrypt wyposażyliśmy w funkcję zapisu po kliknięciu przycisku Zapisz. Aby było to możliwe, został utworzony nowy obiekt JavaScriptu. Następnie dla każdej z komórek dodaliśmy obiekt zawierający wartość koloru tła. Po tej operacji do pliku save_drawing.php zostało wysłane żądanie POST. Musimy wykorzystać metodę POST, ponieważ przesyłane dane są zbyt długie dla metody GET. Wewnątrz skryptu PHP kodujemy zmienną $_POST na format JSON i zapisujemy w pliku (listing 15.22). Listing 15.22. Funkcja zapisująca naszego programu $("#zapisz").click(function(){ var koloryJson = new Object(); var i=0; $("#siatka td").each(function() { koloryJson[i] = $(this).css("background-color"); ++i; }); $.ajax( { type: "post", url: "save_drawing.php", dataType: "text", data: koloryJson, success: function(data) { $("#debug_wiadomosc").html("rysunek zapisany"); }, failure: function(){ $("#debug_wiadomosc").html( "Podczas próby zapisania rysunku nastąpił błąd"); } }); });
Nasz obrazek jest już zapisany, możemy zatem załadować dane przy powtórnej wizycie. Aby to zrobić, wysyłamy żądanie $.getJSON do pliku load_drawing.php. Zwracana jest zawartość w formacie JSON zapisana przez nas w pliku. Wewnątrz jQuery iterujemy po wszystkich komórkach siatki i przypisujemy im odpowiednie kolory (listing 15.23). Wynik jest pokazany na rysunku 15.11. Listing 15.23. Funkcja ładująca naszego programu $.getJSON("load_drawing.php", function(dane){ $("#siatka td").each(function(indeks){ $(this).css("background-color", dane[indeks]); }); });
Podczas pracy z Ajaksem warto korzystać z narzędzi pomagających w procesie debugowania. Rozszerzenie Firebug dla przeglądarki Firefox jest jednym z najlepszych narzędzi. W Firebugu dane Ajax są dostępne w zakładce Sieć/XHR. Bardzo użyteczne są również narzędzia przeglądarki Chrome.
334
ROZDZIAŁ 15. JSON I AJAX
Rysunek 15.11. Nasz program z załadowanymi danymi, przyciskiem zapisu oraz podglądem w programie Firebug
Podsumowanie W tym rozdziale wyjaśniliśmy, w jaki sposób żądania asynchroniczne pozwalają budować bogate, użyteczne i przyjemne strony. Jest to możliwe dzięki wstawieniu warstwy pośredniej — silnika Ajax — pomiędzy klientem a serwerem. Serwer otrzymuje mniej żądań związanych z aktualizacją warstwy prezentacji i wykonywane są odwołania nieblokujące strony podczas przesyłania danych. Najpopularniejszym językiem skryptowym pozwalającym na wysyłanie żądań Ajax jest JavaScript. Wykorzystanie API wyższego poziomu, takiego jak jQuery, może znacznie ułatwić i uprzyjemnić pracę z Ajaksem w porównaniu z bezpośrednią pracą z obiektem XMLHttpRequest. Formaty danych, które mogą być zastosowane podczas pracy z Ajaksem, to XML (omówiony w rozdziale 14.), JSON (omówiony w tym rozdziale), HTML i zwykły tekst. Ajax w tworzeniu współczesnych stron internetowych jest mieczem obosiecznym. Z jednej strony wprowadza możliwości interakcji ze stroną i przesyłania danych niedostępne w modelu klasycznym. Z drugiej jednak użytkownicy oczekują bogatych doznań. Aby sprostać tym oczekiwaniom, potrzeba wiele pracy. Aby zapewnić użytkownikowi dobre doznania (ang. User Experience — UX) z Ajaksem, twórca musi poznać kilka technologii; najważniejsze z nich to JavaScript, selektory DOM, JSON i XML. Powstają poza tym nowe techniki, takie jak odwrotny Ajax, związany z długotrwałymi połączeniami HTTP i przesyłaniem danych z serwera do klienta. Rola, jaką odgrywa Ajax w procesie tworzenia stron, będzie w przyszłości jeszcze większa.
335
ROZDZIAŁ 15. JSON I AJAX
336
ROZDZIAŁ 16
Konkluzja
Mamy nadzieję, że z przyjemnością czytałeś tę książkę oraz że zrobiłeś dobry użytek z informacji i linków zamieszczonych w każdym z rozdziałów. Bardzo się staraliśmy, aby publikacja ta była dla Ciebie przydatna, i mamy nadzieję, że będziesz do niej często zaglądał, że znajdzie się na Twoim biurku obok komputera, a nie na półce razem z innymi książkami programistycznymi. Oczywiście rozumiemy, że branża IT rozwija się z prędkością światła i że za kilka miesięcy wiele zawartych tu informacji będzie nieaktualnych. W związku z tym dodaliśmy ten rozdział — aby spróbować odpowiedzieć na niezadane jeszcze pytania związane z tworzeniem aplikacji internetowych. Jest to zbiór najlepszych znanych nam źródeł informacji dotyczących programowania, stanowiących świetny dodatkowy materiał, z którym możesz zapoznawać się podczas swojej ciągłej edukacji programistycznej — w szczególności jeżeli chodzi o programowanie PHP.
Zasoby Najpierw chcielibyśmy zapoznać Cię z szeroką gamą zasobów dostępnych w internecie. Kolejne podrozdziały wyjaśniają, co możesz znaleźć na podanych stronach.
www.php.net Tutaj znajdziesz dostępne do pobrania najświeższe wersje PHP, a także pełną dokumentację zawierającą przykłady, w tym przykłady i wyjaśnienia dodawane przez użytkowników. Dokumentację można łatwo przeszukiwać; jeżeli nie znajdziesz potrzebnych informacji pod danym hasłem, sugerowane są jego alternatywy. Poza wspomnianymi materiałami znajdziesz wiadomości o nadchodzących wydarzeniach związanych z PHP, takich jak konferencje, spotkania grup użytkowników, i oczywiście najnowsze linki, które według administratorów strony mogą być przydatne (rysunek 16.1). UWAGA. Jeżeli szukasz na stronie informacji dotyczących kodu, spróbuj wyszukiwania poprzez modyfikację adresu URL — dodaj nazwę funkcji na końcu adresu, np.:
php.net/date W tym przypadku otworzy się strona dokumentacji dotycząca daty.
ROZDZIAŁ 16. KONKLUZJA
Rysunek 16.1. www.php.net
www.zend.com Prawdopodobnie dużo czasu spędzisz na stronie Zend Corporation. Jest to samozwańcza „firma PHP”. Znajdziesz tutaj wiele narzędzi, jakie Zend opracował, aby pomóc zarówno w tworzeniu aplikacji PHP, jak i uruchamianiu ich na serwerze. Spoglądając na pełną listę produktów, zauważysz, że firma przyczynia się do rozwoju samego języka PHP. Poza produktami komercyjnymi oferowane są tu inne wartościowe narzędzia i materiały (rysunek 16.2).
devzone.zend.pl Jest to strona siostrzana portalu Zend. Znajdziesz tutaj społeczność programistów chcących pomagać sobie nawzajem w sprawach związanych z PHP i użytkowaniem produktów firmy Zend. Są tu zamieszczane recenzje książek i reportaże z konferencji. Gdy naprawdę utkniesz w martwym punkcie, tutaj będziesz mógł porozmawiać z ekspertami. Każdy temat ma odrębne forum, podcasty, samouczki, artykuły, a nawet ścieżki do dokumentacji PHP (rysunek 16.3).
www.phparch.com Kolejną polecaną stroną jest magazyn „php|architect”. Pod powyższym adresem możesz obejrzeć bezpłatny numer — jeżeli Ci się spodoba, za niewielką opłatą możesz zaprenumerować wersję PDF. W magazynie tym są publikowane bardzo wartościowe informacje przeznaczone dla programistów średnio zaawansowanych i zaawansowanych; omawiane tematy są z reguły związane z nowinkami technicznymi.
338
ROZDZIAŁ 16. KONKLUZJA
Rysunek 16.2. www.zend.com
Rysunek 16.3. devzone.zend.pl
Konferencje W przyswajaniu wiedzy i umiejętności w zakresie PHP nie ma nic lepszego niż uczestnictwo w konferencji. Dzięki temu możesz się oderwać od codziennego stresu i skupić się na spotykaniu i poznawaniu innych programistów PHP, dyskutowaniu z nimi o życiu, wszechświecie i wszystkim innym. Społecznościowy aspekt tych spotkań jest równie ważny. Możesz wierzyć lub nie, ale na tego typu spotkaniach wymieniane są ogromne ilości informacji — nawet gdy pod koniec dnia popijasz z innymi uczestnikami spotkania napój w hotelowym barze. Naturalnie najważniejszym elementem konferencji jest prezentacja, jednak korzyści z pobocznych rozmów są niezaprzeczalne.
339
ROZDZIAŁ 16. KONKLUZJA
Jeżeli masz duże doświadczenie w danym obszarze, możesz podzielić się nim podczas prelekcji. Gdy wystąpisz jako mówca, wkroczysz w inny wymiar życia konferencyjnego, którego niewiele osób ma szanse doświadczyć — poznasz znaczące osobistości świata PHP i zyskasz bezcenne kontakty z wartościowymi ludźmi. Oczywiście będziesz uważany za jednego z nich, ponieważ stałeś się prelegentem na konferencji i ekspertem w danej dziedzinie. Oto lista polecanych konferencji, w których powinieneś uczestniczyć, jeżeli tylko będziesz miał okazję (przedstawiamy je tu od najciekawszych, w razie gdybyś miał ograniczone fundusze): • ZendCon. Główna światowa konferencja PHP odbywająca się co roku, przeważnie w listopadzie. Z pewnością zobaczysz tutaj wielkie osobistości świata PHP i nawiążesz wartościowe kontakty. Właśnie na tej konferencji ogłasza się większość komunikatów i premier. Jeśli więc chcesz być jednym z pierwszych poinformowanych, postaraj się w niej uczestniczyć. • OSCON. Konferencja firmy O’Reilly dotycząca tworzenia aplikacji internetowych i oprogramowania open source. Są to dyskusje o tematyce znacznie szerszej niż PHP, jednak PHP zdecydowanie ma tutaj swoje miejsce. OSCON z reguły odbywa się w lipcu. • ConFoo. Jest to duża kanadyjska konferencja, wcześniej znana pod nazwą PHP Quebec, dotycząca nie tylko tematów związanych z PHP, jednak jej korzenie tkwią w obszarze związanym z tym językiem. • International PHP Conference. Zawsze odbywa się w Niemczech dwa razy do roku — wiosną i jesienią. Wiosenna konferencja przeważnie ma miejsce w Berlinie, natomiast jesienna organizowana jest w październiku w różnych miastach Niemiec. • Open Source India. Trzydniowa konferencja odbywająca się co roku, jesienią. Dotyczy oprogramowania open source, więc jak w przypadku OSCON, porusza się na niej tematy związane nie tylko z PHP. Jest to jedna z największych tego typu konferencji w Azji. Jeżeli chcesz nawiązać jakieś kontakty biznesowe w tej części świata, powinieneś postarać się wziąć udział w tym spotkaniu. UWAGA. Nie zapomnij zaglądać do sekcji dotyczącej konferencji na stronie php.net, ponieważ ciągle organizowane są nowe konferencje.
Certyfikacja PHP Ostatnim tematem tego rozdziału jest wartość (lub domniemana wartość) certyfikatów PHP i to, jak trzeba się przygotować do testu. Na ten temat bardzo dużo się dyskutuje. Certyfikaty dostępne są od wersji 4.0 PHP (czerwiec 2004). Warto zauważyć, że trzech z czterech autorów tej książki posiada certyfikaty PHP. To może być dla Ciebie wskazówka dotycząca wartości certyfikacji. Zobaczmy, z czym się to wiąże. Egzamin organizowany jest przez Zend Corporation. Zend prosi ekspertów PHP z całego świata, aby zajęli miejsce w komisji przygotowującej pytania egzaminacyjne i odpowiedzi. Ma to przynajmniej dwie następujące zalety: 1. Test przygotowywany jest przez kilka grup lub firm, więc obejmuje zróżnicowane zagadnienia. 2. Dzięki temu, że test jest przygotowywany przez różnych ekspertów, nie będzie dotyczył wąskiej dziedziny PHP. Obecnie egzamin bazuje na wersji PHP 5.3 i jest uważany za nieznacznie trudniejszy niż ten z wersji 4.0. Liczba poprawnych odpowiedzi koniecznych do zdania egzaminu nie jest znana, jednak przypuszcza się, że kandydat musi uzyskać ich 60%. UWAGA. Czasami test jest oferowany bezpłatnie na niektórych konferencjach, między innymi na konferencji ZendCon. Kurs przygotowujący do egzaminu dostępny jest na tej samej konferencji. Jest to świetna okazja, aby bezpłatnie przystąpić do egzaminu.
340
ROZDZIAŁ 16. KONKLUZJA
Test składa się z 70 losowo wybranych pytań, na które musisz udzielić odpowiedzi w ciągu 90 minut. Jest dwanaście różnych obszarów tematycznych, z których losowane są pytania — przystępując do testu, powinieneś doskonale znać każdy z nich. Dodatkowo powinieneś mieć od półtora roku do dwóch lat styczności z PHP w codziennej pracy, aby dysponować wystarczającym doświadczeniem praktycznym. Jeżeli zdasz egzamin, otrzymasz tytuł ZCE (Zend Certified Engineer) oraz podpisany certyfikat, gotowy do oprawienia w ramkę; będziesz mógł także oficjalnie posługiwać się tytułem ZCE. UWAGA. Na stronach www.zend.com i www.phparch.com dostępne są podręczniki i testy przygotowujące do egzaminu. Jeśli potrzebujesz dodatkowych materiałów do nauki, warto tam zajrzeć.
Oto obszary tematyczne testu: • Podstawy PHP • Funkcje • Tablice • Programowanie zorientowane obiektowo (OOP) • Ciągi znaków i wyrażenia regularne • Projektowanie i teoria • Funkcje internetowe • Różnice pomiędzy PHP4 i PHP5 • Pliki, strumienie, sieci • XML i usługi sieciowe • Bazy danych • Bezpieczeństwo Czy warto się certyfikować? Absolutnie tak! Tytuł ZCE gwarantuje pewien poziom znajomości PHP i można znaleźć wiele dobrych ofert pracy, w których jest on wymagany. Dysponując certyfikatem, będziesz miał więcej pewności siebie, przydatnej podczas rozmowy z szefem na temat Twoich kompetencji i wyników.
Podsumowanie W tym rozdziale przedstawiliśmy wybrane zasoby i materiały dostępne w sieci, wykraczające poza zakres tej książki. Rozpatrzyliśmy także zasadność ubiegania się o certyfikat PHP. Mamy nadzieję, że będziesz kontynuował naukę i nadal rozwijał swoją wiedzę na temat tego języka.
341
ROZDZIAŁ 16. KONKLUZJA
342
DODATEK
Wyrażenia regularne
Dodatek ten zapozna Cię z wyrażeniami regularnymi. Jest to jednak tylko wprowadzenie — tematyka ta jest w istocie bardzo obszerna i traktują o niej całe książki. Dobrym przykładem jest Regular Expression Recipes Nathana A. Gooda (Apress 2004). Wyrażenia regularne (regex) są metodą dopasowywania ciągów znaków odpowiadających zadanym kryteriom. Ich koncepcja wywodzi się z teorii komputerów i oparta jest na automatach skończonych. Istnieje wiele wariacji wyrażeń regularnych różniących się od siebie w licznych aspektach. Dwa najczęściej wykorzystywane silniki wyrażeń regularnych to Posix oraz PCRE (Perl compatible regular expressions — wyrażenia regularne kompatybilne z Perl). W PHP zastosowano ten drugi silnik. Właściwie możesz wykorzystywać oba, jednak Posix został wycofany w wersji PHP 5.3 i późniejszych. Poniżej wymieniono podstawowe funkcje PHP implementujące silnik PCRE: • • • •
preg_match preg_replace preg_split preg_grep
Istnieją także inne funkcje, będące częścią „maszynerii” wyrażeń regularnych, jednak te cztery są najczęściej stosowane. Prefiks preg w nazwie każdej z nich oznacza Perl regular expression (wyrażenie regularne Perl) w przeciwieństwie do wyrażeń Posix nazywanych też extended regular expressions (rozszerzone wyrażenia regularne). Tak, były wersje funkcji rozpoczynające się od prefiksu ereg, jednak zostały wycofane w PHP 5.3. Dodatek składa się z dwóch części — pierwsza omawia składnię wyrażeń regularnych, druga przedstawia przykłady ich wykorzystania w skryptach PHP.
Składnia wyrażeń regularnych Podstawowym składnikiem wyrażeń regularnych są metaznaki. Metaznaki poprzedzone znakiem lewego ukośnika (\) tracą swoje specjalne znaczenie. Tabela A.1 przedstawia listę metaznaków. Poza metaznakami są dostępne klasy znaków specjalnych przedstawione w tabeli A.2. Wyrażenie .* odpowiada dowolnemu znakowi. Wyrażenie ^.*3 odpowiada znakom od początku ciągu do ostatniej cyfry 3 w linii. To zachowanie może być zmienione; omówimy to w dalszej części dodatku, dotyczącej chciwości wyrażeń. Na razie zobaczmy więcej przykładów wyrażeń regularnych.
PHP. ZAAWANSOWANE PROGRAMOWANIE
Tabela A.1. Metaznaki i ich znaczenie Wyrażenie
Znaczenie
.
Dowolny znak.
*
Zero lub więcej wystąpień znaku poprzedzającego znak *.
?
Zero lub jedno wystąpienie znaku poprzedzającego znak ?. Powoduje także, że wyrażenie nie jest chciwe (wyjaśnienie różnicy pomiędzy „chciwym” i „niechciwym” wyrażeniem znajduje się w dalszej części dodatku). Znak ? jest również wykorzystywany do ustawiania opcji wewnętrznych.
/
Separator wyrażeń regularnych. Oznacza rozpoczęcie i zakończenie wyrażenia.
+
Co najmniej jedno wystąpienie znaku poprzedzającego znak +.
[ ]
Klasy znaków: [a-z] — wszystkie małe litery. [Ab9] odpowiada znakom A, b i 9. Możliwe jest także zanegowanie klasy znaków za pomocą znaku ^ umieszczonego na początku. ^[a-z] oznacza wszystkie znaki poza małymi literami.
^
Początek linii.
$
Koniec linii.
( )
Grupowanie — zostanie wyjaśnione w dalszej części dodatku.
|
Wyrażenie „lub” — rozdziela dwa podwyrażenia.
{ }
Kwalifikatory. \d{3} oznacza „3 cyfry”, \s{1,5} oznacza „jeden, dwa, trzy, cztery lub pięć znaków spacji”, Z{1,} oznacza „jedna lub więcej liter Z” — jest to synonim wyrażenia Z+.
Tabela A.2. Klasy znaków specjalnych Symbol klasy
Znaczenie
\d, \D
Symbol \d pisany małą literą odpowiada liczbie. Symbol \D pisany dużą literą jest negacją i odpowiada znakom niebędącym liczbą.
\s, \S
Symbol \s pisany małą literą odpowiada znakowi spacji lub znakowi tabulacji. Symbol \D pisany dużą literą odpowiada znakom niebędącym znakiem spacji ani znakiem tabulacji.
\w, \W
Symbol \w pisany małą literą odpowiada dowolnym literom i cyfrom. Podobnie jak w poprzednich przypadkach, \W jest negacją i odpowiada znakom niebędącym literami ani cyframi.
Przykłady wyrażeń regularnych Pierwszy przykład dotyczy miejsca i daty. Wiemy, że 8 czerwca 2012 roku w Warszawie będzie miało miejsce pewne ważne wydarzenie. Wzorzec odpowiadający zapisowi „Warszawa, 8 czerwca 2012” wyglądałby następująco: /[A-Z][a-z]{2,},\s\d{1,2}\s[a-z]{3,}\s\d{4}/
Oznacza to: „Duża litera, po niej przynajmniej dwie małe litery i przecinek, następnie spacja, jedna lub dwie cyfry, spacja, przynajmniej trzy małe litery, spacja i na końcu dokładnie cztery cyfry reprezentujące rok”. Listing A.1 prezentuje fragment kodu pozwalający na testowanie wyrażeń regularnych. Listing A.1. Testowanie wyrażeń regularnych
344
Zauważ, że zmienna $ciag z listingu A.1 ma na końcu kropkę. Wyrażenie regularne natomiast kończy się na \d{4} i nie pasuje do kropki na końcu ciągu znaków. Gdybyśmy chcieli to zmienić, moglibyśmy „zakotwiczyć” wyrażenie regularne na końcu linii, pisząc je w ten sposób: /[A-Z][a-z]{2,},\s\d{1,2}\s[a-z]{3,}\s\d{4}$/. Znak dolara dodany na końcu wyrażenia oznacza „koniec linii” — jeżeli po wyrażeniu wystąpią jakieś znaki, wyrażenie nie zostanie dopasowane. Mogliśmy także „zakotwiczyć” wyrażenie na początku linii, wykorzystując znak ^. Wyrażenie dopasowujące całą linię niezależnie od zawartości ma postać /^.*$/. Spójrzmy teraz na inny format daty, YYYY-MM-DD. Zadanie to sparsowanie daty i wyodrębnienie poszczególnych składowych. UWAGA. Można to wykonać za pośrednictwem funkcji date; to dobry przykład wewnętrznego działania niektórych funkcji PHP.
Musimy sprawdzić, czy linia zawiera poprawną datę, ale także wyodrębnić rok, miesiąc i dzień. Aby to zrobić, trzeba wykorzystać grupy wyrażeń. Można o tym myśleć jak o podwyrażeniach ustawionych w kolejności sekwencji. Wyrażenie regularne, które pozwoli nam wykonać zadanie, wygląda następująco: /(\d{4})-(\d{2})-(\d{2})/
Nawiasy wykorzystywane są do grupowania wyrażeń. Te grupy to podwyrażenia i można je traktować jako odrębne zmienne. Listing A.2 przedstawia wykonanie zadania za pomocą wbudowanej funkcji preg_match. Listing A.2. Dopasowanie grup za pomocą wbudowanej funkcji preg_match %s\n",$i,$dopasowania[$i]); } list($rok,$miesiac,$dzien)=array_splice($dopasowania,1,3); print "Rok:$rok Miesiąc:$miesiac Dzień:$dzien\n"; } else { print "Nie pasuje.\n"; } ?>
W powyższym skrypcie funkcja preg_match przyjmuje trzeci parametr, tablicę $dopasowania. Oto wynik działania skryptu: 0:-->2011-05-01 1:-->2011 2:-->05 3:-->01 Rok:2011 Miesiąc:05 Dzień:01
Zerowy element tablicy $dopasowania zawiera ciąg pasujący do całego wyrażenia. To nie jest to samo co cały ciąg wejściowy. Każda kolejna grupa przedstawiona jest jako pozycja w tablicy. Zobaczmy kolejny, bardziej skomplikowany przykład. Sparsujmy adres URL. Ogólna forma adresu URL jest następująca:
345
PHP. ZAAWANSOWANE PROGRAMOWANIE
http://nazwahosta:port/lokacja?argument=wartosc
Oczywiście, może brakować dowolnej części wyrażenia. Wyrażenie parsujące adres URL w powyższej formie wygląda następująco: /^https?:\/\/[^:\/]+:?\d*\/[^?]*.*/
Znalazło się w nim kilka nowych, wartych zauważenia elementów. Zacznijmy od części s? w ^http[s]?: — dzięki niej zostanie dopasowany zarówno ciąg rozpoczynający się od http:, jak i https:. Znak ^ kotwiczy wyrażenie do początku ciągu. Znak ? oznacza „żadne lub jedno wystąpienie poprzedniego wyrażenia”. Poprzednim wyrażeniem jest litera s — czyli „żadne lub jedno wystąpienie litery s”. Aby usunąć specjalne znaczenie znaków /, zostały one poprzedzone znakami \. W przypadku separatora wyrażeń regularnych PHP jest bardzo pobłażliwe. Pozwala na zamienienie go jakimkolwiek innym separatorem. PHP rozpozna nawiasy kwadratowe lub znak |, więc wyrażenie byłoby poprawne, gdyby zostało zapisane w formie [^https?://[^:/]+:?\d*/[^?]*.*] lub nawet przy użyciu znaku pipe |^https?://[^:/]:?\d*/[^?]*.*|. Sposobem na wyłączenie specjalnego znaczenia danego znaku jest poprzedzenie go znakiem \. Wyrażenia regularne są sprytne i potrafią „domyślić się” znaczenia znaków w zależności od kontekstu. Poprzedzanie znaku zapytania znakiem ucieczki w miejscu [^?]* było niepotrzebne, ponieważ z kontekstu wynika, że chodzi o klasę znaków innych niż znak zapytania. Powyższe nie odnosi się do separatora /:, który musieliśmy poprzedzić znakiem \. Jest jeszcze część wyrażenia [^:\/]+, która oznacza „co najmniej jeden znak inny niż dwukropek lub ukośnik”. To wyrażenie regularne pasuje także do bardziej skomplikowanych form adresu URL (listing A.3). Listing A.3. Wyrażenia regularne dla skomplikowanych adresów URL
Wyodrębnijmy teraz adres hosta, port, katalog i argument z podanego ciągu, wykorzystując grupowanie, tak jak zrobiliśmy to na listingu A.2 (listing A.4). Listing A.4. Wyodrębnienie adresu hosta, portu, katalogu i argumentu $host\n"; print "Port=>$port\n"; print "Katalog=>$kat\n"; print "Argumenty=>$arg\n"; } else { print "Nie pasuje.\n"; } ?>
Wykonany skrypt zwróci wynik: Host=>mojekonto.test.pl Port=>
346
DODATEK WYRAŻENIA REGULARNE
Katalog=>logowanie/login Argumenty=>URI=http://
Opcje wewnętrzne Wartość numeru portu nie była zawarta w adresie, więc nie został on wyodrębniony. Wszystko inne zostało pobrane poprawnie. Co by się stało, gdyby adres URL był zapisany wielkimi literami? HTTPS://mojekonto.test.pl/logowanie/login?URI=http://
Taki adres nie będzie pasował, ponieważ aktualne wyrażenie przewiduje tylko małe litery. Ten adres jest jednak zupełnie poprawny i zostałby rozpoznany przez przeglądarkę. Jeżeli chcemy zezwolić na taką ewentualność, musimy zignorować wielkość liter w wyrażeniu regularnym. Możemy to osiągnąć poprzez ustawienie opcji ignorowania wielkości liter. Wyrażenie będzie teraz wyglądało następująco: [(?i)^https?://([^:/]+):?(\d*)/([^?]*)\??(.*)]
Dla wszystkich znaków występujących po (?i) wielkość znaków będzie ignorowana. Wyrażenie Mladen (?i)g będzie pasowało do ciągów Mladen G oraz Mladen g, ale nie MLADEN G. Kolejną często wykorzystywaną opcją jest m — oznacza ona „wiele linii”. Bez tej opcji sprawdzanie wyrażenia będzie przerwane w momencie napotkania znaku nowej linii. To zachowanie może być zmienione poprzez ustawienie opcji (?m). W tym przypadku koniec parsowania nie nastąpi aż do końca ciągu. Znak dolara będzie pasował także do znaków nowej linii — chyba że zostanie ustawiona opcja D. Oznacza ona, że metaznak $ będzie pasował wyłącznie do znaku końca ciągu, a nie do znaków końca linii zawartych wewnątrz tego ciągu. Opcje mogą być grupowane. Użycie (?imD) na początku wyrażenia włączy wszystkie trzy opcje: ignorowanie wielkości liter, parsowanie wszystkich linii oraz „znak dolara pasuje tylko do końca ciągu”. Istnieje też alternatywa — tradycyjna notacja pozwalająca ustawić opcje globalne. W tym przypadku modyfikatory globalne wstawiane są na końcu wyrażenia, po ostatnim separatorze. Przy wykorzystaniu tej notacji nasze wyrażenie miałoby postać: [^https?://([^:/]+):?(\d*)/([^?]*)\??(.*)]i
Zaletą nowej notacji jest to, że może być wykorzystana w dowolnym miejscu wyrażenia, oraz to, że efekty modyfikatora będą dotyczyły części wyrażenia za nim. W przypadku użycia modyfikatora na końcu wyrażenia jego efekty dotyczą całego wyrażenia. UWAGA. Pełna dokumentacja modyfikatorów globalnych dostępna jest pod adresem www.php.net/manual/en/reference.pcre.pattern.modifiers.php.
Chciwość Standardowo wyrażenia regularne są chciwe. Oznacza to, że parser spróbuje dopasować jak największą część ciągu. Gdyby wyrażenie (123)+ było wykorzystane dla ciągu 123123123123123A, dopasowane zostałoby wszystko przed literą A. W kolejnym przykładzie sprawdzimy powyższą notację. Chodzi o to, aby dopasować wyłącznie znacznik img w linii kodu HTML. Pierwsza iteracja skryptu, która nie działa prawidłowo, jest pokazana na listingu A.5. Listing A.5. Niepoprawny skrypt wyszukujący znacznik img /'; $ciag = 'tekst"'; $dopasowania=array(); if (preg_match($wyrazenie, $ciag,$dopasowania)) { printf( "Dopasowano:%s\n",$dopasowania[0]);
347
PHP. ZAAWANSOWANE PROGRAMOWANIE
} else { print "Nie pasuje.\n"; } ?>
Po wykonaniu otrzymamy następujący wynik: Dopasowano:tekst
UWAGA. Niektóre przeglądarki, przede wszystkim Google Chrome, będą starały się poprawić błędny kod HTML. Wynik w obu przypadkach będzie zatem zawierał .
Dopasowaliśmy więcej, niżbyśmy chcieli, ponieważ wyrażenie .*> dopasuje wszystkie znaki aż do wystąpienia ostatniego znaku >, który jest częścią znacznika , a nie znacznika . Wykorzystanie znaku zapytania spowoduje, że kwalifikatory + i * nie będą chciwe — dopasują minimalną liczbę znaków. Przy modyfikowaniu wyrażenia regularnego na postać dopasowane zostaną znaki do pierwszego wystąpienia znaku > — dzięki temu osiągniemy zamierzony efekt: Dopasowano:
Parsowanie kodu HTML i XML jest typową sytuacją, w której wykorzystywane jest niechciwe dopasowanie — właśnie ze względu na konieczność dopasowywania granic znaczników.
Funkcje wykorzystujące wyrażenia regularne Do tej pory sprawdzaliśmy tylko, czy podany ciąg znaków pasuje do wzorca zapisanego w formie wyrażenia PCRE, wyodrębnialiśmy także części tego ciągu. Dzięki wyrażeniom regularnym można robić także inne rzeczy — takie jak zamiana części ciągów lub dzielenie ich i umieszczanie części w tablicy. Tutaj omówimy funkcje inne niż już przedstawiona funkcja preg_match, wykorzystujące mechanizmy wyrażeń regularnych. Najważniejszą z nich jest preg_replace.
Znaczenie argumentów $wyrazenie, $wstawiony_ciag i $ciag_bazowy są oczywiste. Argument $limit ogranicza liczbę zamian, wartość -1 (domyślna) oznacza brak ograniczenia. Ostatni argument $ilosc uzupełniany jest po wykonaniu zamian liczbą wykonanych operacji. Składnia może się wydawać prosta, jest jednak dość skomplikowana. Przede wszystkim parametry wyrażenia i wstawianego ciągu mogą być tablicami, tak jak na listingu A.6. Listing A.6. Wykorzystanie funkcji preg_replace
348
DODATEK WYRAŻENIA REGULARNE
C jak ciastko, które zjem. Ooooo, ciastko, ciastko, ciastko zaczyna się na C. EOT; $wyrazenie = array("/(?i)ciastko/", "/C/", "/które/"); $zamiana = array("pączek", "P", "którego"); $paczek = preg_replace($wyrazenie, $zamiana, $ciastko); print "$paczek\n"; ?>
Wykonany skrypt wyświetla wynik, który najprawdopodobniej nie spodobałby się Ciasteczkowemu Potworowi z „Ulicy Sezamkowej”: Po zaczyna się od litery P? pączek zaczyna się od litery P. Pomyślmy o innych rzeczach rozpoczynających się od litery P. Eeeee, kogo obchodzą inne rzeczy. P jak pączek, którego zjem. P jak pączek, którego zjem. P jak pączek, którego zjem. Ooooo, pączek, pączek, pączek zaczyna się na P.
Należy zwrócić uwagę, że dwa pierwsze parametry są tablicami. Obie tablice powinny mieć jednakową liczbę parametrów. Jeżeli elementów do zamiany jest mniej niż wyrażeń, to elementy pasujące do wyrażeń z brakującymi zamianami zostaną zamienione na pusty ciąg znaków, co efektywnie usunie te dopasowania. Pełną siłę wyrażeń regularnych można zobaczyć na listingu A.7. Skrypt utworzy listę poleceń TRUNCATE TABLE na podstawie listy tabel. To często spotykane zadanie. Normalnie tabele zostałyby załadowane z pliku — umieściliśmy je w tablicy, aby skrócić skrypt. Listing A.7. Generowanie listy poleceń SQL
Wynik działania skryptu jest następujący: truncate truncate truncate truncate
table table table table
pracownik; dzial; bonus; ocenasprzedazy;
Wykorzystanie preg_replace demonstruje kilka rzeczy. Najpierw zastosowane zostało grupowanie (\w+). Grupowanie widzieliśmy już poprzednio, podczas wyodrębniania składowych daty. To grupowanie pojawia się także jako argument zamiany — $1. Wartość każdego podwyrażenia jest zapisywana w zmiennej $n, gdzie n jest liczbą od 0 do 99. Oczywiście, tak jak w przypadku preg_match, $0 zawiera ciąg pasujący do całego wyrażenia, a kolejne zmienne zawierają ciągi dopasowane do podwyrażeń, numerowane od lewej do prawej. Zwróć także uwagę na znak cudzysłowu. Nie ma możliwości pomylenia zmiennej $1 z niczym innym, ponieważ zmienne przyjmujące formę $n, 0<=n<=99 są zarezerwowane i nie można ich użyć w innych miejscach skryptu. Nazwy zmiennych w PHP muszą zaczynać się od litery lub podkreślenia; jest to zapisane w specyfikacji języka.
349
PHP. ZAAWANSOWANE PROGRAMOWANIE
Inne funkcje Do omówienia mamy jeszcze dwie funkcje: preg_split i preg_grep. Pierwsza z nich, preg_split, jest wszechstronniejszą wersją funkcji explode. Funkcja explode podzieli ciąg znaków na podstawie podanego znaku, tworząc tablicę elementów. Innymi słowy — wykonanie funkcji explode dla ciągu znaków $a="A,B,C,D" z separatorem , spowoduje zbudowanie tablicy zawierającej elementy A, B, C i D. Jak rozbić ciąg znaków, jeżeli separator nie jest ustalony, a ciąg wygląda następująco: $a="A, B,C .D"? W tym przypadku znaki spacji znajdują się w różnych miejscach, mamy także separator będący kropką, przez co nie możemy wykorzystać funkcji explode. Funkcja preg_split nie będzie miała żadnych problemów z tym zadaniem. Przy zastosowaniu następującego wyrażenia regularnego ciąg znaków zostanie poprawnie podzielony: $wynik=preg_split('/\s*[,.]\s*/',$a);
Wyrażenie to oznacza „zero lub więcej spacji, po nich znak będący przecinkiem bądź kropką, a po nim zero lub więcej spacji”. Oczywiście porównywanie wyrażeń regularnych jest kosztowniejsze niż zwykłe porównywanie ciągów znaków. W związku z tym funkcja preg_split nie powinna być stosowana, jeżeli możemy skorzystać z explode; warto jednak mieć ją pod ręką. Zwiększony koszt spowodowany jest tym, że wyrażenia regularne „pod maską” są raczej skomplikowane. UWAGA. Wyrażenia regularne to nie magia. Wymagają one uwagi i testów. Jeżeli nie przyłożysz się do ich tworzenia, możesz otrzymać nieprzewidziane lub błędne wyniki. Wykorzystanie funkcji działającej na podstawie wyrażeń regularnych zamiast dobrze znanej funkcji wbudowanej samo w sobie nie gwarantuje sukcesu.
Funkcja preg_grep powinna być znana wszystkim, którzy wiedzą, jak korzystać z aplikacji wiersza poleceń grep. Funkcja ta zyskała swoją nazwę właśnie dzięki tej aplikacji. Jej składnia jest następująca: $wyniki=preg_grep($wyrazenie,$ciag);
Funkcja przetworzy wyrażenie dla każdego elementu tablicy $ciag i wstawi rezultat do tablicy wynikowej. Efektem jest tablica asocjacyjna, która jako klucze przyjmuje klucze tablicy wejściowej. Listing A.8 pokazuje przykład. Listing A.8. Przykład wykorzystania funkcji preg_grep $wartosc) { printf("%d ==> %s\n", $klucz, $wartosc); } ?>
Kropka przed rozszerzeniem php została poprzedzona znakiem ucieczki, ponieważ jest metaznakiem. Wynik działania skryptu na komputerze, na którym był pisany ten dodatek, wygląda następująco: Liczba plików:35 liczba plików PHP:12 4 ==> /usr/share/pear/DB.php 6 ==> /usr/share/pear/Date.php 8 ==> /usr/share/pear/File.php 12 ==> /usr/share/pear/Log.php 14 ==> /usr/share/pear/MDB2.php 16 ==> /usr/share/pear/Mail.php 19 ==> /usr/share/pear/OLE.php 22 ==> /usr/share/pear/PEAR.php 23 ==> /usr/share/pear/PEAR5.php
Wynik może wyglądać inaczej w innym systemie, mającym zainstalowane inne moduły PEAR. Funkcja preg_grep oszczędza nam konieczności sprawdzania wyrażeń w pętli i może być bardzo użyteczna.
Jest jeszcze kilka innych, znacznie rzadziej stosowanych funkcji wykorzystujących wyrażenia regularne. Funkcje te są opisane w dokumentacji dostępnej online na stronie www.php.net.