PEAR. Programowanie w PHP 9788324669349, 8324669345

Dzięki książce "PEAR. Programowanie w PHP" nauczysz się wykonywać codzienne zadania programistyczne przy użyci

238 72 5MB

Polish Pages 304 [298] Year 2013

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Spis treści
O autorach
O redaktorach
Przedmowa
Co można znaleźć w tej książce
Konwencje
Rozdział 1. MDB2
Krótka historia MDB2
Warstwy abstrakcji
Warstwa abstrakcji dla interfejsu bazy danych
Warstwa abstrakcji dla kodu SQL
Warstwa abstrakcji dla typów danych
Uwarunkowania związane z prędkością
Konstrukcja pakietu MDB2
Zaczynamy pracę z MDB2
Instalowanie MDB2
Łączenie się z bazą danych
Tworzenie instancji obiektu MDB2
Opcje
Definiowanie trybu pobierania danych
Rozłączanie się z bazą danych
Korzystanie z MDB2
Przykład
Wykonywanie zapytań
Pobieranie danych
Skróty ułatwiające pobieranie danych
Skróty metod query*()
Skróty metod get*()
Typy danych
Ujmowanie wartości i identyfikatorów w cudzysłowy
Iteratory
Wyszukiwanie błędów
Warstwa abstrakcji kodu SQL w MDB2
Sekwencje
Określanie limitów zapytań
Zastępowanie zapytań
Obsługa subselektów
Instrukcje preparowane
Transakcje
Moduły MDB2
Moduł Manager
Moduł Function
Moduł Reverse
Własne rozszerzenia pakietu MDB2
Własny mechanizm rejestracji w dzienniku
Własne klasy pobierające dane
Własne klasy wyników
Własne iteratory
Własne moduły
Pakiet MDB2_Schema
Instalowanie i tworzenie instancji
Tworzenie kopii bazy danych
Zmienianie bazy danych
Podsumowanie
Rozdział 2. Wyświetlanie danych
Tabele HTML
Format tabel HTML
Tworzenie prostego kalendarza za pomocą HTML_Table
Pakiet HTML_Table_Matrix rozszerzający możliwości pakietu HTML_Table
Arkusze kalkulacyjne Excela
Format Excela
Nasz pierwszy arkusz kalkulacyjny
Słowo o komórkach
Przygotowywanie strony do wyświetlenia
Dodawanie formatowania
Kolory
Wypełnianie barwnym deseniem
Formatowanie liczb
Formuły
Wiele arkuszy kalkulacyjnych, obramowania, obrazki
Inne techniki tworzenia arkuszy kalkulacyjnych
Komponent siatki danych DataGrid
Źródła danych DataSource
Renderery
Prosta siatka danych DataGrid
Stronicowanie wyników
Korzystanie ze źródła danych DataSource
Korzystanie z renderera
Estetyczne formatowanie siatki danych
Poszerzanie możliwości DataGrid
Dodawanie kolumn
Generowanie plików PDF
Kolory
Czcionki
Komórki
Tworzenie nagłówków i stopek
Podsumowanie
Rozdział 3. Praca z formatem XML
Pakiety PEAR wspomagające pracę z XML
Tworzenie dokumentów XML
Tworzenie obiektów przechowujących informacje o nagraniach
Tworzenie dokumentów XML za pomocą klasy XML_Util
Tworzenie dokumentów XML za pomocą pakietu XML_FastCreate
Tworzenie dokumentów XML za pomocą pakietu XML_Serializer
Tworzenie aplikacji Mozilli za pomocą pakietu XML_XUL
Przetwarzanie dokumentów XML
Analizowanie danych XML za pomocą pakietu XML_Parser
Przetwarzanie kodu XML za pomocą pakietu XML_Unserializer
Analizowanie danych RSS za pomocą pakietu XML_RSS
Podsumowanie
Rozdział 4. Usługi WWW
Korzystanie z usług WWW
Korzystanie z usług WWW opartych na XML-RPC
Sięganie do interfejsu API Google
Korzystanie z usług WWW opartych na REST
Tworzenie własnych usług WWW opartych na REST
Oferowanie usług WWW
Oferowanie usług WWW opartych na protokole XML-RPC
Oferowanie usług WWW opartych na protokole SOAP
Oferowanie usług opartych na protokole REST za pomocą pakietu XML_Serializer
Podsumowanie
Rozdział 5. Praca z datami
Praca z pakietem Date
Pakiet Date
Obsługa stref czasowych za pomocą klasy Date_Timezone
Pakiet PEAR::Date — podsumowanie
Pakiet Date_Holidays
Wyliczanie świąt
Czy dziś mamy święto?
Tłumaczenie nazw świąt na inne języki
Pakiet Date_Holidays — podsumowanie
Praca z pakietem Calendar
Podstawowe klasy i pojęcia związane z pakietem Calendar
Tworzenie obiektów
Sprawdzanie poprawności obiektów kalendarza
Modyfikowanie działania standardowych klas
Generowanie danych w formie graficznej
Podsumowanie
Skorowidz
Recommend Papers

PEAR. Programowanie w PHP
 9788324669349, 8324669345

  • 0 0 0
  • Like this paper and download? You can publish your own PDF file online for free in a few minutes! Sign Up
File loading please wait...
Citation preview

Tytuł oryginału: PHP Programming with PEAR Tłumaczenie: Sławomir Dzieniszewski ISBN: 978-83-246-6934-9 Copyright © Packt Publishing 2006. First published in the English language under the title „PHP Programming with PEAR” Polish language edition published by Wydawnictwo Helion S.A. Copyright © 2007 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 032 231 22 19, 032 230 98 63 e-mail: [email protected] WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/pearph.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie?pearph_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Poleć książkę na Facebook.com

• Księgarnia internetowa

• Kup w wersji papierowej

• Lubię to! » Nasza społeczność

• Oceń książkę

Spis treści O autorach

7

Przedmowa

11

Rozdział 1. MDB2

15

Krótka historia MDB2 Warstwy abstrakcji Warstwa abstrakcji dla interfejsu bazy danych Warstwa abstrakcji dla kodu SQL Warstwa abstrakcji dla typów danych Uwarunkowania związane z prędkością Konstrukcja pakietu MDB2 Zaczynamy pracę z MDB2 Instalowanie MDB2 Łączenie się z bazą danych Tworzenie instancji obiektu MDB2 Opcje Definiowanie trybu pobierania danych Rozłączanie się z bazą danych Korzystanie z MDB2 Przykład Wykonywanie zapytań Pobieranie danych Skróty ułatwiające pobieranie danych Skróty metod query*() Skróty metod get*() Typy danych Ujmowanie wartości i identyfikatorów w cudzysłowy Iteratory Wyszukiwanie błędów

16 16 16 17 17 17 18 19 19 20 21 21 23 23 24 24 25 25 26 26 27 29 31 32 33

PEAR. Programowanie w PHP

Warstwa abstrakcji kodu SQL w MDB2 Sekwencje Określanie limitów zapytań Zastępowanie zapytań Obsługa subselektów Instrukcje preparowane Transakcje Moduły MDB2 Moduł Manager Moduł Function Moduł Reverse Własne rozszerzenia pakietu MDB2 Własny mechanizm rejestracji w dzienniku Własne klasy pobierające dane Własne klasy wyników Własne iteratory Własne moduły Pakiet MDB2_Schema Instalowanie i tworzenie instancji Tworzenie kopii bazy danych Zmienianie bazy danych Podsumowanie

Rozdział 2. Wyświetlanie danych Tabele HTML Format tabel HTML Tworzenie prostego kalendarza za pomocą HTML_Table Pakiet HTML_Table_Matrix rozszerzający możliwości pakietu HTML_Table Arkusze kalkulacyjne Excela Format Excela Nasz pierwszy arkusz kalkulacyjny Słowo o komórkach Przygotowywanie strony do wyświetlenia Dodawanie formatowania Kolory Wypełnianie barwnym deseniem Formatowanie liczb Formuły Wiele arkuszy kalkulacyjnych, obramowania, obrazki Inne techniki tworzenia arkuszy kalkulacyjnych Komponent siatki danych DataGrid Źródła danych DataSource Renderery Prosta siatka danych DataGrid Stronicowanie wyników Korzystanie ze źródła danych DataSource Korzystanie z renderera

4

34 34 35 36 36 37 41 42 43 46 47 49 49 51 52 55 56 57 57 58 61 61

63 64 64 65 69 71 71 72 73 74 74 75 77 77 79 80 83 84 85 85 86 87 87 88

Spis treści

Estetyczne formatowanie siatki danych Poszerzanie możliwości DataGrid Dodawanie kolumn Generowanie plików PDF Kolory Czcionki Komórki Tworzenie nagłówków i stopek Podsumowanie

89 90 91 92 95 96 96 97 98

Rozdział 3. Praca z formatem XML Pakiety PEAR wspomagające pracę z XML Tworzenie dokumentów XML Tworzenie obiektów przechowujących informacje o nagraniach Tworzenie dokumentów XML za pomocą klasy XML_Util Tworzenie dokumentów XML za pomocą pakietu XML_FastCreate Tworzenie dokumentów XML za pomocą pakietu XML_Serializer Tworzenie aplikacji Mozilli za pomocą pakietu XML_XUL Przetwarzanie dokumentów XML Analizowanie danych XML za pomocą pakietu XML_Parser Przetwarzanie kodu XML za pomocą pakietu XML_Unserializer Analizowanie danych RSS za pomocą pakietu XML_RSS Podsumowanie

Rozdział 4. Usługi WWW Korzystanie z usług WWW Korzystanie z usług WWW opartych na XML-RPC Sięganie do interfejsu API Google Korzystanie z usług WWW opartych na REST Tworzenie własnych usług WWW opartych na REST Oferowanie usług WWW Oferowanie usług WWW opartych na protokole XML-RPC Oferowanie usług WWW opartych na protokole SOAP Oferowanie usług opartych na protokole REST za pomocą pakietu XML_Serializer Podsumowanie

Rozdział 5. Praca z datami Praca z pakietem Date Pakiet Date Obsługa stref czasowych za pomocą klasy Date_Timezone Pakiet PEAR::Date — podsumowanie Pakiet Date_Holidays Wyliczanie świąt Czy dziś mamy święto? Tłumaczenie nazw świąt na inne języki Pakiet Date_Holidays — podsumowanie

99 100 101 102 106 110 118 133 142 143 155 169 173

175 176 177 182 185 199 208 208 216 223 232

235 235 236 246 250 250 254 258 259 264

5

PEAR. Programowanie w PHP

Praca z pakietem Calendar Podstawowe klasy i pojęcia związane z pakietem Calendar Tworzenie obiektów Sprawdzanie poprawności obiektów kalendarza Modyfikowanie działania standardowych klas Generowanie danych w formie graficznej Podsumowanie

Skorowidz

6

264 265 268 272 274 275 282

283

X O autorach Stephan Schmidt pracuje dla firmy 1&1 Internet, jednego z największych na świecie dostawców internetu, działającego w Karlsruhe (Niemcy). Kieruje zespołem programistów języków PHP i Java oraz specjalizuje się w programowaniu witryn WWW i internetowych systemów składania zamówień dla firmy 1&1 Internet. Od 2001 roku aktywnie współpracuje przy projekcie otwartego kodu źródłowego języka PHP. Wówczas to wraz z przyjaciółmi założył witrynę internetową PHP Application Tools (http://www.php-tools.net), która obecnie jest jednym z najstarszych projektów PHP OSS. Ponadto pracuje nad ponad 15 pakietami PEAR (koncentrując się na języku XML i usługach WWW), jak również nad rozszerzeniem id3. Ostatnio rozpoczął jeszcze projekt XJConf (http://www.xjconf.net) oraz udziela się w społeczności programistów języka Java. Jest autorem (napisanej oryginalnie w języku niemieckim) książki PHP Design Patterns (O’Reilly Verlag, ISBN 3-89721-442-3), jak również współautorem kilku innych publikacji poświęconych językowi PHP; ponadto pisał artykuły dla kilku czasopism informatycznych. Występował również jako prelegent na różnych konferencjach poświęconych projektom otwartego kodu źródłowego (ang. open source), organizowanych na całym świecie. Wolny czas poświęca kolekcjonowaniu amerykańskich komiksów o superbohaterach ze złotego okresu lat 50. Carsten Lucke studiował informatykę na Uniwersytecie Nauk Stosowanych w Brandenburgu (Fachhochshule Brandenburg) w Niemczech. Obecnie pracuje jako programista dla firmy software design and management AG (sd&m AG) w Monachium. W wolnym czasie pisze artykuły dla różnych czasopism i wspiera środowisko programistów open source (szczególnie programistów języka PHP). Jest autorem bardzo pomocnych pakietów PEAR/PECL, twórcą projektu serwera kanałów 3rdPEARty (3rdpearty.net) oraz projektu tool-garage.de oferującego darmowe oprogramowanie open source.

PEAR. Programowanie w PHP

Chciałbym przede wszystkim podziękować Tomowi Kouri oraz zespołowi High-Touch Communications z Montrealu. Szczególne wyrazy wdzięczności kieruję też do Dereka Fonga za wprowadzenie mnie w PEAR i do Michaela Caplana za to, że zawsze dotrzymuje kroku nowinkom wprowadzanym w PEAR.

Aaron Wormus jest niezależnym konsultantem pracującym w okolicach Frankfurtu w Niemczech. Specjalizuje się w programowaniu architektury klient – serwer oraz infrastruktury intranetowej. W pracy korzysta głównie z języka PHP i narzędzi open source. Aaron regularnie publikuje również artykuły w magazynach „PHPMagazine”, „PHPArchitect” i „PHPSolutions”. Są to głównie teksty poświęcone pakietom PEAR, programowaniu w PHP i metodologii programowania. Jest również zapalonym blogerem, dbającym o to, by jego osobisty blog był zawsze aktualny, pełen świeżych technicznych wskazówek, komentarzy politycznych i najnowszych wieści na temat tej cudownej a zarazem tajemniczej rzeczy, jaką jest internet. Chwile, których nie spędza przed komputerem, poświęca zazwyczaj zabawie ze swoimi dwiema córeczkami lub goszcząc w kuluarach jakiejś konferencji poświęconej nowinkom ze świata programowania.

O redaktorach Lukas Kahwe Smith programuje w języku PHP już od roku 2000, a od 2001 roku publikuje w repozytorium kodu PEAR. Od tego czasu zaprogramował i przygotował wiele pakietów PEAR, z których najbardziej znane to MDB2 i LiveUser; miał także istotny wpływ na sam kierunek projektu PEAR, będąc jednym z członków założycieli komitetu zarządzającego PEAR Group (PEAR Group steering committe) oraz głównego zespołu QA. Oprócz licznych publikacji w kilku magazynach branżowych jest też częstym prelegentem na wielu międzynarodowych konferencjach poświęconych językowi PHP. Shu-Wai Chow działa w branży komputerowej i programistycznej od ośmiu lat. Zaczynał karierę w Sacramento, stolicy stanu Kalifornia, przez cztery lata pracując jako administrator sieci w firmach Educaid i First Union, a przez kolejne cztery — jako projektant aplikacji w Vision Service Plan. W tym czasie zgłębił takie technologie, jak Java, JSP, PHP, ColdFusion, ASP, LDAP, XSLT i XSL-FO. Shu działa też społecznie jako administrator sieci i doradca w sprawach adopcji kotów dla kilku organizacji opieki nad zwierzętami w okolicach Sacramento. Obecnie pracuje jako programista w firmie Antenna Software w Jersey City w stanie New Jersey.

8

O autorach

Mimo iż urodzony w byłej brytyjskiej kolonii Hongkongu, większość swego dzieciństwa spędził w Palo Alto w Kalifornii. Studiował antropologię i ekonomię na Uniwersytecie Stanu Kalifornia w Sacramento. Obecnie mieszka na wybrzeżu Atlantyku w stanie New Jersey wraz z siedmiorgiem bardzo wymagających kotów, trzema podejrzanie inteligentnymi ptakami, umiłowaną gitarą elektryczną Fender Stratocaster i swoją ukochaną dziewczyną o anielskiej naturze. Arnaud Limburg programuje w języku PHP już od czterech lat. Uczestniczy w projekcie PEAR jako programista odpowiedzialny za kontrolę jakości i jeden z programistów odpowiedzialnych za pakiet LiveUser. Obecnie pracuje dla firmy telekomunikacyjnej jako programista VoIP.

9

PEAR. Programowanie w PHP

10

X Przedmowa Skrót PEAR wywodzi się od słów: PHP Extension and Application Repository (Repozytorium Rozszerzeń i Aplikacji PHP), i oznacza ramę projektową a zarazem system dystrybucji wszechstronnie sprawdzonych, przeznaczonych do wielokrotnego użytku komponentów PHP, dostępnych w formie „pakietów” oprogramowania. Witryna projektu PEAR to pear.php.net, w której można znaleźć, a następnie pobrać najróżniejsze użyteczne pakiety oprogramowania napisane w języku PHP. W witrynie tej programiści znajdą pakiety oprogramowania PHP pomocne w rozwiązywaniu prawie każdego problemu, z którym mogą się zetknąć w codziennej pracy. Dodatkowo oprócz funkcji i możliwości oferowanych przez udostępniane w witrynie pakiety, kod publikowany w repozytorium PEAR podlega ścisłym zaleceniom, dzięki którym kod PHP zyskuje na spójności. W naszej książce pokażemy, jak korzystać z kilku najbardziej użytecznych pakietów PEAR, które pozwolą czytelnikom znacznie poprawić produktywność w trakcie programowania. Koncentrując się na pakietach kluczowych dla przebiegu programowania, książka ta jest znakomitym przewodnikiem po jednej z najbardziej użytecznych składnic gotowego kodu, dostępnych w internecie.

Co można znaleźć w tej książce Rozdział 1. jest wprowadzeniem do warstwy abstrakcji bazy danych MDB2. Czytelnicy dowiedzą się z niego, w jaki sposób łączyć się z bazą danych, dokonywać instancjacji (tworzyć) obiektu MDB2. Pakiet MDB2 znakomicie wywiązuje się z postawionego przed nim zadania, którym jest obudowanie i ukrycie różnic między systemami baz danych i dostarczenie programiście uniwersalnego interfejsu umożliwiającego dostęp do ich funkcji. Dzięki temu programista nie musi przejmować się szczegółami implementowania różnych funkcji w zależności od systemu bazy danych. Opowiemy, jak korzystać z tej funkcji dokonującej abstrakcji

PEAR. Programowanie w PHP

kodu SQL, by za jej pomocą oferować użytkownikom pola z automatycznie zwiększanymi wartościami (ang. auto-increment fields), wykonywać zapytania zmieniające zawartość rekordów w bazie danych lub też wstawiające nowe rekordy. Powiemy również, jak używać już przygotowanych, standardowych instrukcji SQL oraz jak korzystać z metod zapisywania danych w bazie, które uwzględniać będą wymogi bezpieczeństwa. Czytelnik dowie się również o różnych pakietach MDB2 oraz jak dodawać do pakietu MDB2 nowe klasy pobierające wyniki, nowe iteratory i moduły. Gdy już pobierzemy dane z bazy, trzeba je będzie wyświetlić na stronie. Rozdział 2. omawia kilka popularnych pakietów PEAR wykorzystywanych do prezentowania danych w różnych formatach. Dowiemy się na przykład, jak za pomocą modułów HTML_Table i HTML_Table_Matrix tworzyć i formatować tabele. Jak przy użyciu pakietu Spreadsheet_ Excel_Writer tworzyć i generować arkusze kalkulacyjne Excela. Jak tworzyć wszechstronne i wygodnie dzielone na strony „siatki danych” za pomocą modułu Structures_DataGrid oraz jak na poczekaniu generować dokumenty PDF z wykorzystaniem modułu File_PDF. Kolejnym często używanym formatem w pracy z danymi jest format XML — repozytorium PEAR oferuje oczywiście odpowiednie moduły wspomagające ten format. W rozdziale 3. opowiemy dokładnie, jak repozytorium PEAR ułatwia pracę z formatem XML. Rozdział ten omawiać będzie między innymi tworzenie dokumentów XML za pomocą pakietów XML_Util, XML_FastCreate, XML_Serializer i XML_XUL. Ponadto opisuje, w jaki sposób odczytywać dokumenty XML korzystając z parsera (analizatora) opartego na SAX oraz jak przekształcać obiekty PHP na kod XML (i odwrotnie!) za pomocą modułów XML_Serializer i XML_Unserializer. Rozdział 4. opowiada o pakietach PEAR wspomagających usługi WWW i interfejsy API wykorzystywane w sieci WWW. Dowiemy się między innym, jak korzystać z usług SOAP i XMLRPC, jak sięgać do interfejsu API Google, jak przeszukiwać pozycje w blogach za pomocą pakietu Services_Technorati, jak korzystać z usługi WWW księgarni Amazon, jak sięgać do interfejsu API Yahoo. Dowiemy się również, jak oferować nowe usługi WWW oparte na XML-RPC lub SOAP. Ponadto damy czytelnikowi przedsmak tego, jak może wyglądać usługa WWW oparta na REST, przygotowana za pomocą modułu XML_Serializer. Rozdział 5. jest poświęcony funkcjom daty i czasu oferowanym przez moduły PEAR::Calendar i PEAR::Date z repozytorium PEAR. Dowiemy się, jakie zalety mają te moduły w stosunku do standardowych funkcji daty i czasu języka PHP. Następnie pokażemy, jak tworzyć, zmieniać i porównywać ze sobą obiekty daty Date korzystając z modułu Date_Span. Jak obsługiwać strefy czasowe i uwzględniać święta za pomocą modułów Date_Holidays oraz jak korzystać z klasy Calendar, by za jej pomocą wyświetlać kalendarz HTML.

12

Przedmowa

Konwencje W tej książce znajdziemy wiele stylów tekstu, wykorzystywanych do oznaczania różnych rodzajów informacji. Oto kilka przykładów używanych stylów, wraz z objaśnieniem ich znaczenia. W kodach programów zasadniczo wykorzystujemy trzy różne style. Fragmenty kodu pojawiające się w akapitach zwykłego tekstu formatowane są tak jak w tym przykładzie: „Również ta klasa oferuje metodę setId(), która przywoływana jest przez obiekt Label, gdy do listy artystów dodawany jest kolejny”. Blok kodu jest natomiast formatowany w następujący sposób: function getDGInstance($type) { if (class_exists($type)) { $datagrid =& new $type; return $datagrid; } else { return false; } }

Kiedy natomiast zechcemy zwrócić uwagę czytelnika na jakiś fragment kodu, określone wiersze lub fragmenty kodu zostaną wytłuszczone: $driver = Date_Holidays::factory($driverId, $year); $InternalNames = $driver->getInternalHolidayNames();

Wszelkie polecenia i dane wprowadzane lub zwracane w wierszu poleceń będą natomiast prezentowane takim stylem: $ pear-dh-compile-translationfile --help

Nowe terminy i ważne słowa będą oznaczane kursywą. Podobnie polecenia pojawiające się w menu, na przyciskach lub w oknach dialogowych. Na przykład: „kliknięcie przycisku Next (Dalej) przeniesie nas do kolejnego ekranu”. Ostrzeżenia, wskazówki, porady i ważne uwagi umieszczane będą w takich ramkach.

Pliki z przykładami do pobrania znajdują się pod adresem: ftp://ftp.helion.pl/przyklady/pearph.zip

13

PEAR. Programowanie w PHP

14

1 MDB2 W ciągu ostatnich dziesięciu lat sieć WWW bardzo się rozrosła, jak również dojrzała i sprofesjonalizowała, w związku z czym pojawiło się zapotrzebowanie na coraz bardziej złożone i dynamiczne witryny WWW. Dawniej zupełnie wystarczało przechowywanie informacji w pliku tekstowym lub prostej bazie danych, obecnie jednak każdy programista piszący profesjonalną aplikację WWW musi posiadać rzetelną wiedzę na temat tego, jak komunikować się z profesjonalnymi relacyjnymi bazami danych. Począwszy od najwcześniejszych wersji język PHP zawsze służył programistom solidnym wsparciem w kontaktach z bazami danych. Niemniej do czasu wprowadzenia rozszerzenia PDO (PHP Data Objects, obiekty danych PHP) nie istniał żaden standardowy sposób korzystania z najróżniejszych sterowników baz danych dodawanych do języka PHP. Brak ujednoliconego interfejsu API był oczywiście inspiracją dla kilku projektów mających na celu stworzenie jakiejś warstwy DBAL (Database Abstraction Layer), która oferowałaby uniwersalny poziom abstrakcji dla wszystkich baz danych. Głównym celem tych wysiłków był zamiar ułatwienia życia programistom, tak aby mogli oni pisać kod komunikujący się z bazą danych, który będzie niezależny od sytemu bazy danych wykorzystywanego przez aplikację. Dzięki temu klienci lub użytkownicy mogliby używać aplikacji w połączeniu z tym systemem zarządzania bazami danych, który im najbardziej odpowiada. Trzy najważniejsze z rozpoczętych w tamtych latach prób stworzenia warstwy abstrakcji dla baz danych to: AdoDB, PEAR::DB i Metabase. W ostatnich latach kolejnym bardzo mocnym kandydatem na uniwersalną warstwę abstrakcji dla baz danych był pakiet PEAR::MDB. Niniejszy rozdział poświęcony będzie kolejnej inkarnacji MDB, a mianowicie MDB2.

PEAR. Programowanie w PHP

Krótka historia MDB2 Wszystko zaczęło się, kiedy Lukas Smith, programista PEAR, opublikował kilka łat do istniejącej już warstwy abstrakcji bazy danych, Metabase. W którymś momencie między Lukasem a autorem Metabase wywiązała się dyskusja na temat, jakby opublikować Metabase w repozytorium PEAR jako nowy pakiet. Celem nowego pakietu byłoby połączenia funkcjonalności oferowanych przez Metabase z interfejsem API istniejącego już i bardzo popularnego pakietu PEAR::DB. Dzięki temu programiści otrzymaliby bogatą w funkcje i znakomicie spisującą się bibliotekę abstrakcji bazy danych, co byłoby z wielką korzyścią dla infrastruktury PEAR. W taki właśnie sposób narodził się przodek pakietu MDB2, czyli pakiet PEAR::MDB. Po kilku latach pracy nad pakietem PEAR::MDB, dla autorów stało się oczywiste, że decyzja utrzymywania interfejsu API pakietu w takiej formie, aby był jak najbardziej zbliżony do interfejsów API Metabase i PEAR::DB, nieuchronnie stwarza pewne problemy, które utrudniają przekształcenie MDB w pełni profesjonalną warstwę abstrakcji bazy danych (DBAL). Ponieważ pakiet PEAR::MDB osiągnął już w repozytorium PEAR stabilną formę w pełni dojrzałego pakietu oprogramowania, niemożliwe było usunięcie pewnych wad bez rezygnacji z kompatybilności z tym starszym modułem, czego właśnie autorzy starali się za wszelką cenę uniknąć. Rozwiązaniem było wykorzystanie doświadczeń zdobytych podczas prac nad Metabase i MDB oraz zastosowanie ich w nowym pakiecie, który zawierać będzie profesjonalnie zaprojektowany i w pełni nowoczesny interfejs API. Nowy pakiet otrzymał nazwę MDB2.

Warstwy abstrakcji Zanim przejdziemy do szczegółowego omawiania, w jaki sposób pakiet MDB2 radzi sobie z tworzeniem abstrakcji dla bazy danych, powinniśmy najpierw zapoznać się przynajmniej pobieżnie z teorią tworzenia warstw abstrakcji obudowujących systemy baz danych, by zrozumieć dokładnie, w jaki sposób się to robi. Na tworzenie warstw abstrakcji dla baz danych można spojrzeć z kilku perspektyw. Omówimy je teraz dokładniej po kolei i opowiemy, jakie mają wymagania.

Warstwa abstrakcji dla interfejsu bazy danych Najważniejszym etapem w tworzeniu abstrakcji systemu bazy danych jest przygotowanie odpowiedniej abstrakcji dla interfejsu bazy danych. Dzięki temu programista będzie mógł sięgać do baz danych zarządzanych przez różne systemy, używając tych samych metod. Oznacza to, że zarówno tworzenie połączenia z bazą danych, wysyłanie zapytania, jak i pobieranie danych zawsze przebiegać będzie identycznie, niezależnie od tego, z jaką bazą danych będziemy współpracować.

16

Rozdział 1. • MDB2

Warstwa abstrakcji dla kodu SQL Większość obecnie używanych systemów baz danych korzysta ze standardowego zestawu podstawowych instrukcji SQL, dlatego też większość kodu SQL pisanego przez programistów powinna działać zawsze, niezależnie od tego, z jakiej bazy danych będą korzystać. Niemniej wiele z systemów baz danych wprowadza własne, specyficzne tylko dla danego systemu instrukcje SQL i pomocnicze funkcje, dlatego też może się zdarzyć, że kod SQL napisany specjalnie dla jednej bazy danych nie będzie działał w innej. W miarę jak system zarządzania bazą danych (ang. Relational Database Management System — RDBMS) rozwija się, czasami implementuje funkcje, które nie są kompatybilne ze starszymi wersjami tej samej bazy danych. Dlatego też dla programisty pragnącego napisać kod, który byłby zgodny ze wszystkimi wersjami bazy danych (lub który mógłby współdziałać z kilkoma różnymi systemami baz danych), jedynym rozwiązaniem jest ograniczenie się tylko do tych instrukcji kodu SQL, o których wiadomo, że na pewno będą działać na wszystkich platformach baz danych. Lepszą opcją jest jednak skorzystanie ze specjalnej warstwy abstrakcji baz danych, która w razie potrzeby emuluje odpowiednie funkcje, jeśli nie będą dostępne na danej platformie. Mimo iż nie jest wykonalne obudowanie każdej możliwej funkcji SQL, pakiet MDB2 obsługuje bardzo wiele powszechnie wykorzystywanych funkcji SQL. Funkcje te to między innymi obsługa zapytań LIMIT, subselektów (podzapytań select) i zapytań preparowanych. Korzystanie z mechanizmu abstrakcji kodu SQL, oferowanego przez MDB2, daje nam gwarancję, że będziemy mogli korzystać z tych zaawansowanych funkcji — nawet wtedy, gdy baza danych, z której korzystamy, samoistnie ich nie obsługuje. W dalszej części tego rozdziału opowiemy o różnych funkcjach oferujących abstrakcję dla zaawansowanych narzędzi SQL, które zapewnia pakiet MDB2.

Warstwa abstrakcji dla typów danych Na koniec wreszcie konieczne jest przygotowanie abstrakcji dla typów danych stosowanych przez bazy. Wynika to z faktu, że różne systemy baz danych często obsługują typy danych w zupełnie inny sposób.

Uwarunkowania związane z prędkością Zapewne czytelnikom cieknie już ślinka, by skosztować tych wspaniałych funkcji wkomponowanych w pakiet MDB2, niemniej najpierw należy powiedzieć kilka słów o zagadnieniach związanych z prędkością i wydajnością. Warto wiedzieć, że gdy korzystamy z warstwy abstrakcji bazy danych, często za bogactwo funkcji oferowanych przez pakiet dokonujący abstrakcji musimy zapłacić mniejszą wydajnością i szybkością działania bazy danych. Nie jest to

17

PEAR. Programowanie w PHP

tylko ułomność pakietu MDB2 ani też warstw abstrakcji bazy danych, ale w ogóle wszelkiego rodzaju warstw abstrakcji i systemów wirtualizacji. Na szczęście, inaczej niż w przypadku VMWare lub Miscrosoft Virtual PC, które dokonują abstrakcji każdego wykonywanego wywołania systemowego, pakiet MDB2 oferuje abstrakcję tylko wtedy, gdy dana funkcja nie jest dostępna w określonym systemie baz danych. Oznacza to, że wydajność zależeć będzie od platformy, na której skorzystamy z MDB2. Jeśli szczególnie zależy nam na szybkości i wydajności, to należy skorzystać z pamięci podręcznej dla kodów operacji (ang. opcode cache) lub włączyć mechanizm przechowywania zapytań w pamięci podręcznej w systemie baz danych, którego używamy. Dzięki wykorzystaniu wspomnianych możliwości języka PHP lub systemu baz danych będziemy mogli w znacznym stopniu ograniczyć negatywne efekty spowolnienia działania bazy danych, nieodłącznie związane z użyciem warstwy abstrakcji.

Konstrukcja pakietu MDB2 Interfejs API pakietu MDB2 został zaprojektowany w taki sposób, aby gwarantować maksimum wszechstronności i elastyczności. Poszczególnym obsługiwanym systemom baz danych i określonym zaawansowanym funkcjom przypisano określone moduły. Każdy z licznych sterowników (ang. drivers) dla baz danych jest osobnym i niezależnie rozwijanym modułem PEAR. Oznacza to, że każdy pakiet sterownika funkcjonuje niezależnie, a kolejne wersje i wersje stabilne publikowane są we własnych, niezależnych od innych sterowników cyklach. Dzięki temu programiści odpowiedzialni za przygotowywanie poszczególnych sterowników mogą je wypuszczać, ilekroć zachodzi taka potrzeba, bez konieczności czekania na publikację kolejnej wersji głównego pakietu MDB2. Pakiet MDB2 może zatem zachowywać stabilność, niezależnie od stanu prac nad pakietami obsługującymi poszczególne sterowniki. W efekcie zdarza się czasem, że stabilna wersja pakietu oferuje sterowniki dla niektórych systemów baz danych jedynie w wersji beta. Ponadto w chwili wypuszczenia nowego sterownika dla bazy danych oznaczany jest on jako wersja alfa i pakiet podlega procedurze sprawdzania zgodnie ze standardami repozytorium PEAR. Drugi rodzaj modułów wbudowanych w pakiet MDB2 to moduły dodające specjalne, rozszerzone funkcje oferowane przez pakiet MDB2. Zamiast dołączać te funkcje do głównego pakietu MDB2 lub dodawać do niego nową klasę implementującą te funkcje, programista ma możliwość utworzenia nowej klasy w osobnym module i następnie załadowanie jej do pakietu MDB2 za pomocą metody loadModule(). Gdy już nowy moduł zostanie załadowany do pakietu MDB2, do jego metod będzie można sięgać w taki sam sposób, jakby były metodami wbudowanymi w pakiet MDB2. Pakiet MDB2 stosuje to rozwiązanie, aby jego wewnętrzne pakiety działały tak szybko, jak to tylko możliwe, a jednocześnie by pozostawić użytkownikom swobodę dołączania do pakietu MDB2 swoich własnych klas. Szczegółowe informacje o tym, jak we własnym zakresie rozwijać pakiet MDB2, można znaleźć w dalszej części tego rozdziału.

18

Rozdział 1. • MDB2

Zaczynamy pracę z MDB2 Poniżej omówimy podstawowe kroki, które trzeba wykonać, by zainstalować pakiet MDB2, utworzyć obiekt MDB2 oraz skonfigurować kilka opcji definiujących tryb pobierania danych. Na koniec powiemy, jak rozłączać się z bazą danych.

Instalowanie MDB2 Podczas instalowania pakietu MDB2 należy pamiętać, że nie zawiera on żadnych sterowników baz danych, dlatego trzeba je będzie zainstalować później osobno. Pakiet MDB2 jest rozprowadzany w wersji stabilnej, niemniej, jak już wspomnieliśmy, niektóre z wchodzących w jego skład modułów sterowników i rozszerzeń mogą być rozwijane w niezależnych cyklach, dlatego niektóre z wykorzystywanych przez nas modułów mogą być dopiero w wersji beta, alfa lub nawet jeszcze w fazie programowania. Należy o tym pamiętać podczas instalowania pakietów zawierających sterowniki poszczególnych baz danych. Najprościej jest zainstalować MDB2 korzystając z programu instalacyjnego repozytorium PEAR: > pear install MDB2

To polecenie zainstaluje klasy tworzące rdzeń MDB2, natomiast nie zainstaluje żadnego z dostępnych sterowników baz danych. Aby zainstalować sterownik właściwy dla systemu baz danych, którego używamy, należy skorzystać z polecenia: > pear install MDB2_Driver_mysql

To akurat polecenie zainstaluje sterownik dla bazy MySQL. Aby zainstalować sterownik dla bazy SQLite, należy wpisać: > pear install MDB2_Driver_sqlite

Oto pełna lista dostępnych aktualnie sterowników: „ fbsql — Front Base „ ibase — InterBase „ mssql — MS SQL Server „ mysql — MySQL „ mysqli — system MySQL korzystający z rozszerzenia mysqli PHP; więcej informacji pod adresem: http://php.net/mysqli „ oci8 — Oracle „ pgsql — PostgreSQL „ querysim — Querysim „ sqlite — SQLite

19

PEAR. Programowanie w PHP

Łączenie się z bazą danych Aby połączyć się z wybraną bazą danych już po udanym zainstalowaniu pakietu MDB2 i modułu sterownika, konieczne będzie najpierw określenie nazwy źródła danych, DSN (Data Source Name). Nazwa DSN może mieć postać łańcucha lub tablicy i definiuje parametry połączenia z bazą danych takie jak: nazwa bazy danych, typ systemu RDBMS (systemu zarządzającego relacyjną bazą danych), nazwa użytkownika i hasło wykorzystywane do łączenia się z bazą danych itp.

Nazwa DSN jako tablica Jeśli nazwa źródła danych, DSN, jest definiowana w formie tablicy, będzie wyglądać mniej więcej tak: $dsn = array ( 'phptype' => 'mysql', 'hostspec' => 'localhost:3306', 'username' => 'user', 'password' => 'pass', 'database' => 'mdb2test' );

Oto lista różnych kluczy parametrów używanych w tablicy nazwy DSN: „ phptype — nazwa wykorzystywanego sterownika; innymi słowy: nazwa definiująca system RDBMS „ hostspec — (specyfikacja hosta) określa nazwę hosta, na którym działa baza danych; może przyjmować postać host:port lub też podawać tylko samą nazwę hosta, a port będzie wtedy definiowany osobno w kluczu port „ database — nazwa bazy danych, z którą się łączymy „ dbsyntax — jeśli używana składnia jest inna niż właściwa dla systemu phptype „ protocol — wykorzystywany protokół komunikacyjny, na przykład TCP „ socket — gniazdo, które należy określić, jeśli łączymy się za pośrednictwem gniazd „ mode — służy do definiowania trybu otwierania pliku bazy danych

Nazwa DSN jako łańcuch Szybszym i bardziej przyjaznym dla człowieka sposobem (gdy już się do niego przyzwyczaimy) jest definiowanie nazw DSN za pomocą łańcucha tekstowego wyglądającego podobnie do adresu URL. Zasadniczo, jego składnia wygląda tak: phptype://nazwa-użytkownika:hasło@specyfikacja-hosta/baza-danych

gdzie phptype to oczywiście typ systemu baz danych. Dla systemu MySQL łańcuch nazwy DSN może wyglądać na przykład tak: $dsn = 'mysql://user:pass@localhost:3306/mdb2test';

20

Rozdział 1. • MDB2

Więcej informacji na temat nazw DSN oraz inne przykłady prawidłowych łańcuchów DSN można znaleźć w podręczniku repozytorium PEAR, dostępnym pod adresem: http://pear.php.net/ ¦manual/en/package.database.mdb2.intro-dsn.php.

Tworzenie instancji obiektu MDB2 Istnieją trzy metody umożliwiające tworzenie (instancjację) obiektu MDB2: $mdb2 =& MDB2::connect($dsn); $mdb2 =& MDB2::factory($dsn); $mdb2 =& MDB2::singleton($dsn);

Metoda connect() tworzy obiekt i łączy się z bazą danych. Metoda factory() tworzy obiekt, natomiast połączenie utworzy dopiero, gdy będzie ono potrzebne. Wreszcie metoda singleton() działa podobnie jak metoda factory(), ale upewnia się, że istnieje tylko jeden obiekt MDB2 o danej nazwie źródła danych, DSN. Jeśli więc taki obiekt już istnieje, metoda zwraca ten obiekt, a jeśli nie, tworzy nowy. Istnieje też sposób „zakłócania” działania metody singleton() za pomocą metody setDatabase(), która pozwala określić, że bieżąca baza danych ma być inna niż ta określona w nazwie DSN. $dsn = 'mysql://root@localhost/mdb2test'; $mdb2_first =& MDB2::singleton($dsn); $mdb2_first->setDatabase('inna_db'); $mdb2_second =& MDB2::singleton($dsn);

W tym przypadku będziemy mieli dwie różne instancje MDB2. Wszystkie trzy wspomniane metody tworzą obiekt klasy sterownika bazy danych. Jeśli na przykład korzystamy ze sterownika bazy MySQL, zmienna $mdb zdefiniowana powyżej będzie instancją klasy MDB2_Driver_mysql.

Opcje Pakiet MDB2 udostępnia kilka opcji, które można definiować przywołując metody connect(), factory() lub singleton() lub też później korzystając z metody setOption() (by zdefiniować jedną opcję) lub setOptions() (by zdefiniować kilka opcji na raz). Na przykład: $options = array ( 'persistent' => true, 'ssl' => true, ); $mdb2 =& $MDB2::factory($dsn, $options);

lub $mdb2->setOption('portability', MDB2_PORTABILITY_NONE);

21

PEAR. Programowanie w PHP

Pełną listę dostępnych opcji można znaleźć w dokumentacji interfejsu API pakietu MDB2, dostępnej pod adresem: http://pear.php.net/package/MDB2/docs/. Przyjrzyjmy się teraz dwóm najważniejszym opcjom.

Opcja „persistent” Jest to opcja logiczna, która określa, czy utworzone połączenie powinno być połączeniem trwałym, czy też nie. W witrynie mysql.com można znaleźć bardzo dobry artykuł na temat zalet i wad korzystania z trwałych połączeń z bazą danych w systemie MySQL. Należy zajrzeć pod adres: http://www.mysql.com/news-and- events/ newsletter/2002-11/a0000000086.html.

Domyślnie przypisywana jest jej wartość false (fałsz), określająca, że połączenie nie powinno być trwałe. Podczas tworzenia obiektu można zmienić to domyślne ustawienie: $options = array ( 'persistent' => true ); $mdb2 =& MDB2::factory($dsn, $options);

Natomiast metoda setOption() pozwala definiować opcje już po utworzeniu obiektu: $mdb2->setOption('persistent', true);

Opcja „portability” Pakiet MDB2 próbuje poradzić sobie jakoś z różnicami w sposobie implementowania pewnych funkcji baz danych przez różne systemy RDBMS. Opcja portability pozwala określić, w jakim zakresie warstwa bazy danych ma dbać o przenośność naszych skryptów. Różne wartości opcji portability definiowane są jako stałe zaczynające się od MDB2_PORTABILITY_*, a domyślna wartość opcji to MDB2_PORTABILITY_ALL i oznacza „zrób wszystko, co tylko możliwe, by zagwarantować przenośność skryptów”. Pełną listę stałych dla opcji portability oraz ich opis można znaleźć pod adresem: http://pear.php.net/manual/en/package.database. ¦mdb2.intro-portability.php. Można definiować kilka wartości opcji portability, jak również definiować wyjątki za pomocą operatorów bitowych — dokładnie w taki sposób, w jaki definiuje się zasady raportowania błędów w języku PHP. To na przykład ustawienie poleca dbać o przenośność w pełnym zakresie, z wyjątkiem stosowania małych liter: MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_LOWERCASE

Jeśli natomiast nie interesują nas wszystkie funkcje przenośności oferowane przez MDB2, a chcielibyśmy tylko usunąć z wyniku spacje i zmienić puste wartości na łańcuchy null, to należy użyć opcji: 22

Rozdział 1. • MDB2

MDB2_PORTABILITY_RTRIM | MDB2_PORTABILITY_EMPTY_TO_NULL

Prawdopodobnie najlepszym rozwiązaniem będzie jednak pozostawienie domyślnego ustawienia MDB2_PORTABILITY_ALL. W ten sposób, w przypadku jakichś problemów z aplikacją, będziemy wiedzieli, że kod związany z sięganiem do bazy danych został dodatkowo sprawdzony pod kątem współpracy z różnymi systemami baz danych.

Definiowanie trybu pobierania danych Kolejnym ustawieniem, które warto zdefiniować na początku, jest tryb pobierania danych (ang. fetch mode) lub też innymi słowy — sposób, w jaki dane te będą nam zwracane. Możemy otrzymywać dane w postaci uporządkowanej listy (ustawienie domyślne), w formie tablic asocjacyjnych lub w formie obiektów. Oto przykłady definiowania trybu pobierania danych: $mdb2->setFetchMode(MDB2_FETCHMODE_ORDERED); $mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC); $mdb2->setFetchMode(MDB2_FETCHMODE_OBJECT);

Oczywiście najbardziej przyjaznym dla człowieka i najczęściej stosowanym trybem pobierania danych będzie pobieranie ich w tablicach asocjacyjnych, ponieważ wyniki umieszczane są wtedy w tablicy, której klucze odpowiadają nazwom kolumn tabeli w bazie danych. Aby pokazać, na czym polega różnica, przyjrzyjmy się różnym sposobom pobierania danych zawartych w naszym zbiorze wyników: echo $result[0]; // uporządkowana lub indeksowana tablica, domyślnie w MDB2 echo $result['name']; // tablica asocjacyjna echo $result->name; // obiekt

Jest jeszcze jeden tryb pobierania danych, o nazwie MDB2_FETCHMODE_FLIPPED (tryb odwrócony). Jest on cokolwiek nietypowy i jego działanie zostało w dokumentacji API pakietu MDB2 opisane w następujący sposób: „W przypadku wyników wielowymiarowych, zazwyczaj pierwszy poziom tablic jest numerem wiersza, podczas gdy drugi poziom jest indeksowany według nazwy bądź numeru kolumny. Tryb MDB2_FETCHMODE_FLIPPED odwraca ten porządek, w efekcie czego pierwszy poziom tablic będzie nazwą kolumny, a drugi poziom — numerem wiersza”.

Rozłączanie się z bazą danych Aby rozłączyć się z bazą danych, należy użyć następującego kodu: $mdb2->disconnect();

Niemniej nawet jeśli sami nie zaznaczymy w kodzie, że chcemy rozłączyć się z bazą danych, pakiet MDB2 zrobi to za nas automatycznie w swoim destruktorze. 23

PEAR. Programowanie w PHP

Korzystanie z MDB2 Gdy już połączymy się z bazą danych i określimy odpowiednie opcje połączenia oraz tryb pobierania danych, będzie można przystąpić do wykonywania zapytań. Na potrzeby przykładów prezentowanych w tym rozdziale założymy, że mamy tabelę o nazwie people (ludzie), z kolumnami id (identyfikator), name (imię), family (nazwisko) i birth_date (data_urodzenia): id

name

family

birth_date

1

Eddie

Vedder

1964-12-23

2

Mike

McCready

1996-04-05

3

Stone

Gossard

1966-07-20

Przykład Oto prosty przykład pokazujący, jak korzystać z MDB2. W dalszej części opowiemy o wszystkim szczegółowo, teraz jednak rzućmy okiem na kod, starając się zrozumieć w ogólnym zarysie, jak on działa.

Wykonywanie zapytań Aby wykonać zapytanie, można użyć metod query() lub exec(). Metoda query() zwraca obiekt wyniku MDB2_Result, natomiast metoda exec() zwraca liczbę wierszy w tabelach zmienionych przez zapytanie. Dlatego też metoda exec() będzie bardziej odpowiednia w przypadku zapytań, które modyfikują dane. Mimo iż metoda query() pozwala wykonać praktycznie każdą operację na bazie danych, MDB2 oferuje również inne metody, które lepiej nadają się do określonych, często wykonywanych operacji.

Pobieranie danych W przedstawionym wyżej przykładzie można znaleźć następujące wiersze: $sql = 'SELECT * FROM people'; $result = $mdb2->query($sql);

Zmienna $result jest obiektem wyniku typu MDB2_Result lub też, ściślej mówiąc, zależną od konkretnego sterownika bazy danych klasą, która jest rozszerzeniem typu MDB2_Result, na przykład MDB2_Result_mysql. Do przeglądania zbioru wyników można natomiast użyć w pętli metody fetchRow(), która pobiera pojedyncze wiersze. while ($row = $result->fetchRow()) { echo $row['name'], '
'; }

Za każdym razem gdy przywołujemy metodę fetchRow(), sprawdzi ona następny rekord i zwróci odwołanie (ang. reference) do zawartych w nim danych. Oprócz metody fetchRow() jest jeszcze parę innych metod z grupy fetch*(): „ fetchAll() zwraca od razu tablicę zawierającą wszystkie rekordy. „ fetchOne() jeśli zostanie przywołana bez żadnych parametrów, zwraca wartość pierwszego pola z bieżącego wiersza. Natomiast jeśli prześlemy jej odpowiednie parametry, będziemy mogli za jej pomocą pobrać dowolne pole z dowolnego wiersza. Na przykład wywołanie fetchOne(1,1) zwróci w naszym przykładzie imię Mike, czyli drugą kolumnę drugiego wiersza.

25

PEAR. Programowanie w PHP

„ fetchCol($column) zwraca pola w kolumnie o numerze $column dla wszystkich wierszy lub też pierwszą kolumnę, jeśli parametr $column nie zostanie określony. Warto zwrócić uwagę, że metody fetchRow() i fetchOne() przenoszą wewnętrzny wskaźnik do bieżącego rekordu, podczas gdy metody fetchAll() i fetchCol() przeniosą go na koniec zbioru wyników. Można również skorzystać z wywołania $result->nextResult(), by z jego pomocą przenieść wskaźnik do następnego rekordu w zbiorze wyników lub z wywołania $result-> seek($rownum), by przenieść wskaźnik do wiersza określonego w parametrze $rownum. W razie wątpliwości można też skorzystać z wywołania $result->rowCount(), by sprawdzić, w którym miejscu zbioru wyników aktualnie znajduje się nasz wskaźnik. Można również ustalić liczbę wierszy i liczbę kolumn w zbiorze wyników: $sql = 'SELECT * FROM people'; $result = $mdb2->query($sql); echo $result->numCols(); // wyświetla 4 echo $result->numRows(); // wyświetla 3

Skróty ułatwiające pobieranie danych Bardzo często znacznie wygodniej jest pobierać dane w formie tablicy asocjacyjnej (lub zdefiniować je jako preferowany tryb pobierania danych) i nie kłopotać się szczegółami technicznymi związanymi z przeglądaniem zbioru wyników. Pakiet MDB2 oferuje dwa zestawy metod umożliwiających pobieranie danych „na skróty”: metody z grupy query*() i metody z grupy get*(). Za ich pomocą następujące czynności wykonuje się przy użyciu pojedynczego wywołania metody: 1. Wykonywanie zapytania 2. Pobieranie zwracanych danych 3. Zwalnianie zasobów wykorzystywanych przez pobrany wynik

Skróty metod query*() W tej grupie mamy do dyspozycji metody queryAll(), queryRow(), queryOne() i queryCol(), które odpowiadają analogicznym metodom z grupy fetch*(), omówionym wyżej. Oto przykład ilustrujący, czym różni się korzystanie z metod z grupy query*() od metod fetch*(): // instrukcja SQL

$sql = 'SELECT * FROM people'; // jeden ze sposobów pobierania wszystkich danych

$result = $mdb2->query($sql); $data = $result->fetchAll(); $result->free(); // nie wymagane // krótszy sposób

$data = $mdb2->queryAll($sql);

26

Rozdział 1. • MDB2

W obu przypadkach, jeśli wyświetlimy za pomocą metody print_r() zawartość zmiennej $data i korzystamy z trybu pobierania używającego tablic asocjacyjnych, otrzymamy: Array ( [0] => Array ( [id] => 1 [name] => Eddie [family] => Vedder [birth_date] => 1964-12-23 ) [1] => Array ( [id] => 2 [name] => Mike [family] => McCready [birth_date] => 1966-04-05 ) ... )

Skróty metod get*() Oprócz metod z grupy query*() można jeszcze korzystać ze skrótów oferowanych przez metody get*(). Metody z grupy get*() generalnie zachowują się w taki sam sposób jak metody z grupy query*(), niemniej pozwalają również na stosowanie w zapytaniach parametrów. Rozważmy następujący przykład: $sql = 'SELECT * FROM people WHERE id=?'; $mdb2->loadModule('Extended'); $data = $mdb2->getRow($sql, null, array(1));

W tym przykładzie znak zapytania pojawiający się w instrukcji jest zmienną, która zostanie zastąpiona wartością przesłaną w trzecim parametrze metody getRow(). Można również korzystać z parametrów posiadających własne nazwy: $sql = 'SELECT * FROM people WHERE id=:the_id'; $mdb2->loadModule('Extended'); $data = $mdb2->getRow( $sql, null, array('the_id' => 1) );

Warto zwrócić uwagę na to, że metody get*() są częścią modułu Extended pakietu MDB2, co oznacza, że aby były dostępne, należy je najpierw załadować używając polecenia $mdb2-> loadModule('Extended'). Dzięki ładowaniu modułów mamy możliwość przeciążania obiektów, czego nie można było robić przed pojawieniem się PHP5. Dlatego w wersji PHP4 języka, aby sięgnąć do metod modułu Extended, trzeba je było przywoływać w następujący sposób: $mdb2->extended->getAll($sql);

27

PEAR. Programowanie w PHP

Natomiast obecnie wystarczy wpisać: $mdb2->getAll($sql);

getAsoc() Kolejną użyteczną metodą z grupy get*(), która nie ma bezpośredniego odpowiednika w grupie fetch*() ani w grupie query*(), jest metoda getAssoc(). Zwraca ona wyniki w podobny sposób jak metoda getAll(), niemniej kluczami w tablicy wyników będą wartości z pierwszej kolumny tabeli. Dodatkowo zbiór wyników zawiera (w naszym przykładzie) tylko dwie kolumny, ponieważ jedną wykorzystaliśmy już jako indeks tablicy. Druga kolumna zostanie zwrócona w formie łańcucha (a nie w formie tablicy z jednym elementem). Oto kilka przykładów ilustrujących różnice pomiędzy metodami getAll() i getAssoc(): $sql = 'SELECT id, name FROM people'; $mdb2->loadModule('Extended'); $data = $mdb2->getAll($sql);

Metoda getAll() zwróci uporządkowaną tablicę, w której każdy z elementów będzie tablicą asocjacyjną zawierającą wszystkie pola. Array ( [0] => Array ( [id] => 1 [name] => Eddie ) [1] => Array ( [id] => 2 [name] => Mike ) ... )

Jeśli wykonalibyśmy to samo zapytanie za pomocą metody getAssoc(), na przykład wpisując w kodzie $data=$mdb2->getAssoc($sql), to otrzymalibyśmy następujący wynik: Array ( [1] => Eddie [2] => Mike [3] => Stone )

Jeśli zapytanie zwraca więcej niż dwie kolumny, to każdy z wierszy będzie tablicą, a nie skalarem. Oto kod wykonujący takie zapytanie: $sql = 'SELECT id, name, family FROM people'; $mdb2->loadModule('Extended'); $data = $mdb2->getAssoc($sql);

A oto wynik: Array ( [1] => Array ( [name] => Eddie [family] => Vedder ) ... )

28

Rozdział 1. • MDB2

Typy danych Aby poradzić sobie z problemem wynikającym z tego, że różne systemy bazy danych obsługują różne typy danych dozwolone dla pól tabel, pakiet MDB2 dostarcza własnego, uniwersalnego zestawu typów danych. Programista może korzystać z typów danych oferowanych przez MDB2 i pozwolić, by sam pakiet MDB2 zadbał o przenośność typów danych między różnymi systemami RDBMS, po prostu mapując swoje typy na typy odpowiedniego systemu baz danych. Oto lista typów danych oferowanych przez MDB2 i ich domyślne wartości: $valid_types = array ( 'text' => '', 'boolean' => true, 'integer' => 0, 'decimal' => 0.0, 'float' => 0.0, 'timestamp' => '1970-01-01 00:00:00', 'time' => '00:00:00', 'date' => '1970-01-01', 'clob' => '', 'blob' => '', )

Więcej informacji na temat typów danych MDB2 można znaleźć w pliku datatypes.html, znajdującym się w podkatalogu docs w katalogu, w którym zainstalowaliśmy PEAR. Dokument ten jest również dostępny w internecie, w witrynie repozytorium PEAR CVS: http://cvs.php.net/viewcvs.cgi/pear/MDB2/docs/datatypes.html?view=co

Określanie typów danych We wszystkich metodach służących do pobierania danych, którym się do tej pory przyglądaliśmy (z grup query*(), fetch*() i get*()), można było określać typ zbioru wyników, który chcemy otrzymać, i pakiet MDB2 automatycznie konwertował wartości na odpowiedni typ danych. Na przykład metodzie query() można było przesłać jako drugi parametr tablicę zawierającą oczekiwane typy danych dla pól: $sql = 'SELECT * FROM people'; $types = array(); $result = $mdb2->query($sql, $types); $row = $result->fetchRow(); var_dump($row);

W tym przypadku tablica typów $types była pusta, więc metoda zachowała się w domyślny sposób (nie wykonując żadnej konwersji typów danych) i wszystkie wyniki zostały zwrócone w formie łańcuchów. Przykład ten zwraca następujące dane:

29

PEAR. Programowanie w PHP

array(2) { ["id"] => string(1) "1" ["name"]=> string(5) "Eddie" ... }

Możemy jednak zażyczyć sobie, aby pierwsze pole w każdym zwracanym rekordzie było typu integer (całkowitoliczbowego), a drugie typu text (tekstowego), definiując tablicę $type w następujący sposób: $types = array('integer', 'text');

W tym przypadku otrzymamy następujący wynik: array(2) { ["id"]=> int(1) ["name"]=> string(5) "Eddie" ... }

Podczas określania typów można również użyć tablicy asocjacyjnej, w której kluczami będą poszczególne pola tabeli. W takim przypadku można nawet pominąć niektóre pola, jeśli nie chcemy dla nich definiować typów. Oto przykłady poprawnych definicji takiej tablicy: $types = array( 'id' => 'integer', 'name' => 'text' ); $types = array('name'=>'text'); $types = array('integer');

Określanie typów danych podczas pobierania wyników Jeśli nie chcemy określać typów danych już podczas przywoływania metody query(), możemy to zrobić później. Zanim rozpoczniemy pobieranie danych, możemy określić typy danych używając metody setResultTypes(). // wykonujemy zapytanie

$sql = 'SELECT * FROM people'; $result = $mdb2->query($sql); // pobieramy pierwszy wiersz bez konwertowania typów

$row = $result->fetchRow(); var_dump($row['id']); // wynik będzie następujący: string(1) "1" // określamy typy

$types = array('integer'); $result->setResultTypes($types);

30

Rozdział 1. • MDB2

// wszystkie następne pobrania będą konwertować // pierwszą kolumnę na liczbę całkowitą

$row = $result->fetchRow(); var_dump($row['id']); // wynik będzie następujący: int(2)

Określanie typów danych dla metod get*() i query*() Wszystkie metody z grup get*() i query*(), które omawialiśmy wyżej w tym rozdziale, pozwalają na określanie w drugim przesyłanym im parametrze, jakie typy danych mają zwracać. Dokładnie tak samo jak w metodzie query(). Możemy definiować parametr przesyłający typy danych nie tylko jako tablicę $types = array('integer'), ale również jako łańcuch $types = 'integer'. Jest to wygodne, kiedy pracujemy z metodami, które mają zwracać tylko jedną kolumnę danych, takimi jak getOne(), queryOne(), getCol() czy queryCol(), niemniej należy bardzo ostrożnie korzystać z tej techniki w przypadku metod typu *All() i *Row(), ponieważ wówczas łańcuch w parametrze podającym typ określi typ dla wszystkich pól w zbiorze danych.

Ujmowanie wartości i identyfikatorów w cudzysłowy Różne systemy RDBMS zarządzające relacyjnymi bazami danych stosują różne konwencje ujmowania danych w cudzysłowy (na przykład tam gdzie jedne używają pojedynczych cudzysłowów ', inne stosują podwójne cudzysłowy "). Nie ma też żadnej powszechnie przyjętej konwencji ujmowania w cudzysłowy typów danych. Na przykład w systemie MySQL możemy (jeśli chcemy) ujmować wartości typu integer (całkowitoliczbowe) w cudzysłowy, natomiast w innych systemach może to być zabronione. Z tego powodu właśnie lepiej pozostawić obsługę cudzysłowów warstwie abstrakcji bazy danych, ponieważ pakiet MDB2 „wie”, jakie konwencje stosują tutaj różne systemy baz danych. Pakiet MDB2 oferuje specjalną metodę quote(), która umożliwia ujmowanie danych w cudzysłowy, i metodę quoteIdentifier() pozwalającą na ujmowanie w cudzysłowy nazw baz danych, tabel i pól. Wszelkie cudzysłowy wstawione przez pakiet MDB2 będą odpowiednie dla wykorzystywanego systemu RDBMS. Oto przykład: $sql = 'UPDATE %s SET %s=%s WHERE id=%d'; $sql = sprintf( $sql, $mdb2->quoteIdentifier('people'), $mdb2->quoteIdentifier('name'), $mdb2->quote('Eddie'), // domniemany typ danych $mdb2->quote(1, 'integer') // wyraźnie określony typ danych );

Jeśli teraz w bazie MySQL wykonamy polecenie echo $sql, otrzymamy: UPDATE `people` SET `name`='Eddie' WHERE id=1

31

PEAR. Programowanie w PHP

Natomiast w bazie SQLite ten sam kod zwróci: UPDATE "people" SET "name"='Eddie' WHERE id=1

Jak można było zauważyć, przyglądając się poprzednim przykładom, metoda quote() pozwala na przesłanie jej drugiego, opcjonalnego parametru, określającego typ danych (oczywiście typ MDB2), który ma zostać ujęty w cudzysłowy. Jeśli pominiemy drugi parametr, MD2 postara się zgadnąć typ danych.

Iteratory Pakiet MDB2 korzysta ze standardowej biblioteki PHP (Standard PHP Library http://php.net/spl) i implementuje interfejs Iterator, który pozwala na znacznie prostsze i wygodniejsze przeglądanie wyników zapytania: foreach ($result as $row) { var_dump($row); }

W każdej kolejnej iteracji (powtórzeniu) pętli zmienna $row zawierać będzie kolejny rekord (wiersz), przechowywany w formie tablicy. Prezentowany tu kod jest równoważny przywołaniu w pętli metody fetchRow(): while ($row = $result->fetchRow()) { var_dump($row); }

Aby skorzystać z zalet interfejsu iteratorów Iterator, konieczne będzie załączenie pliku Iterator.php z katalogu pakietu MDB2, przy użyciu metody loadFile(): MDB2::loadFile('Iterator');

Po załadowaniu tego pliku będzie można przesyłać metodzie query() jako czwarty parametr nazwę klasy Iterator: $query = 'SELECT * FROM people'; $result = $mdb2->query($query, null, true, 'MDB2_BufferedIterator');

Pakiet MDB2 oferuje dwie klasy iteratorów: „ MDB2_Iterator — Klasa ta implementuje klasę Iterator biblioteki SPL i najlepiej sprawdza się w pracy z niebuforowanymi zbiorami wyników. „ MDB2_BufferedIterator — Ta klasa jest rozszerzeniem klasy MDB_Iterator i implementuje interfejs SeekableIterator. Podczas pracy z buforowanymi zbiorami wyników (domyślnymi w MDB2) lepiej jest korzystać z klasy MDB2_BufferedIterator, ponieważ oferuje ona parę dodatkowych przydatnych w tej sytuacji metod, takich jak count() czy rewind().

32

Rozdział 1. • MDB2

Wyszukiwanie błędów Pakiet MDB2 umożliwia programiście przechowywanie listy wszystkich zapytań wykonywanych w danej instancji obiektu MDB2, znacznie ułatwiając wyszukiwanie błędów w naszych aplikacjach. Aby włączyć tę opcję wyszukiwania błędów (debugowania), trzeba przypisać opcji debug dodatnią liczbę całkowitą. $mdb2->setOption('debug', 1);

Po włączeniu jej w ten sposób będziemy mogli w dowolnym momencie sięgnąć do zbieranych przez pakiet MDB2 danych debugowania: $mdb2->getDebugOutput();

Można również włączyć opcję log_line_break, która pozwala określić, w jaki sposób będą oddzielane od siebie dane zapisywane w dzienniku debugowania. Domyślnie odseparowane są znakiem nowego wiersza \n. Oto prosty przykład, w którym włączona została opcja debug i określony odpowiedni separator, następnie wykonano kilka zapytań, a na koniec pobrano nieuporządkowaną listę tych zapytań z zanotowanych w dzienniku danych debugowania. $mdb2->setOption('debug', 1); $mdb2->setOption('log_line_break', "\n\t"); $sql = 'SELECT * FROM people'; $result = $mdb2->query($sql); $sql = 'SELECT * FROM people WHERE id = 1'; $result = $mdb2->query($sql); $sql = 'SELECT name FROM people'; $result = $mdb2->query($sql); $debug_array = explode("\n\t", trim($mdb2->getDebugOutput())); echo '

  • '; echo implode('
  • ', $debug_array); echo '
';

Przykład ten zwróci następującą listę zapytań: „ query(1): SELECT * FROM people „ query(1): SELECT * FROM people WHERE id = 1 „ query(1): SELECT name FROM people Gdy już aplikacja wejdzie w fazę produkcyjną, warto ustawić poziom debugowania (rejestrowania informacji w dzienniku) jako 0, aby uniknąć niepotrzebnego zaśmiecania dziennika debugowania informacjami o wszystkich wykonywanych zapytaniach.

33

PEAR. Programowanie w PHP

Warstwa abstrakcji kodu SQL w MDB2 Sposoby implementowania różnych funkcji SQL i składni SQL w różnych systemach obsługiwanych przez pakiet MDB2 mogą być bardzo odmienne. Pakiet MDB2 stara się obudować te różnice najlepiej jak to możliwe, by móc zaoferować programiście jednolity interfejs pozwalający na wygodne sięganie do tych funkcji. Dzięki temu programista nie musi się przejmować szczegółami implementacji różnych funkcji SQL w systemie bazy danych, z którym przychodzi mu pracować.

Sekwencje Standardowym i wygodnym sposobem definiowania (oraz aktualizowania) identyfikatorów pełniących funkcję kluczy głównych tabeli są pola z opcją automatycznego zwiększania identyfikatora (ang. auto-increment fields), dzięki czemu wartości identyfikatorów tworzą rosnący ciąg (lub potocznie mówiąc: sekwencję). Niestety nie wszystkie systemy RDBMS obsługują opcję automatycznego zwiększania (autoinkrementacji) identyfikatora. Aby poradzić sobie z tą niespójnością między różnymi systemami baz danych, pakiet MDB2 wykorzystuje pomysł tzw. tablic sekwencji (ang. sequence tables). Polega to na tym, że pakiet MDB2 tworzy i aktualizuje nową tabelę (o którą programista nie musi się martwić) służącą do przechowywania i zwiększania ostatniej, najwyższej wartości identyfikatora. Uzyskana za jej pomocą nowa, zwiększona wartość identyfikatora będzie następnie wstawiana do właściwej tabeli danych, która powinna zawierać autoinkrementowany identyfikator. Załóżmy, że tabela people, którą wykorzystujemy w przykładach w tym rozdziale, jest pusta. Zanim wstawimy dane do tej tabeli, będziemy potrzebować następnego w kolejności identyfikatora. W tym celu przywołamy metodę nextId(), która zwróci nam nowy identyfikator: $my_new_id = $mdb2->nextId('people');

Teraz zmienna naszego nowego identyfikatora $my_new_id ma wartość 1, a za kulisami pakiet MDB2 tworzy nową tabelę o nazwie people_seq, posiadającej tylko jedno pole sequence oraz tylko jeden wiersz przechowujący wartość 1. Gdy następnym razem przywołamy metodę $mdb2>nextId('people'), pakiet MDB2 zwiększy wartość przechowywaną w tabeli people_seq i zwróci nam wartość 2. sequence 1

Metodzie nextId() można przesłać jako parametr dowolny identyfikator. Pakiet NDB2 doda do identyfikatora końcówkę _seq i tworzy nową tabelę o tej nazwie, o ile oczywiście taka tabela już nie istnieje. Jeżeli z jakichś powodów nie jest nam potrzebna inna nazwa, najlepiej

34

Rozdział 1. • MDB2

używać identyfikatora będącego nazwą tabeli, do której wstawiamy zwiększoną wartość — poprawi to czytelność kodu. Mimo iż domyślna nazwa pola w tabeli sekwencji to sequence, można ją zmienić, definiując odpowiednio opcję seqcol_name: $mdb2->setOption('seqcol_name', 'id');

Dodatkowo można też dostosować do własnych potrzeb nazwę tabeli sekwencji, zmieniając wartość opcji seqname_format. Domyślna przypisana jej wartość to %s_seq, gdzie %s zastępowane jest nazwą identyfikatora, który przesyłamy metodzie nextId().

Określanie limitów zapytań W bazach MySQL można ograniczyć liczbę rekordów zwracanych przez zapytanie używając instrukcji LIMIT. Na przykład to zapytanie zwróci tylko dwa pierwsze rekordy: SELECT * FROM people LIMIT 0, 2;

Instrukcja LIMIT jest specyficzna dla systemu MySQL, więc w innych systemach baz danych może być nieobecna lub też może być implementowana w zupełnie inny sposób. Aby obudować różnice między implementacją tej funkcji i dostarczyć programiście jednolitego interfejsu umożliwiającego ograniczanie liczby zwracanych wyników zapytania, pakiet MDB2 udostępnia specjalną metodę setLimit(). Oto przykład jej użycia: $sql = 'SELECT * FROM people'; $mdb2->setLimit(2); $result = $mdb2->query($sql);

Jeśli chcemy zdefiniować przesunięcie (od którego rekordu zaczynać pobieranie, gdy narzucamy limit), wartość przesunięcia możemy określić w drugim parametrze: $mdb2->setLimit(2, 1);

Warto wiedzieć, że innym sposobem ograniczenia liczby zwracanych wyników jest skorzystanie z metody limitQuery() modułu Extended. W tym przypadku zamiast najpierw ustalać limit, a potem dopiero wykonywać zapytanie, możemy to wszystko zrobić w jednym wywołaniu metody. Na przykład, aby pobrać dwa rekordy zaczynając od przesunięcia 1, należy wpisać: $mdb2->loadModule('Extended'); $sql = 'SELECT * FROM people'; $result = $mdb2->limitQuery($sql, null, 2, 1);

Ograniczenie określone w metodzie limitQuery() nie będzie miało wypływu na zapytania wykonywane po niej, a sama metoda, podobnie jak metoda query(), zwraca obiekt typu MDB2_Result.

35

PEAR. Programowanie w PHP

Zastępowanie zapytań System MySQL obsługuje, oprócz standardowych instrukcji aktualizacji UPDATE i wstawiania INSERT danych, również instrukcje zastępowania danych REPLACE. Instrukcja REPLACE aktualizuje rekordy, które już istnieją, a tam gdzie rekord nie istnieje, wstawia nowy. Korzystanie z instrukcji REPLACE może jednak generować w naszej aplikacji problemy z przenośnością kodu i dlatego pakiet MDB2 obudowuje tę funkcję za pomocą metody replace(). Metodę replace() wywołuje się przesyłając jej nazwę tabeli i tablicę danych na temat rekordów. $fields=array ( 'id' => array ( 'value' => 6, 'key' => true ), 'name' => array ('value' => 'Stoyan'), 'family' => array ('value' => 'Stefanov'), 'birth_date' => array ('value' => '1975-06-20') ); $mdb2->replace('people', $fields);

Jak widać, dane zapisywane w tabeli zostają określone za pomocą kluczy asocjacyjnych value. Ponadto określiliśmy, że kolumna id będzie kluczem tabeli, dzięki czemu (jeśli nie ma możliwości bezpośredniego korzystania z funkcji REPLACE) pakiet MDB2 może sprawdzić, czy rekord o danym identyfikatorze istnieje. Jeśli mamy klucz tabeli, który składa się z kilku pól, to należy każde z nich określić jako pole (kolumnę) klucza, wpisując 'key' => true index. Inne elementy tabeli, z których można korzystać, to: „ type — pozwalający określić typ danych MDB2 „ null — (może przyjmować wartość true lub false, prawda lub fałsz) pozwalający określić, czy należy korzystać z wartości null, ignorując zawartość klucza asocjacyjnego value. Metoda replace() zwraca liczbę wierszy, które zmienia lub tworzy, podobnie jak metoda exec(). Z technicznego punktu widzenia operacja zastępowania jest operacją wstawiania, jeśli rekord nie istnieje, lub operacją usuwania rekordu i wstawiania nowego, jeśli istnieje. Dlatego też metoda replace() będzie zwracać dla pojedynczego zastąpionego rekordu wartość 1, jeśli rekord nie istniał wcześniej, i wartość 2, jeśli aktualizowała już istniejący rekord.

Obsługa subselektów Osobnym zagadnieniem związanym z przenośnością kodu SQL jest obsługa subselektów (ang. sub-selects) lub inaczej podzapytań SELECT. Aby wykonać takie podzapytanie, należy przesłać metodzie subSelect() łańcuch zapytania. W tym przypadku, jeśli system baz danych obsługuje subselekty, wynikiem będzie to samo, niezmienione zapytanie. Jeśli natomiast system nie obsługuje subselektów, to metoda wykona zapytanie i zwróci dzieloną średnikami listę wyników zapytania. Ważne, aby pamiętać, że zapytanie, które przesyłamy metodzie subSelect(), zwraca tylko jedną kolumnę wyników. Oto przykład: 36

Rozdział 1. • MDB2

// łańcuch zapytania subselektu

$sql_ss = 'SELECT id FROM people WHERE id = 1 OR id = 2'; // główne zapytanie

$sql = 'SELECT * FROM people WHERE id IN (%s)'; // przywołujemy subSelect()

$subselect = $mdb2->subSelect($sql_ss); // aktualizujemy i wyświetlamy główne zapytanie

echo $sql = sprintf($sql, $subselect); // wykonujemy zapytanie

$data = $mdb2->queryAll($sql);

Jeśli system baz danych obsługuje podzapytania SELECT, to instrukcja echo w kodzie powyżej zwróci następujące informacje: SELECT * FROM people WHERE id IN (SELECT id FROM people WHERE id = 1 OR id = 2)

Jeśli nie, instrukcja echo zwróci: SELECT * FROM people WHERE id IN (1, 2)

Warto pamiętać, ze metoda subSelect() nie jest idealnym zastąpieniem subselektów, a tylko emuluje tak zwane podzapytania nieskorelowane (ang. non-correlated sub queries). Oznacza to, że nasze subselekty oraz główne zapytanie powinny dać się uruchomić jako osobne zapytania, tak aby podzapytanie nie odwoływało się do wyników zwróconych przez główne zapytanie i vice versa.

Instrukcje preparowane Instrukcje preparowane (ang. prepared statements) są wygodną i bardzo bezpieczną metodą zapisywania danych w bazie. Niemniej tak jak w przypadku innych omawianych tutaj funkcji, nie wszystkie systemy baz danych obsługują instrukcje preparowane. Dlatego też pakiet MDB2 emuluje dla nas tę funkcję, jeśli system RDBMS jej nie oferuje. Potrzebne są następujące elementy składowe: „ Bazowe, ogólne zapytanie, zawierające zamiast rzeczywistych danych odpowiednie zmienne przechowujące. Zapytanie to jest przesyłane metodzie prepare(). „ Jakieś dane na temat rekordów, które mają zostać wstawione, zaktualizowane lub usunięte. „ Odwołanie do metody execute(), która wykonuje spreparowaną instrukcję. Bazowe zapytanie może wykorzystywać zarówno zmienne przechowujące bez własnych nazw, jak i posiadające nazwy. Na przykład: $sql = 'INSERT INTO people VALUES (?, ?, ?, ?)';

lub też: $sql = 'INSERT INTO people VALUES (:id, :first_name, :last_name, :bdate)';

37

PEAR. Programowanie w PHP

Następnie przywołujemy metodę prepare(), zwracającą instancję klasy MDB2_Statement_* odpowiednią dla sterownika bazy danych, z którego korzystamy: $statement = $mdb2->prepare($sql);

Jeśli korzystamy z parametrów bez nazw (znaków zapytania), to konieczne będzie uporządkowanie danych za pomocą tablicy: $data = array( $mdb2->nextId('people'), 'Matt', 'Cameron', '1962-11-28' );

Następnie przesyłamy dane metodzie execute() klasy MDB2_Statement: $statement->execute($data);

Na koniec wreszcie zwalniamy wykorzystywane zasoby: $statement->free();

Używanie parametrów z nazwami Jeśli w bazowym, ogólnym zapytaniu używamy parametrów posiadających nazwy, to przesyłając dane możemy korzystać z wygodnych tablic asocjacyjnych i nie zaprzątać uwagi porządkiem parametrów, tak jak to było w przypadku używania parametrów bez nazw. Oto przykład poprawnego zdefiniowania danych dla zapytania wykorzystującego parametry posiadające własne nazwy: $data = array( 'first_name' => 'Jeff', 'last_name' => 'Ament', 'id' => $mdb2->nextId('people'), 'bdate' => '1963-03-10' );

Wiązanie danych Kolejnym sposobem przygotowywania danych dla instrukcji preparowanych jest skorzystanie z wiążącej dane metody bindParam(). Oto przykład: // przygotowujemy instrukcję

$sql = 'INSERT INTO people VALUES (:id, :first_name, :last_name, :bdate)'; $statement = $mdb2->prepare($sql); // ustalamy dane

$id = $mdb2->nextId('people'); $first_name = 'Kirk'; $last_name = 'Hammett'; $bdate = '1962-11-18';

38

Rozdział 1. • MDB2

// wiążemy dane

$statement->bindParam('id', $id); $statement->bindParam('first_name', $first_name); $statement->bindParam('last_name', $last_name); $statement->bindParam('bdate', $bdate); // wykonujemy zapytanie i zwalniamy dane

$statement->execute(); $statement->free();

Jedną z rzeczy, którą warto pamiętać o metodzie bindParam(), jest to, że pobiera ona jako parametr odwołanie do zmiennej zawierającej dane. Jeśli wstawiamy kilka nowych rekordów i w związku z tym przywołujemy metodę execute() wielokrotnie, nie musimy przywoływać metody bindParam() dla każdej metody execute(). Wystarczy przywołać ją raz, a potem można po prostu modyfikować zawartość zmiennych z danymi (w tym przypadku: $id, $first_name, $last_name i $bdate). Jeśli jednak chcielibyśmy zachować rzeczywistą wartość już w momencie wiązania, to należy zamiast metody bindParam() skorzystać z metody bindValue(). Kolejnym sposobem dostarczania danych przed wykonaniem instrukcji preparowanej jest skorzystanie z metody bindParamArray(), która pozwala na powiązanie od razu wszystkich parametrów. W kodzie poprzedniego przykładu należałoby w tym celu zastąpić cztery wywołania metody bindParam()jednym wywołaniem metody bindParamArray(): $array_to_bind = array('id' => $id, 'first_name' => $first_name, 'last_name' => $last_name, 'bdate' => $bdate ); $statement->bindParamArray($array_to_bind);

Wykonywanie wielu wierszy na raz Gdy już mamy instrukcję preparowaną, możemy za jej pomocą wstawiać kilka wierszy za jednym zamachem, korzystając z metody executeMultiple(). Metoda ta również pochodzi z modułu Extended pakietu MDB2, tak więc najpierw trzeba go załadować. Dane dla niej należy zdefiniować w formie wielowymiarowej tablicy, w której każdy z elementów najwyższego poziomu tablicy będzie jednym rekordem. $sql = 'INSERT INTO people VALUES (?, ?, ?, ?)'; $statement = $mdb2->prepare($sql); $data = array( array($mdb2->nextId('people'), 'Janusz', 'Halski', '1969-06-06'), array($mdb2->nextId('people'), 'Lech', 'Uliasz', '1968-02-02') ); $mdb2->loadModule('Extended'); $mdb2->executeMultiple($statement, $data); $statement->free();

39

PEAR. Programowanie w PHP

Metoda autoPrepare() Zamiast pisać ogólne zapytanie i następnie preparować za jego pomocą instrukcję, można zlecić to zadanie metodzie autoPrepare(), która zrobi to za nas automatycznie. Należy jej przesłać tylko nazwę tabeli, tablicę z nazwami pól i typ zapytania (wstawianie INSERT, aktualizację UPDATE lub usuwanie DELETE). Jeśli wykonujemy aktualizację lub usuwanie, możemy również przesłać jej dodatkowy warunek WHERE w postaci łańcucha lub tablicy zawierającej różne warunki, które pakiet MDB2 sklei następnie dla nas za pomocą operatora AND. Oto przykład automatycznego preparowania instrukcji INSERT: $mdb2->loadModule('Extended'); $table = 'people'; $fields = array('id', 'name', 'family', 'birth_date'); $statement = $mdb2->autoPrepare($table, $fields, MDB2_AUTOQUERY_INSERT);

W ten sposób otrzymamy utworzony z ogólnego zapytania obiekt MDB2_Statement, który wyglądać będzie następująco: INSERT INTO people (id, name, family, birth_date) VALUES (?, ?, ?, ?)

Jeśli natomiast potrzebować będziemy instrukcji aktualizacji, to należy użyć mniej więcej takiego kodu: $mdb2->loadModule('Extended'); $table = 'people'; $fields = array('name', 'family', 'birth_date'); $where = 'id = ?'; $statement = $mdb2->autoPrepare($table, $fields, MDB2_AUTOQUERY_UPDATE, $where);

Przygotuje on ogólne zapytanie o następującej formie: UPDATE people SET name = ?, family = ?, birth_date = ? WHERE id = ?

Wewnętrznie metoda autoPrepare() korzysta z metody buildManipSQL(), która wykonuje prawie całą pracę związaną z tworzeniem ogólnego zapytania, tylko nie przywołuje metody prepare(), gdy już zapytanie zostanie zbudowane. Metoda ta przydaje się w sytuacjach, gdy po prostu potrzebujemy zapytania i nie zamierzamy korzystać z preparowanych instrukcji. Oto przykład, jak usunąć z tabeli wszystkie rekordy związane z ludźmi, których nazwiska zaczynają się od litery S lub s: $mdb2->loadModule('Extended'); $sql = $mdb2->buildManipSQL( 'people', false, MDB2_AUTOQUERY_DELETE, 'family like "s%"'); echo $mdb2->exec($sql);

40

Rozdział 1. • MDB2

Metoda autoExecute() Metoda autoExecute() działa podobnie jak metoda autoPrepare() z tą różnicą, że od razu wykonuje spreparowaną instrukcję. Kolejna różnica polega na tym, że przesyłana jej w parametrach tablica pól powinna być tablicą asocjacyjną zawierającą zarówno nazwy pól, jak i dane, które mają zostać wstawione lub aktualizowane. $mdb2->loadModule('Extended'); $table = 'people'; $fields = array ( 'id' => $mdb2->nextId('people'), 'name' => 'Ryszard', 'family' => 'Burton', 'birth_date' => '1962-02-10' ); $result = $mdb2->autoExecute($table, $fields, MDB2_AUTOQUERY_INSERT);

Transakcje Jeśli używany przez nas system RDBMS obsługuje transakcje, to warto z nich korzystać, pozwalają one bowiem utrzymać spójność danych, jeśli w trakcie zachowywania kilku elementów danych w jednej lub więcej tabelach wystąpi jakiś błąd. Przede wszystkim trzeba zacząć od sprawdzenia, czy używany system zarządzania relacyjnymi bazami danych (RDBMS) obsługuje transakcje, a następnie należy inicjować transakcję za pomocą metody beginTransaction(). Teraz rozpoczynamy wykonywanie różnych zapytań składających się na transakcję. Po każdym zapytaniu możemy sprawdzić jego rezultat i jeśli wykryjemy błąd (PEAR_Error), będziemy mogli wycofać (ang. roll back) całą transakcję, a więc wszystkie wcześniejsze zapytania wchodzące w jej skład. Jeśli natomiast wszystko pójdzie gładko, będziemy mogli zatwierdzić (ang. commit) transakcję, by ją sfinalizować. Przed przywołaniem metod rollback() lub commit() trzeba będzie jeszcze sprawdzić, czy rzeczywiście jesteśmy w obrębie transakcji, używając metody inTransaction(). if ($mdb2->supports('transactions')) { $mdb2->beginTransaction(); } $result = $mdb2->exec('DELETE FROM people WHERE id = 33'); if (PEAR::isError($result)) { if ($mdb2->inTransaction()) { $mdb2->rollback(); } } $result = $mdb2->exec('DELETE FROM people WHERE id = invalid something');

41

PEAR. Programowanie w PHP

if (PEAR::isError($result)) { if ($mdb2->inTransaction()) { $mdb2->rollback(); } } elseif ($mdb2->inTransaction()) { $mdb2->commit(); }

Warto pamiętać, że jeśli używany przez nas system RDBMS nie obsługuje transakcji, pakiet MDB2 nie emuluje dla nas tej funkcji, w związku z czym to na nas spoczywać będzie obowiązek troski o zachowanie spójności danych.

Moduły MDB2 Przyglądając się przykładom prezentowanym wyżej w tym rozdziale, czytelnicy zorientowali się już zapewne, w jaki sposób działa modułowa konstrukcja pakietu MDB2. Głównym celem takiej konstrukcji jest zachowanie maksymalnej prostoty i szybkości działania podstawowych funkcji, a jednocześnie dostarczanie na żądanie dodatkowych funkcji ładowanych za pomocą metody loadModule(). Wyżej w tym rozdziale ładowaliśmy moduł Extended w następujący sposób: $mdb2->loadModule('Extended');

Po wykonaniu tego wywołania będziemy mieli dostęp do wszystkich metod oferowanych przez moduł Extended, takich jak wszystkie metody z grupy get*(). Do metod tych można sięgać za pośrednictwem właściwości extended instancji mdb2: $mdb2->extended->getAssoc($sql);

Oprócz tego w PHP5, dzięki możliwości przeciążania (ang. overloading) obiektów, możemy sięgać do tych metod bezpośrednio, tak jak gdyby były metodami instancji $mdb2: $mdb2->getAssoc($sql);

W tym rozdziale zostało przyjęte założenie, że korzystamy z PHP5, dlatego przywołując metody modułów stosować będziemy tę właśnie krótszą notację dzięki możliwości przeciążania obiektów. Kolejnym sposobem sięgania do metod modułu jest poprzedzanie ich nazw skrótową nazwą modułu, w tym przypadku ex (od „Extended”). Ta technika również dostępna jest dopiero w PHP5. $mdb2->exGetAssoc($sql);

42

Rozdział 1. • MDB2

Na koniec wreszcie można określić własną nazwę właściwości, do której zostanie załadowany moduł (ta opcja dostępna jest zarówno w PHP4, jak i w PHP5): $mdb2->loadModule('Extended', 'mine'); $mdb2->mine->getAssoc($sql);

Oto pełna lista dostępnych obecnie modułów pakietu MDB2 (skrótowe nazwy zostały podane w nawiasach): „ Extended (ex) — Zaprezentowaliśmy już część z metod dostępnych w module Extended. Jest to jedyny moduł nie związany z różnymi sterownikami baz danych, a jego plik definicji (Extended.php) zlokalizowany jest w głównym katalogu pakietu, MDB2, a nie w podkatalogu sterowników Drivers. Moduł jest definiowany w klasie MDB2_Extended, która dziedziczy (jest wywiedziona) z klasy MDB2_Module_Common. „ Datatype (dt) — Zawiera metody umożliwiające wykonywanie operacji na typach danych MDB2 oraz konwertowanie ich i mapowanie na typy właściwe dla odpowiedniego systemu bazy danych. „ Manager (mg) — Zawiera metody umożliwiające zarządzanie strukturą (tzw. schematem) bazy danych, czyli tworzeniem, usuwaniem i wyświetlaniem list baz danych, tabel, indeksów itp. „ Reverse (rv) — Zawiera metody umożliwiające przeprojektowywanie (ang. reverse engineering) struktury istniejącej bazy danych. „ Native (na) — Zawiera wszystkie metody specyficzne (natywne) dla wykorzystywanego systemu baz danych. „ Function (fc) — Zawiera metody implementujące rozmaite użyteczne funkcje, które są implementowane w różny sposób w poszczególnych systemach baz danych. Pora teraz na kilka przykładów wykorzystania niektórych z wymienionych modułów.

Moduł Manager Dzięki modułowi Manager uzyskujemy dostęp do metod, które umożliwiają zarządzanie schematem naszej bazy danych. Przyjrzyjmy się działaniu niektórych z nich.

Tworzenie bazy danych Oto przykład tworzenia nowej, pustej bazy danych: $mdb2->loadModule('Manager'); $mdb2->createDatabase('test_db');

43

PEAR. Programowanie w PHP

Tworzenie tabeli Możemy wykorzystać moduł Manager, by na nowo utworzyć tabelę ludzi people, którą wykorzystywaliśmy we wcześniejszych przykładach w tym rozdziale. Jak pamiętamy, tabela ta ma cztery pola: „ id — Identyfikator, klucz główny będący liczbą całkowitą bez znaku i nie mogący przyjmować wartości null. „ name — Imię osoby, wartość tekstowa taka jak typ VARCHAR(255) w bazie MySQL. „ family — Nazwisko osoby, pole takiego samego typu jak name. „ birth_date — Data urodzenia, pole typu data. Aby utworzyć tę tabelę, należy skorzystać z metody createTable(), której przesyłamy nazwę tabeli i tablicę zawierającą definicję tejże tabeli: $definition = array ( 'id' => array ( 'type' => 'integer', 'unsigned' => 1, 'notnull' => 1, 'default' => 0, ), 'name' => array ( 'type' => 'text', 'length' => 255 ), 'family' => array ( 'type' => 'text', 'length' => 255 ), 'birth_date' => array ( 'type' => 'date' ) ); $mdb2->createTable('people', $definition);

Modyfikowanie tabeli Załóżmy, że tabela jest już utworzona, ale stwierdziliśmy, że 255 znaków to zdecydowanie zbyt dużo, jak na potrzeby imienia. W takim przypadku będziemy musieli przygotować nową tablicę z definicją tabeli i przywołać metodę alterTable(). Nowa tablica definicji wykorzystywana do zmodyfikowania tabeli powinna być podzielona na następujące klucze asocjacyjne: „ name — Ten klucz podaje nową nazwę tabeli „ add — Ten klucz określa nowe pola (kolumny), które powinny zostać dodane do tabeli „ remove — Określa pola, które powinny zostać usunięte z tabeli „ rename — Określa pola, których nazwy chcemy zmienić „ change — Określa pola, które chcemy zmodyfikować

44

Rozdział 1. • MDB2

Oto przykład kodu modyfikującego pole name naszej przykładowej tabeli, tak aby miało długość tylko stu znaków: $definition = array( 'change' => array( 'name' => array( 'definition' => array( 'length' => 100, 'type' => 'text', ) ), ) ); $mdb2->alterTable('people', $definition, false);

Jeśli przypiszemy trzeciemu parametrowi metody alterTable() wartość true (prawda), to pakiet MDB2 nie wprowadzi zmian w tabeli, a tylko sprawdzi, czy wprowadzenie tych zmian jest możliwe w wykorzystywanym przez nas systemie zarządzania bazą danych (DBMS).

Ograniczenia Pole id tabeli powinno być jej kluczem głównym (ang. primary key), niemniej jak dotąd nie pełni jeszcze tej funkcji. Aby to naprawić, trzeba skorzystać z tworzącej ograniczenia metody createConstraint(), która wymaga przesłania jej jako parametrów: nazwy tabeli; nazwy, którą wybraliśmy dla ograniczenia; i tablicy z definicją tego ograniczenia. $definition = array ( 'primary' => true, 'fields' => array ( 'id' => array()) ); $mdb2->createConstraint('people', 'myprimekey', $definition); Warto wiedzieć, że system MySQL zignoruje nazwę ograniczenia myprimkey, ponieważ wymaga, aby klucz główny tabeli miał zawsze nazwę PRIMARY.

Teraz możemy ustalić, że imię (ang. name) i nazwisko (ang. family) powinny być unikatowe: $definition = array('unique' => true, 'fields' => array('name' => array(), 'family' => array(), ) ); $mdb2->createConstraint('people', 'unique_people', $definition);

Po zastanowieniu jednak dochodzimy do wniosku, że są przecież ludzie mający takie same imiona i nazwiska, więc usuwamy to ograniczenie: $mdb2->dropConstraint('people', 'unique_people');

45

PEAR. Programowanie w PHP

Indeksy Jeśli w polu daty urodzenia birth_date wykonywane jest bardzo wiele zapytań, można je przyśpieszyć, tworząc indeks dla tego pola. $definition = array('fields' => array('birth_date' => array(),) ); $mdb2->createIndex('people', 'dates', $definition);

Warto zauważyć, że domyślnie pakiet MDB2 dodaje do wszystkich indeksów i ograniczeń przyrostek _idx. Aby to zmodyfikować, należy zmienić ustawienie opcji idxname_format: $mdb2->setOption('idxname_format', '%s'); // bez przyrostka

Listy Moduł Manager zawiera także wiele metod, które umożliwiają zdobywanie informacji na temat baz danych takich jak: lista baz danych listDatabases(), lista użytkowników listUsers(), lista tabel listTables(), lista perspektyw listTableViews() czy lista pól listTableFields().

Moduł Function Moduł Function zawiera metody pozwalające na sięganie do typowych pomocniczych funkcji bazy danych, takich jak możliwość odwoływania się do bieżącego znacznika czasu czy sklejanie lub pobieranie częściowych łańcuchów. Sięganie w instrukcjach do bieżącego znacznika czasu umożliwia metoda now(), która zwraca znacznik czasu w formacie właściwym dla używanego przez nas systemu baz danych. $mdb2->loadModule('Function'); echo $mdb2->now();

Ten kod zwróci na przykład CURRENT_TIMESTAMP, jeśli korzystamy z bazy MySQL, albo datetime('now'), jeśli korzystać będziemy z bazy SQLite. Metoda now() pozwala na przesłanie jej jako parametru łańcucha (z wartościami date — data, time — czas i timestamp — znacznik czasu), który określać będzie bieżącą datę, aktualny czas lub obie te informacje. Jeśli natomiast chcielibyśmy sklejać w naszych instrukcjach łańcuchy w sposób, który byłby niezależny od używanej bazy danych, to należy skorzystać z metody concat(), której można przesłać w parametrach dowolną liczbę łańcuchów do sklejenia. Do wydobywania składowych podłańcuchów służy natomiast metoda substring(). Oto przykład ilustrujący korzystanie z obu metod: $mdb2->loadModule('Function'); $sql = 'SELECT %s FROM people'; $first_initial = $mdb2->substring('name', 1, 1);

46

Rozdział 1. • MDB2

$dot = $mdb2->quote('.'); $all = $mdb2->concat($first_initial, $dot, 'family'); $sql = sprintf($sql, $all); $data = $mdb2->queryCol($sql); echo $sql; print_r($data);

Metoda print_r() użyta w tym przykładzie zwróci następujące informacje: Array ( [0] => E.Vedder [1] => M.McCready [2] => S.Gossard ... )

Natomiast wiersz echo $sql wyświetli inny wynik, zależny od sterownika bazy danych, z którego korzystamy. Dla bazy MySQL będzie to: SELECT CONCAT(SUBSTRING(name FROM 1 FOR 1), '.', family) FROM people

Z kolei, jeśli korzystać będziemy ze sterownika bazy Oracle (oci8), otrzymamy: SELECT (SUBSTR(name, 1, 1) || '.' || family) FROM people

W tym przykładzie został wydobyty tylko pierwszy znak z wartości w polu name. Następnie został sklejony ze znakiem kropki i pełną wartością z pola family, w efekcie czego otrzymaliśmy nazwisko z inicjałem imienia. Warto zwrócić uwagę na to, że kropka musiała zostać ujęta w cudzysłów, aby instrukcja traktowała ją jak łańcuch, a nie jak nazwę pola.

Moduł Reverse Jeśli chcemy pobrać informacje na temat tabeli people, której używaliśmy w przykładach prezentowanych w tym rozdziale, możemy skorzystać z metody tableInfo(): $mdb2->loadModule('Reverse'); $data = $mdb2->tableInfo('people');

Jeśli wyświetlimy wynik za pomocą metody print_r(), to otrzymamy mniej więcej takie informacje: Array( [0] => Array ( [table] => people [name] => id [type] => int [length] => 11 [flags] => not_null primary_key [mdb2type] => integer )

47

PEAR. Programowanie w PHP

[1] => Array ( [table] => people [name] => name [type] => char [length] => 100 [flags] => [mdb2type] => text ) ... )

Podstawowa zaleta metody tableInfo() polega na tym, że potrafi zwracać informacje na temat pól bazując na wynikach zapytania, a nie tylko na tabeli. Załóżmy, że mamy w bazie danych jeszcze jedną tabelę o nazwie phones, która przechowuje numery telefonów ludzi z tabeli people (oraz identyfikatory telefonów id i identyfikatory ludzi person_id) i która wygląda mniej więcej tak: id

person_id

Phone

1

1

555-666-7777

2

1

555-666-7788

Możemy uruchomić zapytanie, które wykona złączenie obu tabel, i otrzymany wynik przesłać metodzie tableInfo(): $mdb2->loadModule('Reverse'); $sql = $sql.= $sql.= $sql.=

'SELECT phones.id, people.name, phones.phone '; ' FROM people '; ' LEFT JOIN phones '; ' ON people.id = phones.person_id ';

$result = $mdb2->query($sql); $data = $mdb2->tableInfo($result);

Teraz jeśli wyświetlimy wynik zwrócony przez metodę tableInfo() za pomocą metody print_r(), to otrzymamy tabelę, która opisuje trzy pola wybrane w instrukcji złączenia JOIN — id (identyfikator), name (imię) i phone (telefon): Array ( [0] => Array ( [table] => phones [name] => id [type] => int [length] => 11 [flags] => primary_key [mdb2type] => integer ) [1] => Array ( [table] => people [name] => name [type] => char

48

Rozdział 1. • MDB2

[length] => 100 [flags] => [mdb2type] => text ) [2] => Array ( [table] => phones [name] => phone [type] => char [length] => 20 [flags] => [mdb2type] => text ) )

Własne rozszerzenia pakietu MDB2 Działanie pakietu MDB2 można bez trudu modyfikować, nie tylko korzystając z jego licznych opcji konfiguracyjnych, ale również dodając własny kod. Na przykład możemy obudować wyniki zapytań za pomocą naszych własnych klas i korzystać z nich podczas wykonywania zapytań lub pobierania danych. Możemy utworzyć własny mechanizm obsługi błędów, przygotować własną implementację iteratora Iterator i na koniec wreszcie możemy dodawać do pakietu nowe funkcje, tworząc własne moduły. W tej części rozdziału opowiemy, jak tworzyć własne, nowe funkcje pakietu MDB2.

Własny mechanizm rejestracji w dzienniku Wiemy już, że po przypisaniu opcji debug dodatniej liczby całkowitej: $mdb2->setOption('debug', 1);

będziemy mogli później w dowolnym momencie otrzymać listę wykonanych zapytań: $mdb2->getDebugOutput();

Warto jednak wiedzieć, że możemy również przygotować własny mechanizm rejestracji w dzienniku, który tworzyć będzie listę zapytań. Przygotowany przez nas mechanizm obsługi błędów może być funkcją lub metodą klasy — zasadniczo może być dowolnym elementem kodu, będącym poprawnym pseudotypem funkcji zwrotnej PHP (ang. callback, patrz http://php.net/callback/). Utwórzmy nową klasę, która będzie odpowiedzialna za zbieranie, a następnie wyświetlanie informacji debugowania. W naszym przykładzie dodamy do tego mechanizmu jeszcze licznik rejestrujący, ile razy dane zapytanie zostało wykonane. Opcja ta może się przydać podczas badania wydajności bardziej rozbudowanych aplikacji. Bardzo często w aplikacji umieszczamy część jej funkcjonalności w różnych pomocniczych klasach i metodach, a potem przywołujemy je, nie zastanawiając się, jak dokładnie one działają. Jest to jedna z zalet programowania

49

PEAR. Programowanie w PHP

obiektowego. Często jednak zdarza się, że niektóre z metod komunikujących się z bazą będą przywoływane częściej niż inne podczas standardowej pracy z aplikacją i bazą danych. Dzięki naszemu mechanizmowi rejestracji informacji w dzienniku będzie nam łatwiej odnajdywać te metody i potem ewentualnie je optymalizować. class Custom_Debug_Class { // zliczamy, ile zapytań zostało wykonanych

var $query_count = 0; // które zapytania i ich liczba

var $queries = array(); // ta metoda przywoływana jest przy każdym zapytaniu

function collectInfo(&$db, $scope, $message, $is_manip = null) { $this->query_count++; @$this->queries[$message]++; } // wyświetlamy dziennik

function dumpInfo() { echo 'Całkowita liczba zapytań na tej stronie: '; echo $this->query_count; // sortujemy malejąco

arsort($this->queries); echo ''; print_r($this->queries); echo ''; } }

Aby przekonać się, jak zaprogramowany przez nas mechanizm działa w praktyce, konieczne będzie utworzenie instancji nowo przygotowanej klasy, określenie, że jest funkcją zwrotną, i wykonanie kilku zapytań (jedno z nich wykonamy dwukrotnie). Na koniec wyświetlimy informacje zapisane w dzienniku: $my_debug_handler = new Custom_Debug_Class(); $mdb2->setOption('debug', 1); $mdb2->setOption('debug_handler', array($my_debug_handler, 'collectInfo')); $sql = 'SELECT * FROM people'; $result = $mdb2->query($sql); $sql = 'SELECT * FROM people WHERE id = 1'; $result = $mdb2->query($sql); $my_debug_handler->dumpInfo();

50

Rozdział 1. • MDB2

Wynik tych operacji będzie następujący: Całkowita liczba zapytań na tej stronie: 2 Array ( [SELECT * FROM people]=>1 [SELECT * FROM people WHERE id = 1]=>1 )

W fazie programowania naszej aplikacji możemy nawet zarejestrować metodę dumpInfo(), sprawiając, by była przywoływana automatycznie, na zakończenie każdego skryptu: register_shutdown_function(array($my_debug_handler, 'dumpInfo'));

Oto przykład monitorowania wydajności naszych zapytań MySQL za pomocą pakietu MDB2. Możemy utworzyć nową metodę (i zarejestrować ją, aby była wykonywana na zakończenie każdego skryptu) w naszej klasie debugowania. Metoda ta będzie wybierała wyłącznie zapytania SELECT i uruchamiała je ponownie, poprzedzając je instrukcją EXPLAIN. Następnie będziemy mogli wykonać kilka dodatkowych testów wykrywających zapytania, które używają nieprawidłowych indeksów. Więcej na ten temat można znaleźć w opisie instrukcji EXPLAIN pod adresem: http://dev.mysql.com/doc/refman/5.0/en/explain.html.

Własne klasy pobierające dane Jak już wiemy, pobierając dane z wyniku zapytania, możemy korzystać z różnych trybów pobierania danych — tablicy asocjacyjnej, uporządkowanej listy lub obiektu. Kiedy korzystamy z trybu pobierania używającego obiektów (MDB2_FETCHMODE_OBJECT), dla każdego wiersza tworzona jest instancja standardowej klasy PHP (stdClass) — po prostu wykonywane jest rzutowanie tablicy wyników na obiekt. Dzięki temu będziemy mogli sięgać do danych z pól tablicy, odwołując się do właściwości obiektu, na przykład wpisując $row->name, by pobrać imię, lub $row->id, by pobrać identyfikator. Pakiet MDB2 pozwala zmodyfikować sposób pobierania danych za pomocą obiektów, umożliwiając tworzenie własnej klasy służącej do pobierania danych. Każdy wiersz ze zbioru wyników będzie przesyłany konstruktorowi naszej klasy w postaci tablicy. Utwórzmy prostą klasę, która naśladować będzie w działaniu standardową klasę stdClass — z tą tylko różnicą, że będzie konwertować wartości z pola identyfikatora id na liczby całkowite. class My_Fetch_Class { function __construct($row) { foreach ($row as $field => $data) { if ($field == 'id')

51

PEAR. Programowanie w PHP

{ $data = (int)$data; } $this->{$field} = $data; } } }

Aby przetestować tę klasę, trzeba oczywiście zmienić tryb pobierania danych na MDB2_FETCHMODE_OBJECT, a opcji definiującej klasę pobierania fetch_class przypisać nazwę naszej nowej klasy. $mdb2->setFetchMode(MDB2_FETCHMODE_OBJECT); $mdb2->setOption('fetch_class', 'My_Fetch_Class');

Ten sam efekt można uzyskać bezpośrednio przywołując metodę setFetchMode(). $mdb2->setFetchMode(MDB2_FETCHMODE_OBJECT, 'My_Fetch_Class');

Jeśli teraz wykonamy zapytanie w następujący sposób: $sql = 'SELECT * FROM people WHERE id=1'; $data = $mdb2->queryRow($sql);

po czym wyświetlimy wynik za pomocą metody var_dump(), to otrzymamy: object(My_Fetch_Class)#3 (2) { ["id"]=> int(1) ["name"]=> string(5) "Eddie" ... }

Warto również wiedzieć, że rdzeń pakietu MDB2 zawiera klasę MDB2_Row, która robi prawie dokładnie to samo, co nasza własna przykładowa klasa, z tą tylko różnicą, że nie konwertuje wartości pól id na liczby całkowite. Gdybyśmy zbudowali naszą własną klasę pobierania danych na bazie tej właśnie klasy MDB2_Row, moglibyśmy dodatkowo korzystać z oferowanych przez nią możliwości.

Własne klasy wyników Jak już się dowiedzieliśmy, po wykonaniu instrukcji SQL za pomocą metody query() otrzymamy obiekt odpowiedniej klasy MDB2_result. Jeśli korzystalibyśmy z systemu MySQL, byłaby to klasa MDB2_Result_mysql, rozszerzająca typową funkcjonalność klasy wyników oferowaną przez standardową klasę MDB2_Result_Common, która z kolei jest rozszerzeniem klasy MDB2_Result.

52

Rozdział 1. • MDB2

Pakiet MDB2 pozwala programistom na rozszerzanie i dostosowywanie do własnych potrzeb klas wyników lub innymi słowy na zastępowanie lub rozszerzanie klas z grupy MDB2_Result_* za pomocą własnych klas. W tym celu programista musi: „ Utworzyć swoją własną klasę wyników „ Upewnić się, że załączył jej definicję „ Przesłać jej nazwę pakietowi MDB2 w odpowiedniej opcji Utwórzmy teraz klasę o nazwie MyResult, która będzie naszym własnym rozszerzeniem klasy MDB2_Result_mysql, dzięki czemu będziemy mogli korzystać z możliwości oferowanych przez tę standardową klasę. Klasę tę wzbogacimy o prostą metodę, która demonstruje jej działanie: class MyResult extends MDB2_Result_mysql { function newResultMethod() { echo 'To jest MyResult::newResultMethod()'; // $this->db to nasza bieżąca instancja MDB2 // na wypadek, gdybyśmy jej potrzebowali

} }

Następnie trzeba uczynić naszą klasę dostępną dla pakietu MDB2, przypisując jej nazwę odpowiedniej opcji: $mdb2->setOption('buffered_result_class', 'MyResult');

Teraz możemy wykonać zapytanie i przywołać na zwróconych przez nie wynikach naszą nową metodę: $sql = 'SELECT * FROM people'; $result = $mdb2->query($sql); $result->newResultMethod();

Jak widzieliśmy wcześniej, włączona została opcja buforowanej klasy wyników buffered_result_ class. Zrobiliśmy tak, ponieważ pakiet MDB2 domyślnie korzysta z buforowanych zapytań. Można to jednak zmienić za pomocą następującego kodu: $mdb2->setOption('result_buffering', false);

Po tej zmianie, gdy pracujemy z niebuforowanymi wynikami, jeśli będziemy chcieli użyć własnej klasy wyników, konieczne będzie włączenie opcji result_class, a nie jak poprzednio buffered_result_class: $mdb2->setOption('result_class', 'MyResult');

53

PEAR. Programowanie w PHP

Aby utworzyć własne klasy wyników, specyficzne dla określonych systemów baz danych, warto dodawać na końcu ich nazw końcówkę właściwą dla odpowiedniego systemu baz danych (na przykład MyResult_mysql). Dodatkowo, podczas definiowania naszej własnej klasy w opcji można używać w miejsce nazwy sterownika odpowiedniej zmiennej przechowującej: $mdb2->setOption('result_class', 'MyResult_%s');

Pakiet MDB2 automatycznie zastąpi tę zmienną nazwą odpowiedniego sterownika bazy danych, używanego przez bieżącą instancję MDB2. Przyjrzyjmy się kolejnemu, odrobinę bardziej złożonemu przykładowi. Tym razem chcemy utworzyć w naszej klasie wyników nową metodę, która będzie wyliczała przecięty wiek ludzi uwzględnionych w tym zapytaniu. Oto kod naszej nowej klasy — MyResult2. class MyResult2 extends MDB2_BufferedResult_mysql { function getAverageAge() { $current_row = $this->rowCount(); // gdzie jesteśmy $this->seek(); // przewijamy $total_ts = 0; // suma wszystkich znaczników czasu dat urodzenia while ($row = $this->fetchRow(MDB2_FETCHMODE_ASSOC)) { $total_ts += strtotime($row['birth_date']); } $avg_ts = $total_ts / $this->numRows(); // średnia wartość znacznika czasu

$age = date('Y') - date('Y', $avg_ts); if (date('md') < date('md', $avg_ts)) { $age--; // to jeszcze nie data urodzenia } $this->seek($current_row); // wracamy tam, gdzie byliśmy return $age; } }

Aby skorzystać z tak przygotowanej własnej klasy wyników, możemy również (oprócz przypisania nazwy tej klasy do odpowiedniej opcji pakietu MDB2) określać ją przy każdym zapytaniu, przesyłając naszą klasę jako trzeci parametr metodzie query(). W ten sposób będziemy mogli w większości zapytań używać domyślnej klasy wyników, a naszej własnej używać tylko w wybranych zapytaniach. Aby użyć nowej klasy, należy w kodzie wpisać: $sql = 'SELECT * FROM people'; // lub może --> $sql = 'SELECT * FROM people WHERE name // LIKE "J%"';

$result = $mdb2->query($sql, null, 'MyResult2'); echo $result->getAverageAge();

54

Rozdział 1. • MDB2

W implementacji metody getAverageAge(), ustalającej przeciętny wiek, można zauważyć, że zmienna $this odnosi się do obiektu wyników. Metoda najpierw pobiera pozycję wskaźnika zbioru wyników, przywołując w tym celu metodę $this->rowCount(). Następnie używa odwołania do metody seek(), by przenieść się na początek zbioru wyników. Zanim metoda zakończy działanie, wraca do miejsca, w którym był wskaźnik zbioru wyników, przed jej przywołaniem. Jest to bardzo wygodne rozwiązanie, ponieważ dzięki temu możemy nawigować w przód i w tył w zbiorze wyników — do miejsca, w którym byliśmy przed wywołaniem metody getAverageAge() — bez zakłócania całej funkcjonalności kodu. W przeciwnym razie, gdyby okazało się, że przed przywołaniem metody getAverageAge() pobraliśmy już jakieś wiersze, wskaźnik do bieżącego wiersza byłby już przesunięty i metoda wyliczyłaby średnią tylko z części pobranych wyników. Teraz gdy zbiór rekordów zostanie zresetowany, pobieramy po prostu wszystkie rekordy, sumujemy znaczniki czasu dla wszystkich dat urodzenia i wykonujemy odpowiednie operacje na dacie, by uzyskać przeciętny wiek. Warto zwrócić uwagę, że klasa MyResult2 rozszerza buforowaną klasę z pakietu MDB2, w przeciwnym bowiem razie nie mogłaby korzystać z metod seek() i numRow().

Własne iteratory Jak już się dowiedzieliśmy, pakiet MDB2 oferuje dwie implementacje interfejsu Iterator ze standardowej biblioteki SPL języka PHP5 — MDB2_Iterator i MDB2_BufferedIterator. Czytelników nie zaskoczy zapewne również wiadomość, że możemy przygotowywać własne implementacje interfejsu Iterator. W kolejnym przykładzie tworzymy klasę My_Iterator. Została ona zbudowana na bazie klasy MDB2_BufferedIterator. // ładujemy iteratory MDB2

MDB2::loadFile('Iterator'); // własna klasa iteratora

class My_Iterator extends MDB2_BufferedIterator { function foo() { echo 'bar'; } } // wykonujemy zapytanie

$sql = 'SELECT * FROM people'; $result = $mdb2->query($sql, null, true, 'My_Iterator'); // przeglądamy krok po kroku zbiór wyników

foreach ($result as $row) { var_dump($row); } // przywołujemy własną metodę

$result->foo();

55

PEAR. Programowanie w PHP

Własne moduły Czytelnicy, którym nie wystarczają wszystkie opisane wcześniej możliwości rozwijania pakietu MDB2 i którzy chcieliby uzupełnić go o jakieś pominięte w nim funkcje, ucieszą się zapewne z informacji, że możemy również tworzyć własne moduły MDB2, wychodząc od sześciu już istniejących modułów (Extended, Manager, Reverse, Function Datatype i Native). Będą one naszym własnym rozszerzeniem pakietu MDB2, ale nadal będzie je można dołączać używając tej samej metody loadModule() i zachowywać się będą jak część pakietu MDB2. Oto opis czynności, które należy wykonać, by przygotować własny moduł, a następnie go używać. Najpierw utworzymy klasę, której nazwa zaczynać się będzie od przedrostka MDB2_. W tym przypadku nazwiemy ją MDB2_Mymodule. class MDB2_Mymodule { function sayHi() { echo "OK, witam!"; } }

Następnie umieszczamy tę klasę w pliku o nazwie opartej na nazwie modułu, czyli Mymodule.php, i kopiujemy go do katalogu, w którym ładująca moduły metoda MDB2::loadModule() poszukuje modułów pakietu MDB2 — czyli do utworzonego przez nas katalogu MDB2 umieszczonego na naszej ścieżce dołączanych plików. Można byłoby również umieścić ten plik gdzieś w głównym katalogu pakietu MDB2 w katalogu instalowanych modułów PEAR. Lepiej jednak zostawić zarządzanie katalogiem PEAR tylko instalatorowi PEAR. Aby rzecz całą uprościć, założymy, że utworzony przez nas katalog MDB2 będzie podkatalogiem tego samego katalogu, w którym zlokalizowany jest skrypt korzystający z naszego nowego modułu. Następnie w tymże testowym skrypcie po prostu załadujemy przygotowany moduł w taki sam sposób, jak każdy ze standardowych modułów pakietu MDB2, i wywołamy jego metodę: $mdb2->loadModule('Mymodule'); $mdb2->sayHi();

Voilà! Właśnie utworzyliśmy i przetestowaliśmy nasz pierwszy własny moduł pakietu MDB2.

Kolejny moduł, Mymodule2 Zazwyczaj jednak we własnym module umieszcza się bardziej złożone funkcje niż tylko wyświetlenie prostego komunikatu za pomocą instrukcji echo. Najprawdopodobniej potrzebny nam będzie dostęp do bieżącej instancji MDB2. Oto drugi przykład modułu będącego rozszerzeniem klasy MDB2_Module_Common, który pobiera odwołanie do bieżącego obiektu MDB2 (za pomocą metody getDBInstance()), aby wykonać na bazie danych pewną operację — obliczenie liczby wierszy w bieżącej tabeli. 56

Rozdział 1. • MDB2

class MDB2_Mymodule2 extends MDB2_Module_Common { function getNumberOfRecords($table) { $mdb2 =& $this->getDBInstance(); $sql = 'SELECT count(*) FROM ' . $mdb2->quoteIdentifier($table); $count = $mdb2->queryOne($sql); return $count; } }

Jeśli umieścimy ten kod w pliku o nazwie Mymodule2.php w naszym katalogu MDB2, to będziemy go mogli następnie łatwo przetestować: $mdb2->loadModule('Mymodule2'); echo $mdb2->getNumberOfRecords('people');

Pakiet MDB2_Schema MDB2_Schema to osobny pakiet PEAR zbudowany na bazie pakietu MDB2 i dostarczający narzędzi umożliwiających zarządzanie schematem bazy danych za pomocą niezależnego od platformy i bazy danych formatu XML. Format XML, odziedziczony z pakietu Metabase, jest bardzo czytelny i zrozumiały. Korzysta on prawdę powiedziawszy tylko z części możliwości oferowanych przez XML, z jego uproszczonej wersji zwanej SML (Simplified Markup Language). Dokładny opis formatu Metabase można znaleźć w folderze docs w naszym katalogu repozytorium PEAR, w pliku o nazwie xml_schemadocumentation.html. Można go również odczytać bezpośrednio z repozytorium PEAR CVS, zaglądając pod adres: http://cvs.php.net/ ¦viewcvs.cgi/pear/MDB2_Schema/docs/. Pakiet MDB2_Schema oferuje kilka dodatkowych metod ułatwiających zarządzanie strukturą naszej bazy danych i śledzenie zmian wprowadzonych w niej w trakcie korzystania z aplikacji. Przyjrzyjmy się kilku przykładom.

Instalowanie i tworzenie instancji Ponieważ MDB2_Schema to osobny pakiet, trzeba go będzie zainstalować niezależnie. W tym celu należy wpisać: > pear install MDB2_Schema

57

PEAR. Programowanie w PHP

Do tworzenia instancji klasy schematu bazy danych Schema można wykorzystać metody connect() i factory(), które podobnie jak metody tworzące instancję klasy w pakiecie MDB2 przyjmują jako parametry nazwę źródła danych DSN i tablicę z opcjami. Kolejny sposób polega na utworzeniu obiektu MDB2 z wykorzystaniem istniejącego obiektu MDB2, jeśli już nim dysponujemy. require_once 'MDB2.php'; require_once 'MDB2/Schema.php'; $dsn = 'mysql://root@localhost/test_db'; $options = array('debug' => 0,); $mdb2 =& MDB2::factory($dsn, $options); $mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC); $schema =& MDB2_Schema::factory($mdb2);

Tworzenie kopii bazy danych Jeśli chcemy przygotować kopię naszej bazy danych w pliku wykorzystującym format XML, możemy skorzystać z metody dumpDatabase(). Przyjmuje ona jako parametr tablicę definiującą bazę danych podobną do tablicy definicji, z którymi zetknęliśmy się już w tym rozdziale przy okazji omawiania modułu Manager. Jeśli nie mamy gotowej tablicy z definicją, możemy zdać się na obiekt Schema, który spróbuje za nas ustalić tę definicję, korzystając z metody getDefinitionFromDatabase(). Oto kod, który się tym zajmie, zakładając oczywiście, że mamy już obiekt schematu Schema: $definition = $schema->getDefinitionFromDatabase(); $dump_options = array ( 'output_mode' => 'file', 'output' => 'test.xml' ); $schema->dumpDatabase($definition, $dump_options, MDB2_SCHEMA_DUMP_STRUCTURE);

Po wykonaniu tego kodu na naszej przykładowej, przygotowanej wcześniej bazie danych test_db, która posiada jedną tabelę o nazwie people, a następnie wyświetlimy za pomocą metody print_r() tablicę $definition, otrzymamy wynik zbliżony do tutaj zaprezentowanego tutaj (cytujemy tylko część): Array ( [name] => test_db [create] => 1 [overwrite] => [tables] => Array ( [people] => Array (

58

Rozdział 1. • MDB2

[fields] => Array ( [id] => Array ( [type] => integer [notnull] => 1 [length] => 4 [unsigned] => 1 [default] => 0 ) [name] => Array ( [type] => text [notnull] => [length] => 100 [fixed] => [default] => ) ... ) [indexes] => Array (...) ) ) [sequences] => Array ( ) )

Kod ten utworzy również (w katalogu, w którym znajduje się skrypt) plik test.xml o następującej zawartości (tak jak poprzednio cytujemy tylko część, usunięte zostały na przykład puste wiersze):

test_db true false people

id integer true 4 true 0

name text

59

PEAR. Programowanie w PHP

100 false

...

...



Plik test.xml został utworzony, ponieważ zostało to określone w tabeli z opcjami $dump_ options, przesyłanej metodzie dumpDathabase(). Jeśli nie chcemy zapisywać pliku XML w systemie plików, ale nadal potrzebny nam jest do innych celów, to możemy pominąć klucz $dump_ options['output_mode'] i następnie podać nazwę funkcji w kluczu $dump_options['output']. W tym przypadku przygotowany kod XML zostanie przesłany w postaci łańcucha do odpowiedniej, określonej przez nas funkcji. Aby obejrzeć tak przygotowaną kopię bazy danych (w kodzie XML), wystarczy przygotować prostą funkcję, podobną do tej: function printXml($input) { echo ''; print_r(htmlentities($input)); echo ''; }

Następnie będzie można przygotować tabelę $dump_options: $dump_options = array ( 'output' => 'printXml' );

Trzeci parametr przesyłany metodzie dumpDatabase() informuje metodę, czego właściwie zapasową kopię chcemy przygotować — struktury bazy danych, danych przechowywanych w bazie, czy i tego i tego. Definiujemy to za pomocą jednej z trzech stałych: „ MDB2_SCHEMA_DUMP_STRUCTURE (strukturę) „ MDB2_SCHEMA_DUMP_CONTENT (dane) „ MDB2_SCHEMA_DUMP_ALL (wszystko) Dokumentacja API informuje, że: Metoda getDefinitionFromDatabase() jest próbą ustalenia definicji bezpośrednio na podstawie bazy danych, więc otrzymana za jej pomocą definicja może wymagać jeszcze kilku dodatkowych poprawek.

60

Rozdział 1. • MDB2

Zmienianie bazy danych Załóżmy, że zdecydujemy się przenieść naszą aplikację obecnie korzystającą z bazy MySQL do bazy SQLite (lub po prostu zależy nam na przetestowaniu, czy nasza aplikacja daje się przenosić miedzy różnymi systemami baz danych). W takim przypadku możemy zlecić obiektowi schematu MDB2_Schema przeniesienie za nas struktury i danych ze starej bazy do nowej. Powiedzmy, że przygotowaliśmy kopię bazy danych, tak jak to zostało pokazane przed chwilą, i mamy odpowiedni plik test.xml. W takiej sytuacji wszystko, co będzie nam potrzebne, to nowa nazwa DSN umożliwiająca połączenie się z bazą SQLite. Potem trzeba będzie przywołać metodę, która dokona analizy pliku XML i pobierze z niego definicję bazy danych, a następnie kolejną metodę, która utworzy nową bazę danych. $dsn2 = 'sqlite:///'; $schema2 =& MDB2_Schema::factory($dsn2); $definition = $schema2->parseDatabaseDefinitionFile('test.xml'); $schema2->createDatabase($definition);

W tym prostym przykładzie nie potrzebowaliśmy tak naprawdę pliku XML i moglibyśmy wykonać przeniesienie aplikacji do innej bazy, korzystając tylko z tablicy zawierającej definicję bazy danych. Całe przeniesienie mogło być wykonane w jednym wierszu, zakładając oczywiście, że mamy już gotowe dwie instancje Schema (schematu): $schema2->createDatabase($schema->getDefinitionFromDatabase());

Podsumowanie Ten rozdział był wprowadzeniem do MDB2, warstwy abstrakcji bazy danych. Zaznajomiliśmy się z problemami, które musi rozwiązywać warstwa abstrakcji bazy danych, oraz dowiedzieliśmy się, jak radzi sobie z nimi pakiet MDB2. Nauczyliśmy się, jak instalować pakiet MDB2, jak tworzyć instancję obiektu MDB2 oraz jak korzystać z najbardziej popularnych metod. Rozdział ten wyjaśniał również, w jaki sposób zbudowany jest pakiet MDB2 oraz z jakich modułów się składa. Pokazaliśmy w nim również, w jaki sposób rozwijać MDB2, tworząc dla niektórych zadań własne klasy i w jaki sposób tworzyć własne rozszerzenia funkcjonalności pakietu. Na koniec wreszcie zaprezentowaliśmy prosty przykład ilustrujący, jak za pomocą obiektu MDB2_Schema zarządzać bazą danych niezależnie od systemu RDBMS, z którego korzystamy.

61

PEAR. Programowanie w PHP

62

2 Wyświetlanie danych Jednym z podstawowych zastosowań internetu jest prezentowanie danych. Niezależnie od tego, czy wyświetlamy w naszej osobistej witrynie WWW daty urodzin naszych przyjaciół, tworzymy interfejs administrowania dla portalu WWW, czy prezentujemy szefowi rozbudowany arkusz kalkulacyjny, wszystko sprowadza się w ostateczności do pobrania danych z odpowiedniego źródła, przetworzenia ich, a następnie sformatowania, by wyglądały zgodnie z naszymi oczekiwaniami. Gdy trzeba tworzyć i formatować dane, wielu programistów implementuje swoje własne skrypty lub klasy, by rozwiązywać te same podstawowe problemy. Istnieje oczywiście wiele sposobów implementowania prezentacji danych, niemniej niestety wiele typowych implementacji nie działa poprawnie lub też funkcjonuje nieefektywnie. Starając się rozwiązać niektóre problemy, programiści często tworzą nie w pełni dopracowane rozwiązania i następnie przenoszą do programowania innych elementów aplikacji, pozostawiając niekompletny kod, który może działać niezbyt wydajnie lub być źródłem kłopotów związanych z bezpieczeństwem. Na szczęście repozytorium PEAR dostarcza kilku pakietów, które zajmują się różnymi aspektami prezentacji danych. Wykonują nie tylko od ręki formatowanie prezentowanych danych, ale umożliwiają również programistom rozbudowywanie własnych skryptów obsługujących różne nie używane i nie obsługiwane wcześniej formaty danych. W tym rozdziale przyjrzymy się formatom danych, które są już nam znane. Dowiemy się, w jaki sposób wyświetlać proste tabele i kalendarz na najbliższy miesiąc, wygenerujemy arkusz kalkulacyjny oraz dokument PDF. Dowiemy się też, jak tworzyć wszechstronną siatkę danych DataGrid, która wykorzystywać będzie wspomniane tu klasy do importowania i eksportowania danych.

PEAR. Programowanie w PHP

Tabele HTML Spośród wszystkich dostępnych elementów HTML tabele są tym chyba, który najczęściej bywa niewłaściwie wykorzystywany i nie do końca zrozumiany. Pierwotnie miały służyć wyłącznie do prezentowania danych, które powinny być wyświetlane właśnie w tabelach, niemniej twórcy stron WWW szybko odkryli, że można je też wykorzystywać w charakterze pojemnika ułatwiającego zarządzanie przestrzennym układem złożonych stron WWW. I wkrótce powszechnie zaczęto stosować różne paskudne techniki projektowania, takie jak na przykład używanie nieprzyzwoicie dużej liczby tabel do wyświetlania rzeczy tak prostych, jak ramka dla bloku tekstu, czy wykorzystywanie specjalnych obrazków GIF, by za ich pomocą określać szerokość komórek tabeli. Na drugim biegunie znalazło się wielu programistów i projektantów stron poczytujących sobie za punkt honoru, by ich strony WWW nie zawierały absolutnie żadnych tabel, i odmawiających korzystania z nich nawet tam, gdzie to było w pełni uprawnione. Odłożymy na razie na bok wszystkie uprzedzenia na temat tabel i różne nietypowe pomysły zastosowań dla nich i skoncentrujemy się na tym, do czego tabele zostały pierwotnie zaprojektowane, czyli na wyświetlaniu danych, które powinny być prezentowane w formie tabularnej.

Format tabel HTML Format tworzenia tabel w języku HTML jest bardzo prosty. Znacznik najwyższego poziomu definiujący początek i koniec tabeli to . Można do niego dodawać atrybuty formatowania, które będą miały zastosowanie do całej tabeli. Poszczególne wiersze tabeli są definiowane za pomocą znaczników . Wewnątrz wierszy natomiast definiuje się komórki tabeli. Komórki mogą być zarówno komórkami danych (ujmowanymi w znaczniki '; } else { echo ''; } if ($Day->isLast()) { echo "\n"; } } echo '
), jak i komórkami nagłówka tabeli (ujmowanymi w znaczniki ). Elementy te tworzą podstawowy szkielet tabeli, tak jak przedstawiamy w poniższym przykładzie:
Nagłówek Pierwszy Nagłówek Drugi Nagłówek Trzeci
Komórka Czwarta Komórka Piąta Komórka Szósta


Oglądając ten kod, już na pierwszy rzut oka łatwo stwierdzić, że ręczne tworzenie tabel HTML byłoby bardzo żmudnym zadaniem. Nawet w języku PHP, gdzie tabele wraz z danymi można tworzyć w pętlach, kod staje się szybko bardzo bałaganiarski, ponieważ musimy bez-

64

Rozdział 2. • Wyświetlanie danych

pośrednio zajmować się wszystkimi znacznikami, wyliczać, gdzie powinny znaleźć się znaczniki zamykające, itp. W tej sytuacji z pomocą przychodzi pakiet HTML_Table, będący obiektowym obudowaniem kodu PHP zajmującego się tworzeniem i modelowaniem tabel HTML. Za pomocą pakietu HTML_Table tabele tworzy się bardzo prosto: include_once 'HTML/Table.php'; $table = new HTML_Table(); $table->addRow(array("raz", "dwa", "trzy"), null, "th"); $table->addRow(array("raz", "dwa", "trzy")); echo $table->toHtml();

Zaczynamy od utworzenia nowej instancji klasy HTML_Table. Aby użyć atrybutów określających formatowanie całej tabeli, przesyłamy je konstruktorowi klasy — zajmiemy się tym jeszcze później. Gdy już uzyskamy nasz obiekt tabeli, będziemy mogli zacząć dodawać do niego wiersze. Pierwszym parametrem zajmującej się tym funkcji addRow() jest tabela zawierająca dane, które chcemy przechowywać w wierszu, drugi parametr umożliwia określanie dowolnych atrybutów formatujących tworzony wiersz, a trzeci atrybut określa, czy komórki tego wiersza powinny, czy też nie, używać znaczników komórek nagłówka
. Zależy nam na tym, aby pierwszy wiersz będący nagłówkiem tabeli używał znaczników , natomiast kolejne nie — powinny zawierać zwykłe komórki tabeli.

Tworzenie prostego kalendarza za pomocą HTML_Table Teraz, gdy już zobaczyliśmy, jak tworzyć tabelę za pomocą pakietu HTML_Table, możemy przyjrzeć się jakiemuś bardziej praktycznemu przykładowi tabeli. Zaczniemy od przygotowania prostego kalendarza dla miesiąca. Nasz kalendarz będzie prezentował listę dni w miesiącu oraz wyświetlał tygodnie i dni w formacie tabeli. Później dodamy do kalendarza jeszcze kilka opcji; na razie jednak skorzystamy z pakietów PEAR::Calendar i HTML_Table, by za ich pomocą przygotować kalendarz na bieżący miesiąc. include_once 'HTML/Table.php'; include_once 'Calendar/Month/Weekdays.php'; $table = new HTML_Table(); $Month = new Calendar_Month_Weekdays(date('Y'), date('n')); $Month->build(); while ($Day = $Month->fetch()) { if ($Day->isFirst()) {

65

PEAR. Programowanie w PHP

if (is_array($week)) { $table->addRow($week); } $week = array(); } $week[] = $Day->isEmpty() ? "" : $Day->thisDay(); } $table->addRow($week); $table->setColAttributes(0, 'bgcolor="#CCCCCC"'); $table->setColAttributes(6, 'bgcolor="#CCCCff"'); $table->updateAllAttributes('align="center"'); echo $table->toHTML();

W kodzie tym po załączeniu niezbędnych pakietów tworzymy nową instancję klasy HTML_Table. Aby zdefiniować obramowania tabeli lub jakikolwiek inny atrybut, należałoby przesłać odpowiedni atrybut do konstruktora klasy HTML_Table. Opiszemy to dokładniej w kolejnym przykładzie. Szczegółowy opis korzystania z klasy Calendar dostępnej w repozytorium PEAR wychodzi poza zakres tego rozdziału. Mówiąc skrótowo jednak, używa się jej w ten sposób, że tworzymy nowy obiekt, który zawiera informacje na temat bieżącego miesiąca, po czym przeglądamy poszczególne dni w pętli, zajmując się każdym dniem z osobna. W naszym przykładzie dodamy każdy dzień do pomocniczej tablicy, a gdy osiągniemy pierwszy dzień kolejnego tygodnia, dodawać będziemy do tabeli, a pomocniczą tablicę opróżnimy, by zrobić miejsce dla kolejnego tygodnia. Niektóre dni pierwszego i ostatniego tygodnia nie będą należeć do bieżącego miesiąca — potraktujemy je jako puste i nie będziemy dodawać ich do kalendarza. Gdy już zakończymy przeglądanie tygodni w pętli, dodamy do naszej tabeli ostatni konstruowany tydzień. Teraz, gdy już wszystkie dane zostały dodane do tabeli, możemy aktualizować i dodać do niej atrybuty dla wierszy lub kolumn, by wzbogacić tabelę o kilka elementów formatowania. Pakiet HTML_Table oferuje funkcje umożliwiające określanie atrybutów wierszy, kolumn lub indywidualnych komórek. Funkcje te to setRowAttributes() dla atrybutów wierszy, setColAttributes() dla atrybutów kolumn i setCell Attributes() dla atrybutów komórek. Kiedy określamy atrybuty części składowych naszej tabeli, należy pamiętać, że wszelkie formatowanie komórki zostanie zmienione, jeśli na wierszu, którego ta komórka jest częścią, zastosujemy funkcję setRowAttribute(). Aby uniknąć tego problemu, trzeba będzie przywoływać funkcje „aktualizujące”, które później aktualizować będą atrybuty komórki. W tym przykładzie, po dodaniu kolorów zaktualizujemy wszystkie komórki tabeli, by je wycentrować. Nie zmieni to wcześniejszego formatowania zastosowanego na nich.

66

Rozdział 2. • Wyświetlanie danych

Definiowanie pojedynczych komórek Trzeba naszego szczęścia, by zaraz po przygotowaniu kalendarza dowiedzieć się, że ktoś z kierownictwa zalecił wprowadzenie w kalendarzu poprawek, tak aby kalendarz wyróżniał nie tylko weekendy, ale również wszystkie pozostałe święta w miesiącu. W celu wprowadzenia tej modyfikacji potrzebujemy możliwości bardziej precyzyjnego sięgania do tabeli, dlatego zamiast dodawać do niej miesiące, będziemy teraz dodawać każdy dzień z osobna. Pociągnie to oczywiście za sobą konieczność przeprojektowania sposobu wprowadzania danych do tabeli. Aby ustalić, które dni w miesiącu są dniami świątecznymi, użyjemy pakietu Date_Holidays dostępnego w repozytorium PEAR. Podczas przeglądania w pętli kolejnych dni miesiąca będziemy sprawdzać, czy bieżący dzień jest świętem, i jeśli tak, odpowiednio formatować jego komórkę tabeli. W prawdziwej aplikacji kalendarza dodawalibyśmy jeszcze do numeru dnia nazwę święta. Pakiet Date_Holidays również i tutaj przychodzi nam z pomocą, w tym prostym przykładzie ograniczymy się jednak tylko do podświetlenia komórki dnia świątecznego. require_once 'HTML/Table.php'; require_once 'Calendar/Month/Weekdays.php'; require_once 'Date/Holidays.php'; $tableAttrs = array('border' => "2"); $table = new HTML_Table($tableAttrs); $Germany =& Date_Holidays::factory('Germany', 2005); $Month = new Calendar_Month_Weekdays(2005, 12); $Month->build(); $table->addRow(array('M', 'T', 'W', 'T', 'F', 'S', 'S'), null, "th"); while ($Day = $Month->fetch()) { if ($Day->isFirst()) { $row++; $col = 0; } if (!$Day->isEmpty()) { $table->setCellContents($row, $col, $Day->thisDay()); $t = sprintf('%4d-%02d-%02d', $Day->thisYear(), $Day->thisMonth(), $Day->thisDay()); if ($Germany->isHoliday($t)) { $table->setCellAttributes($row,$col, 'bgcolor="red"'); }

67

PEAR. Programowanie w PHP

} $col++; } $table->setRowAttributes(0, 'bgcolor="#CC99FF"'); $table->updateAllAttributes('align="center"'); $table->setCaption("Holidays"); echo $table->toHTML();

Pierwsza zmiana, którą łatwo zauważyć, to dodanie podczas tworzenia tabeli atrybutów obramowania. Atrybut obramowania border został dodany do głównego znacznika tabeli. W tym przykładzie pojawia się kilka nowych funkcji. Najważniejsza z nich to funkcja setCellContens(). Jak można domyślić się po jej nazwie, funkcja ta wymaga przesłania jej numeru wiersza i kolumny, określających położenie komórki, i następnie wypełnia komórkę odpowiednimi danymi. Dodaliśmy do tabeli również nagłówek prezentujący podświetlone pierwsze litery dni tygodnia oraz nagłówek samej tabeli. Obecnie nasz gotowy kalendarz wyświetla kalendarz dla bieżącego miesiąca, w którym wszystkie święta są podświetlone na czerwono.

Rysunek 2.1. Tabela prezentująca dni świąteczne w wybranym miesiącu

68

Rozdział 2. • Wyświetlanie danych

Pakiet HTML_Table_Matrix rozszerzający możliwości pakietu HTML_Table Pakiet HTML_Table_Matrix (HTM) jest pakietem podrzędnym względem HTML_Table, rozszerzającym jego możliwości, tak by ułatwić formatowanie danych prezentowanych w tabelach. Podstawowa zaleta korzystania z pakietu HTM polega na tym, że zamiast wypełniać każdy wiersz z osobna za pomocą funkcji addRow(), możemy po prostu określić, ile kolumn powinna mieć nasza tabela, przesłać nasze dane i pozwolić pakietowi HTML_Table_Matrix, aby sam je posortował. Pakiet HTML_Table_Matrix został zaprojektowany za pomocą sterowników wypełniania tabeli Filler, które zajmują się porządkowaniem danych umieszczonych w tabeli. Sterowniki te aktualnie umożliwiają wypełnianie tabeli w naturalny sposób: od lewej do prawej i z góry do dołu, a ponadto z dołu do góry, od prawej do lewej, a także np. ruchem spiralnym w kierunku odwrotnym do ruchu wskazówek zegara, poczynając od środka tabeli, i jeszcze na kilka innych sposobów. Każdy sterownik wypełniania tabeli Filler po prostu oferuje metodę next(), z której klasa renderująca tabelę będzie korzystać, by ustalić, gdzie należy umieścić kolejny element danych. Mimo iż raczej rzadko rozpoczyna się wypełnianie tabeli począwszy od środkowej komórki, został jednak przygotowany odpowiednio elastyczny mechanizm wypełniania, który powinien sprostać wszystkim przyszłym potrzebom. Miejsce składowania danych, które mają trafić do tabeli, jest badane tylko raz, aby pobrać dane. W naszym przykładzie użyjemy pakietu Services_Yahoo, by za jego pomocą pobrać z usługi Yahoo Image Search szesnaście obrazków, które zostaną wyświetlone w tabeli. include_once 'HTML/Table/Matrix.php'; include_once 'Services/Yahoo/Search.php'; $table = new HTML_Table_Matrix(array('border' => "2")); $rows = 4; $cols = 4; $term = 'Pears'; $search = Services_Yahoo_Search::factory("image"); $search->setQuery($term); $search->setResultNumber($rows * $cols); $results = $search->submit(); foreach($results as $image) { $data[] = ""; }

69

PEAR. Programowanie w PHP

$table->setTableSize($rows, $cols); $table->setFillStart(1, 0); $table->setData($data); $table->addRow(array("Obrazki wyszukane dla '$term'"), "colspan='$cols'", "th"); $f = HTML_Table_Matrix_Filler::factory("LRTB", $table); $table->accept($f); echo $table->toHtml();

Po dołączeniu obu pakietów, z których będziemy korzystać w tym przykładzie, definiujemy kilka zmiennych, które przechowywać będą informacje na temat wyników naszego wyszukiwania. Potrzebna nam będzie tabela zawierająca cztery wiersze i cztery kolumny, w której będziemy przechowywać obrazki znalezione dla terminu wyszukiwania „Pears” („Gruszki”). Gdy już otrzymamy z powrotem od Yahoo dane z wynikami wyszukiwania, określamy rozmiar naszej tabeli, bazując na predefiniowanych zmiennych. Będziemy musieli dodać do tabeli nagłówek, tak więc zaczynamy jej wypełnianie od pojedynczego wiersza znajdującego się u góry tabeli. Robimy to korzystając z funkcji setFillstart(). Ponieważ pakiet HTML_Table_Matrix jest pakietem podrzędnym względem pakietu HTML_Table, więc mimo iż metoda setData dodaje do tabeli od razu wszystkie dane, nadal możemy definiować i zmieniać dane znajdujące się w pojedynczych wierszach i komórkach. Tak właśnie postąpimy w przypadku wiersza nagłówka. Kiedy tworzymy instancję pakietu Filler, musimy przesłać metodzie konstruktora obiekt tabeli, jak również wykorzystywany sterownik wypełniający tabelę. Aby wypełnić tabelę od lewej do prawej i z góry na dół, użyjemy parametru LRTB. Następnie wyświetlimy gotową tabelę.

Rysunek 2.2. Wynik wyszukiwania za pomocą usługi Yahoo Image Search

70

Rozdział 2. • Wyświetlanie danych

Arkusze kalkulacyjne Excela Generowanie arkuszy kalkulacyjnych to zadanie, które regularnie zlecane jest wielu programistom. Niezależnie od tego, czy lubimy tę pracę, czy nie, trzeba jednak przyznać, że arkusze kalkulacyjne Excela są obecnie standardem prezentowania danych przedstawionych w tabelach i wymieniania się nimi. Dzięki łatwemu w użyciu formatowi oraz szerokiej dostępności różnych programów kompatybilnych z Excelem większość firm decyduje się właśnie na ten format, gdy zachodzi konieczność przygotowywania raportów dla kierownictwa lub wymiany danych między różnymi biurami. Mimo iż istnieje również kilka innych technik generowania plików kompatybilnych z formatem Excela, które zresztą omówimy na końcu tego podrozdziału, jedyną opartą wyłącznie na PHP metodą tworzenia arkuszy w oryginalnym formacie Excela jest klasa Excel_ SpreadSheet_ Writer z repozytorium PEAR. Klasa Excel_SpreadSheet_Writer została przeniesiona do PHP z języka z modułu Perla Spreadsheet::WriteExcel i obsługuje nie tylko wprowadzanie danych, ale również dodawanie formatowania, formuły, wiele arkuszy kalkulacyjnych, używanie obrazków i mnóstwo innych opcji. Klasa Excel_SpreadSheet_Writer nie korzysta z żadnych zewnętrznych komponentów, takich jak COM, więc pakiet ten jest naprawdę uniwersalny i działać będzie na wszystkich platformach, na których funkcjonuje język PHP.

Format Excela Format wykorzystywany przez pakiet Excel Spreadsheet Writer nosi nazwę BIFF5 (Binary Interchange File Format, format binarnej wymiany plików). Jest to standard binarny wprowadzony przez program Excel 5 i wszystkie obecne wersje programu Microsoft Excel; również pakiety OpenOffice potrafią interpretować format BIFF5. Format ten jest dość powszechnie akceptowany i rozpoznawany przez wiele programów, lecz brak w nim pewnych funkcji oferowanych przez późniejsze wersje Excela. Microsoft nie dostarczył żadnej oficjalnej dokumentacji formatu BIFF5, niemniej wielu programistów pracujących nad różnymi projektami wymagającymi obsługi tego formatu włożyło sporo pracy w zbadanie go i przygotowanie odpowiedniej dokumentacji. Jednym z najlepszych źródeł dokumentacji jest witryna pakietu biurowego OpenOffice. Odpowiednia dokumentacja została udostępniona pod adresem: http://sc.openoffice.org/excelfileformat.pdf. Jedną z wad pakietu Excel Spreadsheet Writer, na którą skarżą się programiści, jest sposób, w jaki obsługuje ona łańcuchy znaków Unicode. Prawdę powiedziawszy jednak, jest to wada nie tyle pakietu Excel Spreadsheet Writer, ile formatu BIFF5. Podejmowano liczne wysiłki, by dodać do klasy Excel_Spreadsheet_Writer obsługę formatu Unicode, niemniej na obecnym etapie nie planuje się jeszcze dodania żadnego z tych indywidualnie przygotowanych rozwiązań do pakietu Excel Spreadsheet Writer.

71

PEAR. Programowanie w PHP

Starsze formaty Microsoftu korzystają z systemu o nazwie OLE, by przy jego użyciu tworzyć dokumenty osadzające formaty różnych programów. Z tego powodu klasa Excel_Spreadsheet_ Writer uzależniona jest od pakietu OLE z repozytorium PEAR, dzięki którego pomocy może obudowywać tworzone przez siebie dokumenty w formacie BIFF5, tak aby były poprawnymi dokumentami Excela.

Nasz pierwszy arkusz kalkulacyjny Podstaw korzystania z klasy Spreeadsheet_Excel_Writer można nauczyć się bez trudu. W naszym pierwszym, prostym przykładzie utworzymy arkusz kalkulacyjny i dodamy dane do dwóch jego komórek. Teraz, gdy już wiemy, co zamierzamy zrobić, możemy przyjrzeć się kodowi. require_once 'Spreadsheet/Excel/Writer.php'; $workbook = new Spreadsheet_Excel_Writer(); $worksheet =& $workbook->addWorksheet('Example 1'); $worksheet->write(0, 0, 'Witam wszystkich!'); $worksheet->write(0, 1, 'To jest nasz pierwszy arkusz kalkulacyjny Excela'); $workbook->send('example1.xls'); $workbook->close();

Podczas pracy z klasą Spreadsheet_Excel_Writer mamy do wyboru dwa sposoby zachowywania naszych gotowych arkuszy kalkulacyjnych. Pierwsza opcja, z której korzystamy w tym przykładzie, polega na użyciu metody send() wysyłającej nagłówki Excela (application/vnd.ms-excel), a zaraz po nich dane arkusza kalkulacyjnego do naszej przeglądarki internetowej. W ten sposób otworzymy arkusz kalkulacyjny w oknie przeglądarki, co pozwoli nam obejrzeć jego zawartość lub zachować na komputerze — w zależności od naszych potrzeb i możliwości przeglądarki oraz ustawień komputera. Druga opcja polega na zachowaniu wygenerowanego pliku w naszym lokalnym systemie plików. W tym celu należy konstruktorowi tworzącemu obiekt klasy Spreadsheet_Excel_Writer przesłać po prostu odpowiednią ścieżkę do pliku. W ten sposób, gdy zamkniemy arkusz kalkulacyjny za pomocą funkcji close(), dane zostaną zachowane w zdefiniowanym przez nas pliku. Zastanawiając się, którą z metod wybrać, warto pamiętać, że gdy wysyłamy arkusz kalkulacyjny bezpośrednio do przeglądarki WWW, nie będziemy mogli przesłać wraz z nim żadnego dodatkowego tekstu w formacie HTML. Tak więc rozwiązanie to sprawdza się, gdy zależy nam tylko na przygotowaniu skryptu, którego jedynym zadaniem byłoby dynamiczne serwowanie użytkownikom arkuszy kalkulacyjnych. Niemniej w większości przypadków chcemy zrobić coś więcej: wyświetlić arkusz kalkulacyjny na stronie HTML lub poinformować użytkownika, że zakończyliśmy generowanie arkusza kalkulacyjnego. W takiej sytuacji wygodniej jest zachować arkusz kalkulacyjny w naszym systemie plików i przystąpić do generowania strony HTML, która będzie z niego korzystać. W kolejnych przykładach będziemy dla wygody korzystać właśnie z tej techniki.

72

Rozdział 2. • Wyświetlanie danych

Gdy już będziemy mieli gotowy obiekt naszego arkusza kalkulacyjnego, możemy przejść do następnego etapu i wstawić do jego komórek odpowiednie dane. Na koniec zamkniemy nasz „skoroszyt” (zmienna $workbook), co spowoduje skompilowanie danych i odpowiednio do wybranej opcji zapisanie ich w pliku lub wysłanie do przeglądarki.

Słowo o komórkach Programista zapisujący arkusz kalkulacyjny może użyć jednej z dwóch metod odnajdywania komórek w arkuszu kalkulacyjnym Excela. Kiedy dodajemy dane do arkusza kalkulacyjnego, określamy pozycję komórki za pomocą współrzędnych X i Y. Ponieważ współrzędne liczone są począwszy od zera, pierwsza komórka określona będzie współrzędnymi 0,0. Aby korzystać z formuł Excela, trzeba z kolei zastosować inną notację, w której kolumny oznaczane są literami, a wiersze numerami. Pierwsza komórka będzie w tej notacji oznaczona jako A1. Różnica pomiędzy oboma wspomnianymi sposobami odwoływania się do komórek stanie się szczególnie wyraźna, gdy zaczynamy pracować z formułami. Na szczęście klasa Speadsheet_ Excel_Writer dostarcza wygodnej funkcji, która dokonuje konwersji z notacji opartej na numerach kolumn i wierszy na drugą notację, opartą na nazwach komórek. $first = 1; $last = 10; for ($i = $first; $i write($i, 1, $i); } $cell1 = Spreadsheet_Excel_Writer::rowcolToCell($first, 1); $cell2 = Spreadsheet_Excel_Writer::rowcolToCell($last, 1); $worksheet1->write($last + 1, 0, "Total ="); $worksheet1->writeFormula($last + 1, 1, "=SUM($cell1:$cell2)");

Jak widać, najpierw używamy numerów kolumn i wierszy, by zapisać dane w odpowiednich komórkach arkusza kalkulacyjnego, a następnie stosujemy statyczną metodę rowcolToCell(), by przekonwertować pozycję definiowana za pomocą numerów i kolumn na adres wykorzystujący do identyfikacji kolumn litery, którego to formatu wymagają formuły Excela. W tym przykładzie wartość łańcucha zapisanego w zmiennej $cell1 to B2, a wartość zapisana w zmiennej $cell2 to B11. Dlatego też formuła wykonywana przez Excel będzie miała postać =SUM(B2:B11). Więcej na temat formuł Excela w dalszej części rozdziału.

73

PEAR. Programowanie w PHP

Przygotowywanie strony do wyświetlenia Dostępnych jest wiele różnych opcji umożliwiających modyfikowanie sposobu wyświetlania arkusza kalkulacyjnego. Okazują się one szczególnie użyteczne, jeśli prezentujemy arkusz kalkulacyjny klientowi i chcielibyśmy mieć wpływ na to, w jaki sposób będzie wyświetlony. Wszystkie opcje formatowania mają zastosowanie do całego arkusza kalkulacyjnego. Funkcja

Przeznaczenie funkcji

$worksheet->setPaper(1);

Określa rozmiar strony używając odpowiedniej stałej.

$worksheet->setPortrait();

Określają orientację strony, odpowiednio pionową (portret) lub poziomą (pejzaż).

$worksheet->setLandscape(); $worksheet->setHeader(); $worksheet->setFooter();

Dodają do każdej strony arkusza kalkulacyjnego odpowiednio nagłówek i stopkę.

$worksheet->setMargins(.5);

Określa margines naokoło każdej wartości, liczony w calach. Każdy z marginesów można określać z osobna.

$worksheet->printArea($firstcol, $firstrow, $lastcol, $lastrow);

Definiuje, jaki obszar strony (pomiędzy jakimi kolumnami i wierszami) ma zostać wyświetlony lub wydrukowany.

$worksheet->hideGridlines();

Ukrywa siatkę arkusza podczas wyświetlania.

$worksheet->fitToPages(2, 2);

Określa maksymalną liczbę stron arkusza kalkulacyjnego używanych podczas wyświetlania (drukowania). W tym przypadku dwie strony wszerz i dwie strony w dół.

$worksheet->setPrintScale($scale);

Określa procentowy współczynnik skalowania arkusza kalkulacyjnego. Domyślnie to 100%. Niweluje działanie opcji „fit to page” (dopasuj do rozmiaru strony).

Dodawanie formatowania Teraz, gdy wiemy już, jak tworzyć za pomocą języka PHP pliki Excela, warto zająć się formatowaniem komórek. Inaczej niż w przypadku tworzącego tabele HTML pakietu HTML_Table, gdzie mogliśmy indywidualnie edytować atrybuty każdej komórki, zmieniając jej formatowanie, pakiet Spreeadsheet_Excel_Writer podczas tworzenia i stosowania na komórkach stylów stosuje podejście obiektowe. Aby utworzyć nowy styl, skorzystamy z funkcji addFormat() z klasy workbook (skoroszyt). W ten sposób utworzymy obiekt formatowania, który będziemy mogli zastosować na tylu komórkach, na ilu zechcemy. Przypomina to trochę tworzenie klas CSS (stylów) w języku HTML. W projekcie

74

Rozdział 2. • Wyświetlanie danych

zazwyczaj tworzy się przynajmniej kilka standardowych obiektów formatowania, które następnie wykorzystywane są w różnych miejscach projektu. require_once 'Spreadsheet/Excel/Writer.php'; $workbook = new Spreadsheet_Excel_Writer('example2.xls'); $worksheet =& $workbook->addWorksheet("Example 2"); $header =& $workbook->addFormat(array("bold" => true, "Color" => "white", "FgColor" => "12", "Size" => "15")); $worksheet->write(0, 0, 'Witam wszystkich', $header);

W tym przykładzie najpierw tworzymy nowy arkusz kalkulacyjny, a następnie przesyłamy nasze parametry formatowania funkcji addFormat(), by pobrać nasze opcje formatowania, które zastosujemy następnie na danych wysyłanych, gdy będziemy dodawać tekst. Każdy z kluczy tablicy przesyłanej jako parametr funkcji addFormat() również będzie posiadać osobną funkcję, której możemy użyć, by niezależnie określić daną wartość formatującą. $header =& $workbook->addFormat(); $header->setBold(); $header->setColor("white"); $header->setFgColor("12");

Dzięki temu, że każdą z wartości formatujących można stosować niezależnie od innych, nasz kod jest łatwiejszy do zarządzania i modyfikacji w przyszłości.

Kolory Excel obchodzi się z kolorami w bardzo ciekawy sposób. Uważny czytelnik zauważył zapewne, że atrybutowi FgColor przypisaliśmy wartość 12, a atrybutowi Color tekstu wartość white (biały). Excel korzysta zarówno z nazw kolorów, jak i z własnego wewnętrznego systemu indeksowania kolorów. Przedstawiony tutaj skrypt generuje schemat kompatybilnych z Excelem kolorów, których możemy używać w naszych arkuszach kalkulacyjnych. require_once 'Spreadsheet/Excel/Writer.php'; $workbook = new Spreadsheet_Excel_Writer('example2a.xls'); $worksheet =& $workbook->addWorksheet("Colors"); $row = 0; $col = 0; for ($i = 1; $i addFormat(array("bold" => true, "Color" => "white", "FgColor" => $i));

75

PEAR. Programowanie w PHP

$worksheet->write($row, $col, '#'.$i, $format); $col++; if ($col == 7) { $col = 0; $row++; } } $workbook->close();

Kod ten wygeneruje następujący schemat:

Rysunek 2.3. Arkusz Excela z barwnymi komórkami

Paleta kolorów zmieniła się odrobinę między Excelem 5 a Excelem 97 i należy o tym pamiętać, jeśli sądzimy, że nasi użytkownicy będą korzystać z bardzo starych wersji Excela. Widoczne na rysunku liczby nie są kodami szesnastkowymi, jak w przypadku języka HTML, a po prostu wewnętrznymi identyfikatorami kolorów Excela. Jak łatwo zauważyć, kolor tła komórki określiliśmy za pomocą atrybutu FgColor. Funkcji tej używamy, ponieważ Excel może użyć jako tła komórki również barwnego desenia. Jeśli nie zostanie określony żaden deseń, domyślnie tło będzie jednolitym kolorem, a atrybut FgColor określi kolor będący w deseniu „na pierwszym planie” (ang. foreground). Owszem, przyznajemy, jest to odrobinę skomplikowane. Desenie opiszemy dokładnie poniżej. Jeśli natomiast zechcemy zastosować kolor, który nie pojawia się na naszym schemacie barw, można spróbować pokryć któryś ze standardowych kolorów naszym własnym kolorem. Nowy kolor tworzy się określając, który z oryginalnych kolorów chcemy zastąpić — w naszym przypadku zastąpimy kolor o identyfikatorze 12 — a następnie wybierając odpowiednie wartości barw składowych RGB (red, green, blue — czerwonej, zielonej i niebieskiej). $workbook->setCustomColor(12, 10, 200, 10);

Podmiana koloru stosować się będzie do całego arkusza kalkulacyjnego.

76

Rozdział 2. • Wyświetlanie danych

Wypełnianie barwnym deseniem Oprócz własnego zestawu kolorów, Excel oferuje również barwne desenia, których także można używać w charakterze tła. Domyślnym deseniem wypełnienia jest jednakże wypełnienie jednolitym kolorem, gdy wyświetlany jest tylko kolor tła. Kolejny rysunek pokazuje dostępne desenie oraz ich numery identyfikacyjne. W tym przykładzie kolorem pierwszego planu jest ciemnoszary, a kolorem tła — jasnoszary. require_once 'Spreadsheet/Excel/Writer.php'; $workbook = new Spreadsheet_Excel_Writer('example2b.xls'); $worksheet =& $workbook->addWorksheet("Colors"); $row = 0; $col = 0; for ($i = 1; $i addFormat(array("bold" => true, "Color" => "white", "BgColor" => 22, "FgColor" => 23, "Pattern" => $i)); $worksheet->write($row, $col, '#'.$i, $format); $col++; if ($col == 4) { $col = 0; $row++; } } $workbook->close();

Rysunek 2.4. Komórki Excela wypełnione różnymi deseniami

Formatowanie liczb Excel dostarcza również bogatego zestawu opcji umożliwiających formatowanie i wyróżnianie kolorami wartości liczbowych.

77

PEAR. Programowanie w PHP

Liczby wewnątrz formatów mogą być reprezentowane za pomocą specjalnych symboli zastępczych # lub 0. Różnica między tymi dwoma symbolami zastępczymi polega na tym, że gdy użyjemy 0, to wyniki zostaną wyrównane do formatu określonego za pomocą symbolu przez dodanie w odpowiednich miejscach wypełniających zer, natomiast użycie symbolu # spowoduje wyświetlenie wyłącznie samych liczb. Tak więc format #####.## zastosowany na liczbie 4201.5 wyświetli po prostu tę liczbę, natomiast format 0000.00 wyświetli ją w postaci 04201.50. Najlepiej jest korzystać z kombinacji obu symboli, na przykład #.00, by wyświetlać liczby w formacie możliwie najwygodniejszym dla użytkownika: 4201.50. Innym symbolem zastępczym, którego można używać w formatowaniu, jest znak zapytania, ?. Pozostawia on w miejsce nie mających znaczenia dla wartości liczby zer pustą spację i przydaje się, gdy chcemy po prostu wyrównać liczby według kropki dziesiętnej. Kiedy określamy format liczby, Excel pozwala na definiowanie osobno formatów zarówno dla liczb dodatnich, jak i ujemnych. Formaty oddziela się średnikiem, a odpowiedni format będzie używany w zależności od wartości tekstu lub liczby w danym polu. Jeśli na przykład chcemy, aby liczby dodatnie były wyświetlane na niebiesko, a ujemne na czerwono i dodatkowo ujęte w nawiasy, to należy użyć następującego łańcucha deklarującego sposób formatowania: $format =& $workbook->addFormat(); $format->setNumFormat('[Blue]$0.00;[Red]($0.00)'); $worksheet->write(2, 1, "-4201", $format); $worksheet->write(2, 2, "4201", $format);

Możemy również określić osobny format dla wartości 0 lub wartości tekstowych. $format =& $workbook->addFormat(); $format->setNumFormat('[Blue]0;[Red]0;[Green]0;@*-'); $worksheet->write(0, 1, 10, $format); $worksheet->write(0, 1, -10, $format); $worksheet->write(0, 1, 0, $format); $worksheet->write(0, 1, "dziesięć", $format);

Ten format będzie wyświetlał liczby dodatnie na niebiesko, liczby ujemne na czerwono, wartości 0 na zielono, a tekst będzie wyrównywany za pomocą tylu kresek, ile zmieści się w komórce. Mając takie możliwości manipulowania formatami, możemy na przykład utworzyć format, który nie będzie wyświetlał wartości 0 lub będzie wyświetlał komunikat o błędzie, jeśli do pola przeznaczonego na wartości liczbowe zostanie dodany tekst. Jeśli chcemy na przykład, aby wynik obliczeń zwracał opisowy tekst 6 zł. 95 gr. zamiast liczby 6.95, to należy użyć następującego łańcucha formatującego: $format =& $workbook->addFormat(); $format->setNumFormat('0 "zł." .00 "gr."'); $worksheet->write(4, 1, 6.95, $format);

Można pójść nawet jeszcze dalej i zażyczyć sobie, aby wartość dziesiętna (grosze) przedstawiana była w formie ułamka wartości liczonej w złotych (PLN).

78

Rozdział 2. • Wyświetlanie danych

$format =& $workbook->addFormat(); $format->setNumFormat('0 ??/?? "PLN"'); $worksheet->write(0, 1, 42.50, $format);

W ten sposób wyświetlimy tekst 42 ½ PLN. Oto tabela zestawiająca niektóre z najczęściej stosowanych formatów: Format

Opis

0000

Pokazuje nie mniej niż pięć cyfr. Liczby wyrównywane są przez dodanie na początku zer.

;;;@

Nie pozwala na wyświetlanie liczb i wyświetla tylko tekst (@).

#.???

Wyrównuje liczby według kropki dziesiętnej.

#,

Wyświetla liczby w tysiącach.

0,000,, "Milion"

Wyświetla liczby w milionach, dodając na końcu tekst „Milion”.

0;[Red]"Błąd!"; ¦0;[red]"Błąd!"

Wyświetla na czerwono tekst Błąd! w przypadku pojawienia się liczby ujemnej lub wartości tekstowej.

0.00_-;0.00-

Wyświetla po prawej stronie liczby znak minusa i dodaje spację, aby wyrównać liczby do kropki dziesiętnej.

'0","000'

Wstawia do liczby przecinek dla oddzielenia tysięcy, dzięki czemu 100000 będzie wyświetlane jako 10,000.

??/??

Wyświetla liczbę dziesiętną jako ułamek zwykły.

# ??/??

Wyświetla część dziesiętną liczby w postaci ułamka zwykłego.

0.00E+#

Wyświetla liczbę w notacji naukowej.

Formuły Tworzenie formuł i przypisywanie ich do komórek jest jedną z podstawowych funkcji Excela. Teraz, gdy wiemy już, jak umieszczać w komórkach arkusza kalkulacyjnego dane i w jaki sposób je formatować, zobaczmy, jak dodawać do komórek formuły, by Excel wykonywał żmudną pracę za nas. require_once 'Spreadsheet/Excel/Writer.php'; $workbook = new Spreadsheet_Excel_Writer('example3.xls'); $worksheet =& $workbook->addWorksheet("Example 3"); $tax =& $workbook->addFormat(); $tax->setNumFormat('.00%'); $price =& $workbook->addFormat(); $price->setNumFormat('$####.00');

79

PEAR. Programowanie w PHP

$worksheet->write(0, 0, 'Arkusz rozliczania podatku'); $worksheet->write(1, $worksheet->write(1, $worksheet->write(2, $worksheet->write(2,

0, 1, 1, 2,

'VAT:'); ".16", $tax); 'Cena'); "Z podatkiem");

$worksheet->freezePanes(array(3)); for ($i = 3; $i < 103; $i++) { $worksheet->write($i, 0, "Item ".($i-2)); $worksheet->write($i, 1, rand(1, 100), $price); $cell = Spreadsheet_Excel_Writer::rowcolToCell($i, 1); $worksheet->writeFormula($i, 2, "=($cell*B2)+$cell", $price); } $worksheet->writeFormula(103, 1, "=SUM(B4:B103,C4:C103)", $price); $workbook->close();

Ten przykład generuje 100 liczb losowych, dodaje je do arkusza kalkulacyjnego i następnie tworzy formułę obliczającą dla nich podatek. Użytkownik arkusza kalkulacyjnego będzie mógł zmieniać tę formułę. Użyliśmy tutaj pomocniczej funkcji rowcolToCell(), szybko przełączającej się z notacji wykorzystującej numer kolumny i wiersza na notację używającą nazw komórek, której Excel wymaga podczas definiowania formuł. Ostatnia formuła, na koniec arkusza kalkulacyjnego, oblicza sumę (SUM) kolumn B i C. Excel jest bardzo czuły, jeśli chodzi o separator argumentów, i przykład ten dodaliśmy po to, by pokazać, że podczas przesyłania argumentów do funkcji Excela metoda writeFormula() będzie wymagać używania jako separatora argumentów przecinka. Warto jednak wiedzieć, że w niektórych, dostosowywanych do lokalnych potrzeb wersjach Excela formuła SUM(B4:B103,C4:C103) będzie musiała być zapisywana w następujący sposób: SUM(B4:B103;C4:C103), z wykorzystaniem jako separatora średnika (;). Z pozoru drobna różnica, lecz może być źródłem trudnych do wykrycia błędów. Ponieważ przykład ten przewija w dół widoczny obszar ekranu, zamroziliśmy trzy górne wiersze używając metody freezePanes().

Wiele arkuszy kalkulacyjnych, obramowania, obrazki Teraz gdy już najcięższa praca za nami, możemy zająć się upiększaniem wyglądu naszego arkusza kalkulacyjnego.

80

Rozdział 2. • Wyświetlanie danych

Aby zilustrować korzystanie z formuł, utworzymy prosty generator faktur (ang. invoice). Dla uproszczenia przykładu usunęliśmy większość formatów, tak aby pozostawić zadanie upiększenia arkusza czytelnikowi — jako ćwiczenie domowe. require_once 'Spreadsheet/Excel/Writer.php'; $workbook = new Spreadsheet_Excel_Writer("example4.xls"); $worksheet =& $workbook->addWorksheet(); $worksheet->writeNote(1, 0, "Faktura dla nowego klienta"); $worksheet->setRow(0, 50); $worksheet->insertBitmap(0, 0, "logo.bmp", 0, 0); $left =& $workbook->addFormat(array("Left" => 2)); $right =& $workbook->addFormat(array("Right" => 2)); $number =& $workbook->addFormat(array("NumFormat" => '$####.00')); $worksheet->write(1, 1, "Klient:"); $worksheet->write(2, 1, "Podatek:"); $worksheet->writeNumber(2, 2, .16); $cart = array("Monitor" => 12, "Drukarka" => 14.4); $top = 4; foreach ($cart as $item => $price) { $worksheet->write($top, 1, $item, $number); $worksheet->write($top, 2, $price, $number); $cell = "C" . ($top + 1); $worksheet->writeFormula($top, 3, "=($cell*C3)+$cell", $number); }

$top++;

$lastrow = $top + 1; for ($i=1; $i writeBlank($i, 0, $left); $worksheet->writeBlank($i, 7, $right); } $worksheet->write($lastrow, 2, "Suma:"); $worksheet->writeFormula($lastrow, 3, "=SUM(D5:D$lastrow)", $number); $workbook->close();

81

PEAR. Programowanie w PHP

Najważniejszymi kwestiami, na które należy tutaj zwrócić uwagę, jest dodanie obrazka i obramowań. Mapy bitowe można dodawać do arkusza kalkulacyjnego za pomocą metody insertBitmap(). Jest to dość prosta technika; ponieważ jednak działa w sposób niezbyt dla niektórych ludzi intuicyjnie zrozumiały, użytkownikom czasem zdarza się uskarżać na błędne zachowanie programu, gdy próbują zmienić wysokość wiersza, w którym znajduje się mapa bitowa. Dzieje się tak dlatego, że wysokość wiersza musi zostać określona, zanim obrazek zostanie załączony do arkusza kalkulacyjnego. Jeśli dodamy obrazek, a następnie spróbujemy zmienić wysokość wiersza, obrazek zostanie rozciągnięty lub ściśnięty, a nie o to nam przecież chodzi. W tym przykładzie najpierw przywołujemy metodę setRow() i dopiero gdy za jej pomocą określimy wysokość wiersza, przywołujemy metodę insertBitmap(), by osadzić w komórce obrazek. Obramowania działają dokładnie w ten sam sposób, co format tekstowy. Po prostu dodajemy styl obramowania do naszego formatu i stosujemy go na komórce. W tym przypadku nie musimy wstawiać do komórek, dla których formatujemy obramowania, żadnych danych, dlatego też używamy metody writeBlank(), która określa formatowanie komórki bez wstawiania żadnych danych. Warto też zwrócić uwagę na to, że w przykładzie tym korzystamy z metod writeNumber() i writeNote(). Są to po prostu jeszcze inne sposoby zapisywania danych w arkuszu kalkulacyjnym przygotowanym za pomocą klasy Spreadsheet_Excel_Writer. W tym przykładzie wygenerowaliśmy tylko jedną fakturę. Niemniej gdybyśmy pobierali dane z jakiegoś zewnętrznego źródła i musielibyśmy przygotować wiele faktur, moglibyśmy bez trudu dodać tyle nowych arkuszy kalkulacyjnych, ile potrzeba. Każdy kolejny arkusz należałoby dodawać za pomocą metody addWorksheet() klasy skoroszytu workbook.

Rysunek 2.5. Przykład wygenerowanej faktury

82

Rozdział 2. • Wyświetlanie danych

Inne techniki tworzenia arkuszy kalkulacyjnych Klasa Spreadsheet_Excel_Writer jest najlepszym sposobem tworzenia doskonałych arkuszy Excela. Niemniej w sytuacjach, gdy nie potrzebujemy wszystkich funkcji oferowanych przez klasę Excel_Spreadsheet_Writer i chcielibyśmy wyświetlić dane w odrobinę prostszy sposób, możemy skorzystać z innych, łatwiejszych metod.

CSV Skromny format CSV (comma separated values, oddzielający wartości przecinkami) jest najprostszym z formatów wykorzystywanych do wymieniania się danymi i dlatego wiele programów umożliwia importowanie i eksportowanie danych w formacie CSV. Repozytorium PEAR oferuje moduł File, za którego pośrednictwem można bez trudu odczytywać i zapisywać dane w plikach w formacie CSV.

Trik z nagłówkiem Content-Type Jedna z często wykorzystywanych technik, która sprawdza się całkiem dobrze w nowych wersjach Excela, polega po prostu na utworzeniu tabeli HTML, która zawierać będzie odpowiednie dane, a następnie na wysłaniu nagłówka HTTP z wartością application/vnd.msexcel, a po nim przygotowaniu tabeli HTML. Przeglądarka WWW rozpozna nagłówek i potraktuje tabelę HTML tak, jakby była arkuszem kalkulacyjnym Excela. Program Excel z kolei zaakceptuje tabelę HTML i również ją wyświetli, oferując jednak znacznie mniej funkcji, niż mielibyśmy do wyboru, gdybyśmy pracowali z prawdziwym dokumentem Excela. Trik ten działa dzięki temu, że najnowsze formaty plików Excela bazują na standardzie języka XML i dlatego Excel jest dość spolegliwy, gdy pora na formatowanie.

Generowania plików Excela 2003 Inaczej niż kłopotliwy w użyciu format BIFF, współczesne formaty dokumentów Excela oparte są na języku XML, standardy są ze sobą zgodne i dobrze udokumentowane. Jedna z często stosowanych technik polega na tworzeniu naszych dokumentów przy użyciu najnowszych wersji Excela, następnie edytowaniu wygenerowanego za jego pomocą dokumentu XML i dodawaniu znaczników PHP właśnie do tego dokumentu XML. Konieczna będzie w tym celu zmiana konfiguracji serwera WWW, by analizował dokumenty Excela tak jak pliki PHP. Po wprowadzeniu tej drobnej zmiany w konfiguracji będziemy jednak mogli używać języka PHP wprost w dokumentach Excela.

83

PEAR. Programowanie w PHP

Tworzenie arkuszy kalkulacyjnych za pomocą formatu OpenDocument repozytorium PEAR Dzięki projektowi Summer of Code firmy Google zostały rozpoczęte prace nad przygotowaniem formatu odczytywania i zapisywania danych OpenDocument dla repozytorium PEAR. Kiedy projekt zostanie zakończony, możliwe będzie tworzenie oferujących pełnię potrzebnych funkcji arkuszy kalkulacyjnych w formacie OpenDocument. W najnowszych wersjach pakietu Microsoft Office podejmowane są zabiegi, by zagwarantować pełną współpracę z formatem OpenDocument. Co prawda obecnie nie można jeszcze pracować z arkuszami kalkulacyjnymi OpenDocument w pakiecie Office, niemniej zostało to uwzględnione w projektach przyszłych wersji pakietu Office.

Komponent siatki danych DataGrid Programistom Windows dobrze znana jest technika polegająca na wykorzystaniu do wyświetlania danych w formie wygodnej w użyciu i łatwej do sortowania siatki za pomocą komponentu DataGrid. W prostych scenariuszach wszystko, co będzie musiał zrobić programista, to pobrać dane z odpowiedniego źródła danych (bazy danych, pliku tekstowego, tablicy), a następnie wyświetlić je w postaci łatwej do skonfigurowania strony WWW w kodzie HTML. W bardziej złożonych scenariuszach programista może przygotować siatkę w ten sposób, aby można było sortować jej zawartość, włączyć filtrowanie danych i renderować je w różnych formatach. Od strony sieci WWW programiści ASP.NET mają do dyspozycji komponent DataGrid. Język PHP nie oferuje żadnej standardowej implementacji komponentu DataGrid i większość programistów języka PHP musi pisać własne komponenty lub korzystać z komponentów przygotowanych przez kogoś innego, lub nawet z komercyjnych implementacji komponentu DataGrid. Jak już wspomnieliśmy, korzystanie z komponentu DataGrid wymaga wykonania kilku czynności. „ Trzeba skądś pobrać dane. Będzie to nasze źródło danych — DataSource. „ Trzeba utworzyć siatkę DataGrid i wybrać format zwracania danych, co potocznie określane jest jako renderer (Renderer). „ Trzeba powiązać źródło danych DataSource z siatką DataGrid i wyświetlić siatkę. Wymaganie to zostało odpowiednio ustandaryzowane w różnych implementacjach DataGrid. Dobrze sprawdza się tutaj pakiet Structures_DataGrid z repozytorium PEAR. Oprócz tego, że oferuje nam standardowe opcje umożliwiające pobieranie danych i wiązanie ich z tabelą HTML, to zapewnia również technikę pobierania i renderowania danych opartą na sterownikach. Dzięki temu pakiet Structures_DataGrid może na przykład skorzystać ze swojego sterownika XML DataSource, by pobrać dane z pliku XML. Następnie trzeba ustalić, które pola należy wyświetlić, i renderować dane w dowolnym formacie, dla którego istnieje odpowiedni

84

Rozdział 2. • Wyświetlanie danych

sterownik renderowania. Jeśli wymagania projektu się zmienią i konieczne będzie renderowanie siatki danych DataGrid w innych formatach, to wystarczy tylko przygotować nowy renderer Structures_DataGrid. Jak pewnie czytelnicy już się domyślili, daje nam to nie tylko swobodę tworzenia bardzo wszechstronnych siatek danych DataGrid, ale również korzystania z bardzo efektywnego silnika konwersji danych, jako że obecnie jesteśmy w stanie konwertować dane z dowolnego formatu, dla którego istnieje sterownik DataSource, na dowolny inny format, dla którego istnieje odpowiedni renderer. W kolejnych tabelach podajemy listę dostępnych źródeł danych DataSource. Trzecia kolumna w obu tabelach podaje stałą, która reprezentuje źródło danych DataSource lub renderer i jest przesyłana do konstruktora Structures_Datagrid.

Źródła danych DataSource Nazwa

Funkcja

Stała

CSV

Analizuje dane w formacie CSV — oddzielane przecinkami.

DATAGRID_SOURCE_CSV

DataObject

Wykorzystuje do pobierania danych interfejsu PEAR Object Interface do tabel bazy danych DB_DataObject.

DATAGRID_SOURCE_DATAOBJECT

RSS

Pobiera i rozpatruje dane z zewnętrznego źródła RSS.

DATAGRID_SOURCE_RSS

DB

Pobiera dane używając modułu PEAR::DB.

DATAGRID_SOURCE_DB

XML

Rozpatruje pliki XML.

DATAGRID_SOURCE_XML

Renderery Nazwa

Funkcja

Stała

Excel

Generuje natywne pliki programu MS Excel korzystając z modułu PEAR::Spreadsheet_Excel_Writer.

DATAGRID_RENDERER_XLS

HTML_Table

Domyślny renderer. Prezentuje dane w postaci konfigurowalnej, sortowalnej i dającej się stronicować tabeli HTML.

DATAGRID_RENDERER_TABLE

XML

Formatuje dane do postaci pliku XML.

DATAGRID_RENDERER_XML

XUL

Renderuje siatkę DataGrid do formatu XUL właściwego dla przeglądarki Mozilla Firefox i innych przeglądarek opartych na silniku Gecko.

DATAGRID_RENDERER_XUL

CSV

Renderuje siatkę DataGrid w formacie CSV.

DATAGRID_RENDERER_CSV

Console

Renderuje siatkę DataGrid w postaci tabeli, którą można wyświetlać w konsoli.

DATAGRID_RENDERER_CONSOLE

85

PEAR. Programowanie w PHP

Przede wszystkim zaczniemy od zaprezentowania kilku prostych przykładów zastosowania pakietu Structures_DataGrid, po czym przejdziemy do zagadnień związanych z renderowaniem, formatowaniem i rozbudowywaniem pakietu Structures_DataGrid.

Prosta siatka danych DataGrid Teraz, gdy wiemy już, czym zajmuje się pakiet Structures_DataGrid, pora przyjrzeć się przykładom kodu, by przekonać się, w jaki dokładnie sposób on działa. require_once 'Structures/DataGrid.php'; $data = array(array('Imie' 'Nazwisko' 'Email' array('Imie' 'Nazwisko' 'Email' array('Imie' 'Nazwisko' 'Email' array('Imie' 'Nazwisko' 'Email' );

=> => => => => => => => => => => =>

'Aaron', 'Wormus', '[email protected]'), 'Clark', 'Kent', '[email protected]'), 'Peter', 'Parker', '[email protected]'), 'Bruce', 'Wayne', '[email protected]')

$dg =& new Structures_DataGrid; $dg->bind($data); $dg->render();

Ten przykład pokazuje trzy kroki związane z tworzeniem siatki danych DataGrid. Najpierw tworzymy instancję klasy Structures_DataGrid. Następnie za pomocą metody bind() wiążemy tablicę danych z siatką DataGrid. Domyślny sterownik źródła danych DataSource wykorzystywany przez pakiet Structures_DataGrid to sterownik ARRAY, możemy więc przesłać tablicę z danymi naszej siatce DataGrid bez konieczności definiowania żadnych innych opcji. Gdy już źródło danych DataSource zostanie powiązane z siatką danych DataGrid, renderujemy siatkę używając metody render(), która przygotuje nam do wyświetlenia w pełni funkcjonalną siatkę danych DataGrid. W tym fragmencie kodu warto zwrócić uwagę na to, że instancja klasy DataGrid musi zostać utworzona jako odwołanie za pomocą składni =&. Ta zmiana w DataGrid ma związek ze sposobem, w jaki pakiet Structures_DataGrid współpracuje ze swoimi sterownikami. Ponieważ jednak zmiana ta niszczy wsteczną kompatybilność, należy o niej pamiętać podczas tworzenia lub unowocześniania instancji DataGrid.

86

Rozdział 2. • Wyświetlanie danych

Imie

Nazwisko

Email

Aaron

Wormus

[email protected]

Bruce

Wayne

[email protected]

Clark

Kent

[email protected]

Peter

Parker

[email protected]

Jak widać, jednym z domyślnych zastosowań siatki DataGrid jest sortowanie prezentowanych rekordów. Wystarczy po prostu kliknąć odpowiednie łącza nagłówków, by siatka danych DataGrid posortowała określoną kolumnę. Więcej przykładów zaprezentujemy w dalszej części rozdziału.

Stronicowanie wyników Pakiet Structures_DataGrid korzysta z klasy PAGER, by za jej pomocą dodawać do stron WWW mechanizm stronicowania (dzielenia na strony) prezentowanych wyników, podobny do tego stosowanego przez wyszukiwarkę Google. Aby ograniczyć liczbę wyników wyświetlanych na każdej stronie, wystarczy po prostu przesłać konstruktorowi liczbę rekordów, które mają być wyświetlane na pojedynczej stronie. $dg =& new Structures_DataGrid('2');

Po wyświetleniu naszej siatki DataGrid konieczne będzie wyświetlenie łączy (kontrolek) umożliwiających użytkownikom przenoszenie się do danych, które nie zostaną wyświetlone na pierwszej stronie. echo $dg->renderer->getPaging();

W ten sposób przywołujemy renderer HTML i wyświetlamy odpowiednie kontrolki stronicowania. Stronicowanie zależeć będzie od wykorzystywanego renderera. Obecnie, niestety, tylko renderer HTML obsługuje stronicowanie.

Korzystanie ze źródła danych DataSource W prawdziwym programie nie dodawalibyśmy jednak danych za pomocą tablicy z danymi, lecz raczej pobieralibyśmy je z jakiegoś zewnętrznego źródła. W tym celu korzystać będziemy z oferowanych DataGrid sterowników DataSource. Aby utworzyć nowe źródło danych DataSource, użyjemy metody create() z klasy Structures_DataGrid_DataSource. Metoda ta wymaga przesłania jej trzech parametrów. Pierwszy parametr wskazuje do lokalizacji danych, drugi przesyła tablicę zawierającą opcje specyficzne dla sterownika, a wreszcie trzeci podaje stałą, która definiuje sterownik źródła danych DataSource. 87

PEAR. Programowanie w PHP

W tym przykładzie skorzystamy ze sterownika CSV źródła danych DataSource, by za jego pomocą odczytać dane z bazy danych klientów, która została eksportowana z naszej książki adresowej. require_once 'Structures/DataGrid.php'; require_once 'Structures/DataGrid/DataSource.php'; $opt = array('delimiter' => ',', 'fields' => array(0, 1, 2), 'labels' => array("First Name", "Last Name", "Email"), 'generate_columns' => true); $data = Structures_DataGrid_DataSource::create('data.csv', $opt, DATAGRID_SOURCE_CSV); $dg =& new Structures_DataGrid(); $dg->bindDataSource($data); $dg->render();

Opcje określają, czego będziemy używać w charakterze delimitatora (symbolu oddzielającego) pola, jakie pola chcemy dołączyć do naszej siatki DataGrid, jakie są etykiety pól i na koniec, czy chcemy tworzyć kolumny z nagłówkami. O ręcznym generowaniu nagłówków opowiemy dokładniej w dalszej części książki, na razie ustawienia widoczne w kodzie powinny nam wystarczyć. Powiążemy nasze dane z siatką DataGrid za pomocą metody bindDataSource(), a następnie renderujemy otrzymaną siatkę z danymi.

Korzystanie z renderera Teraz, gdy już mamy naszą siatkę danych DataGrid, która pobiera dane z pliku CSV i wyświetla je w postaci siatki danych HTML, możemy skorzystać z rendererów pakietu Structures_DataGrid, by za ich pomocą wyeksportować dane do dokumentu Excela. Zrobimy to zmieniając następujące wiersze z poprzedniego przykładu. // Instruujemy pakiet Structures_Datagrid, by korzystał z renderera XLS

$dg =& new Structures_DataGrid(null, null, DATAGRID_RENDER_XLS); // Określamy nazwę pliku, z którego będziemy korzystać

$dg->renderer->setFilename('datagrid.xls'); // Wiążemy dane i renderujemy wynik

$dg->bindDataSource($data); $dg->render();

Mamy już zatem w pełni funkcjonalny mechanizm konwertujący dane z formatu CSV na XLS. Niestety renderer formatu XLS nie może korzystać ze wszystkich funkcji oferowanych przez pakiet Spreadsheet_Excel_Writer, by za ich pomocą formatować wiersze i nagłówki arkusza, ale dobre i to. Aby skorzystać z innych rendererów, wystarczy po prostu zmienić stałą przesyłaną w konstruktorze klasy Structures_DataGrid.

88

Rozdział 2. • Wyświetlanie danych

Konstruktor klasy Structures_DataGrid wymaga przesłania mu trzech parametrów. Pierwszy parametr ogranicza liczbę wyników wyświetlanych na bieżącej stronie, drugi parametr określa, która strona jest wyświetlana, a trzeci, którego z rendererów używamy.

Estetyczne formatowanie siatki danych Teraz, gdy mamy już obiekt Structures_DataGrid, wykonujący dokładnie to, czego od niego chcemy, pora zadbać, aby wyświetlane wyniki wyglądały na tyle estetycznie, by zrobić odpowiednie wrażenie na kierownictwie. Każdy z rendererów oferuje wiele różnych opcji formatowania. My skorzystamy z domyślnego renderera HTML_Table i wstawimy do ostatniego skryptu kilka opcji. $dg =& new Structures_DataGrid(2, null, DATAGRID_RENDER_TABLE); $dg->renderer->setTableHeaderAttributes(array('bgcolor' => '#3399FF')); $dg->renderer->setTableOddRowAttributes(array('bgcolor' => '#CCCCCC')); $dg->renderer->setTableEvenRowAttributes(array('bgcolor' => '#EEEEEE')); // Definiujemy atrybuty tabeli DataGrid

$dg->renderer->setTableAttribute('width', '100%'); $dg->renderer->setTableAttribute('cellspacing', '1'); // Określamy ikony sortowania

$dg->renderer->sortIconASC = '↑'; $dg->renderer->sortIconDESC = '↓'; $dg->bind($data); $dg->render();

Jak łatwo zauważyć, podczas instancjacji klasy (tworzenia obiektu) przesyłamy klasie Structures_DataGrid kilka dodatkowych parametrów. Drugi atrybut określa, którą stronę chcemy wyświetlić, a trzeci definiuje sterownik wykorzystywany przez nas do renderowania siatki DataGrid. Oczywiście nie ma potrzeby wyraźnego definiowania renderera DATAGRID_RENDERER_TABLE, ponieważ jest to domyślny renderer, niemniej przesłaliśmy konstruktorowi odpowiedni parametr dla większej czytelności przykładu. Po utworzeniu instancji obiektu DataGrid możemy zacząć bawić się z rendererem. Jak widać z naszego przykładu, możemy definiować indywidualne atrybuty tabeli, nagłówków lub nawet wierszy parzystych i nieparzystych. Renderer HTML_Table jest jednym z najbardziej dopracowanych rendererów i oferuje o kilka opcji formatowania więcej niż inne. My wykorzystaliśmy tutaj tylko kilka z nich.

89

PEAR. Programowanie w PHP

Zanim zakończymy nasz przykład, dodamy jeszcze ikony umożliwiające sortowanie danych, które pojawiać się będą w momencie sortowania nagłówka określonej kolumny. Skorzystamy tutaj z dostępnych w języku HTML elementów „Strzałki w górę” (Up Arrow) i „Strzałki w dół” (Down Arrow). Warto zauważyć, że możemy tutaj wprowadzić dowolny kod HTML, tak więc można również używać odpowiednich obrazków.

Poszerzanie możliwości DataGrid Prezentowany wyżej kod określający atrybuty tabeli był dość długi i raczej kłopotliwe byłoby wpisywanie go za każdym razem, gdy trzeba wyświetlić jakąś siatkę DataGrid. Aby rozwiązać ten problem, możemy utworzyć klasę, która poszerzy możliwości pakietu Structures_DataGrid, tak aby za każdym razem gdy przywoływać będziemy naszą nową klasę, automatycznie używane były wszystkie określone przez nas atrybuty renderowania. require 'Structures/DataGrid.php'; class myDataGrid extends Structures_DataGrid { function myDataGrid($limit = null, $page = 0) { parent::Structures_DataGrid($limit, $page); $this->renderer->setTableAttribute('width', '100%'); // ... Tutaj wpisujemy resztę kodu formatującego ...

$this->renderer->sortIconDESC = '↓'; } } $dg =& myDataGrid();

Teraz za każdym razem gdy będziemy tworzyć nową instancję obiektu myDataGrid, wszystkie atrybuty tabeli zostaną odpowiednio ustawione, a my będziemy mieli jedno, centralne miejsce, w którym będziemy mogli zmieniać wygląd siatek DataGrid używanych w naszym projekcie. Jeśli natomiast w naszej witrynie korzystamy z kilku różnych siatek danych DataGrid, wygodniejszym rozwiązaniem będzie utworzenie kilku różnych klas, które rozszerzać będą zachowanie klasy Structures_DataGrid na kilka różnych sposobów, zależnie od potrzeb każdej konkretnej siatki danych. Po czym będą dokonywać instancjacji klasy używając prostego mechanizmu fabryki. function getDGInstance($type) { if (class_exists($type)) { $datagrid =& new $type; return $datagrid; } else {

90

Rozdział 2. • Wyświetlanie danych

return false; } } $dg = getDGInstance('myDataGrid'); // Możemy utworzyć kolejną instancję siatki DataGrid używając // osobnej, rozszerzonej klasy, takiej jak ta

$dg = getDGInstance('myDataGrid2');

Zaprezentowany tu przykład jest bardzo prosty, niemniej dobrze ilustruje, jak łatwe jest tworzenie wielu różnych instancji klas rozszerzających możliwości klasy Structures_DataGrid.

Dodawanie kolumn Kolumny naszej siatki danych DataGrid są tak naprawdę instancjami klasy Structures_DataGrid_Column. W poprzednich przykładach siatka DataGrid obsługiwała dla nas kolumny automatycznie za kulisami, tak więc nie musieliśmy sami tworzyć kolumn. Niemniej jeśli chcemy dodać do siatki DataGrid nowe kolumny, trzeba to zrobić ręcznie. W tym przykładzie skorzystamy ze sterownika RSS źródła Danych DataSource, by pobrać dane z zewnętrznego pliku RSS, a następnie wyświetlić je w połączeniu z kilkoma kolumnami dodanymi przez nas. require_once 'Structures/DataGrid/DataSource.php'; // Określamy kolumny RSS, z których chcemy skorzystać

$options = array('fields' => array('title', 'link')); $rss = "http://rss.slashdot.org/Slashdot/slashdot"; $ds = Structures_DataGrid_DataSource::create($rss, $options, DATAGRID_SOURCE_RSS); // Tworzymy instancję naszej rozszerzonej klasy DataGrid

$dg =& new myDataGrid; // Tworzymy dwie kolumny

$titleCol = new Structures_DataGrid_Column('Title', 'title'); $funcCol = new Structures_DataGrid_Column('Function', null); // Dodajemy funkcje formatujące

$titleCol->setFormatter('printLink()'); $funcCol->setFormatter('sendLink()'); // Dodajemy kolumny do siatki danych DataGrid

$dg->addColumn($titleCol); $dg->addColumn($funcCol); // Wiążemy zbiór danych DataSet z siatką DataGrid i renderujemy wynik

$dg->bindDataSource($ds); $dg->render();

91

PEAR. Programowanie w PHP

Większość z prezentowanego tu kodu mieliśmy okazję widzieć już we wcześniejszych przykładach. Najpierw tworzymy źródło danych DataSource używając sterownika RSS, a następnie definiujemy opcje umożliwiające wyświetlanie pól tytułu i łącza. Ciekawy staje się dopiero fragment kodu, w którym tworząc instancje klasy Structures_DataGrid_Columns tworzymy nasze kolumny. Podczas tej czynności przesyłamy parametry określające nazwę kolumny oraz pole, z którym jest ona powiązana. Konstruktor klasy Structures_DataGrid_Column pozwala na przesyłanie mu oprócz tego jeszcze kilku innych wartości, takich jak: opcje formatowania, atrybuty tabeli, wartości automatycznego wypełnienia, czy wartości określające sposób sortowania, niemniej tutaj zrezygnowaliśmy z nich, by nasz przykład był możliwie jak najprostszy. Chcielibyśmy dodać kilka specjalnych opcji do kolumn zawierających adres URL i naszą funkcję, dlatego też skorzystamy z metody setFormatter(), by za jej pomocą wskazać funkcje, które będą formatować konkretne kolumny. Nasz kod będzie teraz wyglądał tak: function printLink($params) { $data = $params['record']; return "$data[title]"; } function sendLink($params) { $data = $params['record']; $link = urlencode($data["link"]); return "Prześlij łącze znajomemu"; }

Zmienna $params jest tablicą zawierającą wszystkie dane z bieżącego rekordu ze zbioru danych. Umieszczamy dane rekordu w zmiennej $data, a następnie zwracamy dane sformatowane tak, jak chcemy, w kolumnie naszej siatki danych DataGrid. W tym przypadku mamy tylko dwie kolumny. Pierwsza jest łączem do odpowiedniego artykułu, a druga formatuje adres URL i łączy go z odpowiednim skryptem, tak aby użytkownik mógł przesłać to łącze znajomemu.

Generowanie plików PDF Pisząc o formatach plików, nie sposób nie wspomnieć o formacie PDF (Portable File Format, format przenośnych plików), mającym wśród formatów plików taką pozycję, jak trzystukilogramowy goryl wśród małp. Pierwotnie był to wewnętrzny format plików wykorzystywany w programach graficznych firmy Adobe, z czasem jednak zdobył sporą popularność wśród użytkowników, dzięki temu, że rozwiązywał pewien bardzo ważny problem: tworząc dokument w formacie PDF użytkownik ma pewność, że będzie on wyglądał dokładnie w ten sam sposób, niezależnie w jakim systemie operacyjnym będzie oglądany.

92

Rozdział 2. • Wyświetlanie danych

Niestety przenośność dokumentów PDF pociąga za sobą pewne koszty. Jest to mianowicie bardzo złożony format, a zatem niezmiernie trudny do rozszyfrowania, nawet dla tych specjalistów, którym udało się przebrnąć przez jego pełną, liczącą ponad 1000 stron specyfikację. Na szczęście dla użytkowników PEAR, nie muszą oni czytać całej tej długiej technicznej specyfikacji modułu i mogą po prostu skorzystać z biblioteki File_PDF, która zadba o wszystko, co jest związane z tworzeniem dokumentów PDF. Za pomocą tego prostego interfejsu API programista może zrealizować większość zadań związanych z dokumentami PDF, takich jak: wyświetlanie tekstu, rysowanie linii i innych obiektów, wyświetlanie obrazów, zapisywanie danych w tabelach itp. Tutaj zaprezentujemy kod generujący prosty list biznesowy za pomocą biblioteki File_PDF. Dla uproszczenia tego w pełni jednak funkcjonalnego przykładu skorzystaliśmy z funkcji setXY(). Funkcja ta definiuje punkt startowy w miejscu określonym przez współrzędne X i Y. Podczas tworzenia większego dokumentu lub dokumentu prezentującego dynamiczną zawartość trzeba będzie jednak zapewne skorzystać z którejś z bardziej zaawansowanych metod wstawiania zawartości, opisanych dalej przy okazji omawiania komórek. require_once "File/PDF.php"; $company_name = "Wormus Consulting"; $my_address = "123 Aaron Way, Gotham City, 12421 RQ, USA"; // Określamy początkowe marginesy

$lm $rm $tm $bm

= = = =

22; 22; 22; 22;

$padding = 10; $pdf = File_PDF::factory(); $pdf->open(); // Można to też zrobić za pomocą metody setMargins

$pdf->setLeftMargin($lm + $padding); $pdf->setRightMargin($rm + $padding); $pdf->addPage(); // Określamy czcionkę tytułu

$pdf->setFont('Arial', 'B', '12'); $pos = $tm + $padding; $pdf->setXY(10, $pos); // Wyświetlamy nazwę firmy

$pdf->cell(0, $padding, $company_name, null, 0, 'R'); $pdf->setFont('Arial', 'B', '8'); $pos += 10; $pdf->setXY(10, $pos);

93

PEAR. Programowanie w PHP

$pdf->cell(0, 0, $my_address, null, 1, 'R'); $pos += 3; $pdf->setXY($lm, $pos); $pdf->line($lm + $padding, $pos, 210 - $rm - $lm, $pos); $pos += 10; $pdf->setXY($lm, $pdf->newLine(); $pdf->write('4', $pdf->newLine(); $pdf->write('4', $pdf->newLine(); $pdf->write('4', $pdf->newLine(); $pdf->write('4', $pdf->newLine(); $pos += 20; $pdf->setXY($lm, $pdf->newLine();

$pos); "John Smith"); "122 Peters Lane"); "32235 City, State"); "Country"); $pos);

$pdf->write('4', "To whom it may Concern:"); $pos += 6; $pdf->setXY($lm, $pos); $pdf->newLine(); // Tekst skróciliśmy dla wygody czytelnika

$text = "Lorem ipsum dolor ... porta eleifend. "; $pdf->MultiCell(210 -$lm -$rm - $padding *2, 3, $text, null, "J"); $pdf->newLine(10); $pdf->write("10", "Best Regards,"); $pdf->output();

Tan prosty przykład prezentuje niektóre z możliwości biblioteki File_PDF i tworzy zupełnie profesjonalny list biznesowy (rysunek 2.6). Po załączeniu głównego pakietu sam proces tworzenia nowej strony będzie już bardzo prosty. Metoda fabryki (factory) utworzy nową instancję klasy File_PDF. Można jej ponadto przesłać kilka parametrów: $pdf = File_PDF::factory(array('orientation' => 'P', 'unit' => 'mm', 'format' => 'A4'));

Tutaj określiliśmy, że dokument ma mieć orientację portretową (wertykalną), jednostkami miary mają być milimetry, a format dokumentu to A4. Są to akurat ustawienia domyślne parametrów, więc w tym przypadku nie zachodziła potrzeba ręcznego określania tych ustawień dokumentu.

94

Rozdział 2. • Wyświetlanie danych

Rysunek 2.6. Przykładowy wygenerowany plik PDF

Gdy już będziemy mieli nową instancję klasy, będziemy mogli przywołać metodę open(), by uruchomić dokument, a następnie dodać do niego stronę. Podczas tworzenia dokumentu najpierw przywoływane są metody nagłówka i stopki header() i footer(). Więcej na ich temat — w dalszej części rozdziału. Gdy będziemy już mieli gotową stronę, można zacząć dodawać do niej dane.

Kolory W naszym prostym przykładzie nie zmienialiśmy kolorów, niemniej warto wiedzieć, że dodawanie kolorów do dokumentów PDF nie jest niczym trudnym. Biblioteka File_PDF oferuje dwie funkcje, które umożliwiają dodawanie kolorów do dokumentu. Kiedy określamy kolor dokumentu, to informujemy, że chcemy używać danego koloru od miejsca, w którym został zainicjowany, aż do końca strony. Kiedy biblioteka File_PDF tworzy nową stronę, dokonuje ponownej instancjacji ustawionych opcji koloru, dlatego też, o ile nie zmienimy koloru albo nie przywrócimy poprzedniej wartości koloru, to raz ustawiony kolor stosowany będzie aż do końca dokumentu.

95

PEAR. Programowanie w PHP

Dwie funkcje, które w tym celu wykorzystujemy, to setDrawColor() i setFillColor(). Każda z tych funkcji stosuje pierwszy parametr, by ustalić, z jakiej przestrzeni barw korzystamy (rgb, cymk czy szarej, gray), a kolejne parametry służą do definiowania wartości każdego z używanych kolorów. Funkcja setDrawColor() określa kolor wykorzystywany w rysowanych liniach, a funkcja setFillColor() określa kolor tekstu, obszarów oraz komórek, które nie mają przezroczystego tła. $pdf->setDrawColor("rgb", 0, 0, 255); $pdf->setFillColor("rgb", 255, 0, 0);

Dodanie tych dwóch wierszy na początku pliku sprawi, że w naszym dokumencie tekst będzie czerwony, a linie w kolorze niebieskim.

Czcionki Podobnie jak w przypadku określania kolorów, ustawienia czcionek będą również stosować się do całego dokumentu, począwszy od punktu, w którym parametry czcionki zostały określone. W tym przykładzie czcionka została zdefiniowana jako 8-punktowa czcionka Arial. $pdf->setFont("Arial", "B", 8);

Standardowy zestaw czcionek dostępnych w większości systemów operacyjnych został zdefiniowany w bibliotece File_PDF. Programiści, którzy chcieliby skorzystać z innych czcionek, powinni upewnić się, czy są one dostępne w danym systemie. Jeśli nie, to należy przekonwertować je na czcionki Type1 lub TrueType i następnie dodać do systemowego zestawu czcionek. Opis procedury konwertowania i dodawania czcionek wykracza poza zakres tego rozdziału, warto jednak wiedzieć, że wymaga to utworzenia pliku definicji czcionki korzystając z załączonego narzędzia makefont.php, a następnie załadowania danych czcionki za pomocą funkcji setFont().

Komórki Prostym sposobem zapisania danych posiadających pewną strukturę w formacie PDF jest zapisanie ich w komórkach. Komórka to po prostu prostokątny obszar, do którego można wstawić tekst i dla którego opcjonalnie określić można obramowania i kolor tła. $pdf->cell(0, $padding, $company_name, null, 0, 'R');

Pierwszy z widocznych tu parametrów to szerokość komórki. Jeśli przypiszemy jej wartość zero, to komórka rozciągnie się aż do prawego marginesu. Drugi parametr określa wysokość komórki, a trzeci parametr tekst, który ma zostać wyświetlony w komórce. Czwarty parametr określa, czy należy narysować obramowanie komórki. Przypisanie mu wartości null sprawi, że

96

Rozdział 2. • Wyświetlanie danych

komórka nie będzie miała widocznego obramowania. Można również określić, które z boków komórki powinny zostać wyrysowane, używając do tego celu piątego parametru. W kolejnym przykładzie rysujemy obramowanie tylko dla lewego i prawego boku komórki. $pdf->cell(0, $padding, $company_name, null, 0, "LR", 'R', 0, "http://example.com");

Kolejny parametr określa, że komórka powinna być justowana do prawej, a siódmy (opcjonalny) parametr pozwala ustalić, czy tło komórki ma być przezroczyste, czy też pomalowane określonym kolorem tła. Na koniec wreszcie możemy określić łącze, do którego komórka ta powinna kierować po jej kliknięciu, oraz utworzyć identyfikator łącza używając funkcji addLink() i umieścić w ten sposób w komórce zamiast adresu URL przypisany mu identyfikator.

Tworzenie nagłówków i stopek Biblioteka File_PDF została tak zaprojektowana, aby pozwalać programistom na rozszerzanie możliwości pakietu, na przykład by dodawać nagłówki i stopki podczas tworzenia każdej ze stron. Aby użyć odpowiednich w tym celu metod, trzeba będzie utworzyć nową klasę, z której skorzystamy podczas generowania naszych dokumentów PDF. class My_File_PDF extends File_PDF { function header() { // Wybieramy czcionkę Arial bold (pogrubioną), rozmiar 15

$this->setFont('Arial', 'B', 15); // Przenosimy się w prawo

$this->cell(80); // Tytuł w ramkach

$this->cell(30, 10, 'Title', 1, 0, 'C'); // Łamanie wiersza

$this->newLine(20); } }

To tylko jeden z przykładów rozszerzania możliwości biblioteki File_PDF, by pokryć domyślnie oferowane przez nią funkcje. Podczas korzystania z biblioteki File_PDF w naszych projektach często zachodzić będzie konieczność rozszerzania możliwości bazowej klasy, by dodać jakieś nowe funkcje. Więcej na ten temat można dowiedzieć się czytając dokumentację i zapoznając się z przykładami kodu rozprowadzanymi wraz z pakietem biblioteki File_PDF.

97

PEAR. Programowanie w PHP

Podsumowanie Rozdział ten pokazywał, jak za pomocą różnych pakietów dostępnych w repozytorium PEAR można wyświetlać dane na stronach WWW. Niemniej prezentowane przez nas przykłady obejmowały tylko niewielką część możliwości oferowanych przez te pakiety. Warto też wiedzieć, że w repozytorium PEAR można znaleźć również i inne pakiety, pozwalające na odczytywanie i zapisywanie danych w innych, często nietypowych formatach, takich jak vcards czy bibTeX. Znaleźć tam również można użyteczne parsery, pozwalające na odczytywanie i zapisywanie danych na przykład w formacie Wiki (Wikipedii) oraz wiele, wiele innych narzędzi, o których nie wspomnieliśmy w tym rozdziale.

98

3 Praca z formatem XML Język XML w ciągu ostatnich lat coraz bardziej zyskuje na popularności. Prawdę powiedziawszy, w nowej wersji języka PHP, mianowicie PHP5, obsługa formatu XML została całkowicie zmodernizowana i obecnie oparta jest na bibliotekach libxml2 oraz libxsl, które praktycznie w całości stosują się do standardów i zaleceń W3C. Popularność XML nie jest tylko i wyłącznie wynikiem szumu medialnego. W niektórych sytuacjach język XML jest zdecydowanie najlepszym rozwiązaniem. Jeśli chcemy na przykład przechowywać hierarchiczne struktury danych, takie jak struktura i zawartość strony w systemie zarządzania zawartością, to format XML idealnie nadaje się do tego zadania. Niemniej systemy zarządzania zawartością to nie jedyne miejsce, gdzie format XML okazuje się pomocny. Nawet gdy projektujemy małą, prostą aplikację WWW, możemy wykorzystać format XML w naszych plikach konfiguracyjnych. W ten sposób staną się one bardziej wszechstronne i łatwiej je będzie rozwijać, jeśli dodamy nowe funkcje. Dokument XML zawiera nie tylko pary klucza i wartość, tak jak w standardowej konfiguracji plików INI, dodatkowo wartości są zawsze powiązane z pozostałą zawartością poprzez ich pozycję w strukturze drzewa dokumentu XML. Kolejnym typowym zastosowaniem formatu XML jest wymiana danych między różnymi firmami, aplikacjami lub serwerami. Jeden ze scenariuszy wymiany danych za pomocą formatu XML został opisany w rozdziale 4., ponieważ praktycznie wszystkie współczesne usługi WWW wykorzystują język XML jako swój podstawowy format danych. Wielość zastosowań nie jest jedyną zaletą tego prostego, lecz bardzo użytecznego formatu. Dzięki jego podobieństwu do formatu HTML ludzie nie mają problemów z odczytywaniem i interpretowaniem jego plików. W odróżnieniu jednak od formatu HTML, XML opiera się na prostszych zasadach, dzięki czemu jest łatwiejszy do przetwarzania dla komputerów i aplikacji. Co więcej, format XML zapewnia projektantowi znacznie więcej swobody niż format HTML. Podczas gdy język HTML definiuje, jakich znaczników należy używać w dokumencie, język XML definiuje tylko pewne podstawowe zasady, do których dokument musi się stosować. Pozostawia programiście swobodę wyboru, których znaczników użyć w dokumencie

PEAR. Programowanie w PHP

oraz w jaki sposób aplikacja przetwarzająca dokument ma je interpretować. Tworzenie nowej aplikacji opartej na XML polega po prostu na zdefiniowaniu zestawu znaczników, które będą wykorzystywane w dokumencie. Obecnie istnieje już kilka takich opartych na formacie XML aplikacji: XHTML (kompatybilna z XML wersja języka HTML), SGV (Scaleable Vector Graphics, skalowalna grafika wektorowa), XML Schema (język XML definiujący zasady dla innych aplikacji XML), czy też XUL — język wykorzystywany przez przeglądarkę Mozilla do tworzenia swojego interfejsu użytkownika. Ponadto warto wiedzieć, że aby stworzyć własną aplikację XML, nie trzeba należeć do żadnej organizacji ani oficjalnego komitetu. Może to zrobić każdy, kto uważa, że taka aplikacja jest mu potrzebna.

Pakiety PEAR wspomagające pracę z XML Ponieważ język XML cieszy się coraz większym zainteresowaniem programistów, nawet tych pracujących na co dzień w języku PHP, nic więc dziwnego, że kategoria XML jest jedną z najszybciej rozwijających się kategorii dostępnych w repozytorium PEAR. W czasie gdy powstawała ta książka, repozytorium PEAR oferowało 28 różnych pakietów (nie wliczając w to usług WWW) wspomagających tworzenie i przetwarzanie dokumentów XML. W tym rozdziale opowiemy o najważniejszych pakietach dostępnych w tej kategorii. Rozdział będzie podzielony na dwie części. Podczas gdy w pierwszej części zostanie pokazane, jak można wykorzystać repozytorium PEAR do tworzenia od podstaw dokumentów, XML, druga część omawiać będzie rozpatrywanie i analizowanie już istniejących dokumentów. Na kolejnych stronach dowiemy się, jak korzystać z pakietów XML_Util lub XML_FastCreate, by za ich pomocą przekształcać drzewo obiektów w poprawny dokument XML, przeglądając po kolei dane. Następnie nauczymy się korzystać ze znakomitego pakietu XML_Serializer umożliwiającego tworzenie dokumentów XML na podstawie dowolnych przesłanych mu danych. Jak się przekonamy, dzięki pakietom tym tworzenie dokumentów XML za pomocą repozytorium PEAR staje się dziecinnie proste, niezależnie od tego, czy mamy dane zorganizowane w tablice, obiekty, czy jakiekolwiek inny format danych źródłowych. W drugiej części tego rozdziału wykorzystamy pakiet XML_Parser do tworzenia programu odczytującego dane konfiguracyjne, który będzie mógł pobierać informacje z dokumentu XML i przygotuje obiektowy interfejs API do konfiguracji. W następnej kolejności skorzystamy z klasy XML_Unserializer, wchodzącej w skład pakietu XML_Serializer, by za jej pomocą konwertować różne dokumenty XML na zagnieżdżone tablice i struktury obiektów. Wykorzystamy również tę klasę do odczytywania tych samych danych konfiguracyjnych w formacie XML, które przetwarzaliśmy przy użyciu pakietu XML_Parser, niemniej tym razem nie przejmując się analizą formatu XML. Na koniec wreszcie przyjrzymy się pakietowi XML_RSS, by nauczyć się, jak pobierać do naszej aplikacji PHP najświeższe wiadomości pochodzące z dowolnej witryny WWW oferującej informacje w którymś z formatów RSS. Zanim rozpoczniemy pracę z repozytorium PEAR, przyjrzyjmy się najpierw regułom obowiązującym w dokumentach XML.

100

Rozdział 3. • Praca z formatem XML

Tworzenie dokumentów XML Na pierwszy rzut oka tworzenie dokumentów XML wydaje się dziecinnie proste. W końcu dokument składa się głównie ze znaczników w prostej formie tekstowej, więc w zasadzie nie różni się wiele od dokumentu HTML i powinniśmy móc bez problemu korzystać tutaj z narzędzi sklejania lub funkcji języka PHP przeznaczonych do pracy na łańcuchach. Niemniej istnieje kilka spraw, o których często zapomina się podczas tworzenia dokumentów XML, a które potem mogą stać się przekleństwem gotowej, produkcyjnej aplikacji. Są one ściśle związane z regułami, do których musi się stosować dokument XML: „ Format XML (w odróżnieniu od formatu HTML) jest wrażliwy na wielkość liter używanych w znacznikach, tak więc znacznik nie będzie równoważny znacznikowi . „ Każdy znacznik XML musi być domknięty; jeśli określony znacznik ze swej natury nie zawiera żadnych danych, to można pominąć drugi znacznik, a sam znacznik można zapisać w formie znacznika pustego elementu. Oznacza to, że należy używać znacznika
zamiast
. „ Znaczniki muszą być prawidłowo zagnieżdżone i dlatego ostatni otwarty znacznik musi być domykany jako pierwszy. Dla przykładu kod Clark Kentto Superman jest oczywiście nieprawidłowy, natomiast kod Clark Kent to Superman będzie całkowicie poprawny. „ Każdy dokument XML musi posiadać dokładnie jeden element nadrzędny, który otwierany jest u góry dokumentu i zamykany jako ostatni ze znaczników dokumentu. „ Specjalne znaki &, , " oraz ' muszą być zastępowane odpowiadającymi im jednostkami (kodami) XML, odpowiednio: &, , " i ', jeśli używane są jako część danych lub wartości atrybutów. Są to jedyne jednostki, których można używać w dokumentach XML bez konieczności ich wcześniejszego deklarowania. „ Wszystkie atrybuty muszą być ujmowane w cudzysłowy: albo podwójne, albo pojedyncze. „ Dokument musi być zgodny ze swoją definicją zestawu znaków. Zestaw znaków definiowany jest w deklaracji XML, która poprzedza właściwą część dokumentu XML. Jeśli dokument przesyłany jest za pośrednictwem protokołu HTTP, to zestaw znaków można również zdefiniować w nagłówku dokumentu. Jeśli nie zostanie określone żadne kodowanie, stosuje się domyślne kodowanie UTF-8. Dokument XML, który spełnia wszystkie te wymagania, nazywany jest prawidłowo sformułowanym (ang. well formed) dokumentem XML. Oto przykład takiego prawidłowo sformułowanego dokumentu XML.



101

PEAR. Programowanie w PHP

Elvis Presley

That's All Right (Mama) & Blue Moon Of Kentucky



Good Rockin' Tonight



Carl Perkins

Gone, Gone, Gone





Jak widać, przygotowanie dokumentu XML przy użyciu tylko podstawowych funkcji i narzędzi języka XML służących do pracy na łańcuchach nie jest wcale taką prostą sprawą. Na kolejnych stronach dowiemy się, jak generować dokumenty XML za pomocą kilku użytecznych pakietów z repozytorium PEAR.

Tworzenie obiektów przechowujących informacje o nagraniach Zanim jednak sięgniemy po pakiety PEAR, by za ich pomocą utworzyć dokument XML, przygotujmy najpierw strukturę danych PHP, którą będziemy mogli wykorzystać jako pojemnik przechowujący rzeczywiste dane potrzebne do wygenerowania dokumentu XML. Jeśli przyjrzymy się uważniej zaprezentowanemu wcześniej dokumentowi, przekonamy się, że zawiera on informacje o trzech różnych elementach: etykiecie studia (Sun Records), artystach, którzy nagrali dane utwory (Elvis Presley i Carl Perkins), i wreszcie o albumach, które ci wy-

102

Rozdział 3. • Praca z formatem XML

konawcy nagrali. Dlatego też najpierw implementujemy klasy, które będzie można wykorzystać do przechowywania właściwości tych trzech elementów. Ponieważ główny, bazowy element to etykieta studia, zaczniemy więc od klasy etykiety Label: /** * Przechowuje informacje o etykiecie studia (albumu) * i artyście, który go nagrał */

class Label { public $name = null; public $artists = array(); public function __construct($name) { $this->name = $name; } public function signArtist(Artist $artist) { // pobieramy kolejny, wyższy identyfikator

$artist->setId(count($this->artists)+1); $this->artists[] = $artist; } }

Oprócz właściwości nazwy $name klasa ta posiada również właściwość $artists, w której później będziemy przechowywać nazwiska artystów. Nazwa etykiety przesyłana jest jako parametr konstruktorowi, a dodawanie nowego artysty do listy umożliwia metoda signArtist(). Metoda ta akceptuje jako argument instancję klasy Artist, którą napiszemy w następnej kolejności: /** * Przechowuje informacje o artyście * i nagranych przez niego albumach */

class Artist { public $id = null; public $name = null; public $records = array();

}

public function __construct($name) { $this->name = $name; } public function setId($id) { $this->id = $id; } public function recordAlbum(Record $album) { $this->records[] = $album; }

Tak jak poprzednio, konstruktor klasy wykorzystywany jest do definiowania nazwy, tym razem nazwiska artysty, a metoda recordAlbum() umożliwia dodanie do listy nagranych albumów nowej instancji klasy Album. Klasa ta zawiera również metodę setId(), która przywoływana

103

PEAR. Programowanie w PHP

jest przez obiekt Label, kiedy do listy artystów dodawany jest kolejny artysta. Na koniec wreszcie będziemy musieli zaimplementować klasę Record, która przechowuje wszystkie informacje o nagranym albumie: /** * Przechowuje informacje o albumie. */

class Record { public $id = null; public $name = null; public $released = null;

}

public function __construct($id, $name, $released) { $this->id = $id; $this->name = $name; $this->released = $released; }

Teraz gdy wszystkie klasy pojemników zostały już zaimplementowane, przygotowanie samej struktury danych jest już bardzo proste: // tworzymy nową etykietę

$sun

= new Label('Sun Records');

// tworzymy nowego artystę

$elvis

= new Artist('Elvis Presley');

// dodajemy artystę do listy artystów

$sun->signArtist($elvis); // dodajemy informacje o dwóch albumach

$elvis->recordAlbum( new Record('SUN 209', 'That\'s All Right (Mama) & Blue Moon Of Kentucky', 'July 19, 1954' ) ); $elvis->recordAlbum( new Record('SUN 210', 'Good Rockin\' Tonight', 'September, 1954' ) ); // Tworzymy drugiego artystę i informacje o kolejnym albumie

$carl = new Artist('Carl Perkins'); $carl->recordAlbum( new Record('SUN 224', 'Gone, Gone, Gone', 'October 22, 1955' ) );

104

Rozdział 3. • Praca z formatem XML

// Dodajemy artystę do etykiety studia

$sun->signArtist($carl); // tworzymy listę etykiet (jeśli mamy więcej // niż jedną etykietę)

$labels = array($sun);

Po utworzeniu nowego obiektu etykiety Label możemy bez większych problemów dodać tyle obiektów artystów Artist, ile zechcemy, a dla każdego artysty dowolną liczbę obiektów albumów Record. Jeśli więc dane etykiety studia będą przechowywane w bazie danych, będziemy mogli bez problemu przygotować skrypt, który pobierze dane i zbuduje odpowiednią strukturę korzystając z tych trzech klas. Jeśli teraz gotowa struktura zostanie wyświetlona na ekranie za pomocą funkcji print_r(), to otrzymamy następujący wynik: Array { [0] => Label Object { [name] => Sun Records [artists] => Array { [0] => Artist Object { [id] => 1 [name] => Elvis Presley [records] => Array { [0] => Record Object { [id] => SUN 209 [name] = That’s All Right... [relased] => July 19, 1954 } } } [1] => Artist Object { [id] => 2 [name] => Carl Perkins [records] => Array { [0] => Record Object { [id] => SUN 224 [name] => Gone, gone, Gone [relased] => Julu 19 1954 } } } } } }

Dane wyświetlane przez funkcję print_r() zostały oczywiście dla oszczędności miejsca odrobinę skrócone.

105

PEAR. Programowanie w PHP

Dlaczego nie należy generować dokumentów XML bezpośrednio z danych pobranych z bazy? Czytelnicy mogliby się zastanawiać, dlaczego nasze trzy pomocnicze klasy zostały zaimplementowane jako obiekty wartości, podczas gdy dokument XML mógłby być przecież wygenerowany bezpośrednio z danych pobranych z bazy. Nowe klasy służą jako swego rodzaju narzędzia abstrakcji umożliwiające przechowywanie danych i ich przydatność ujawnia się wtedy, gdy pobieramy dane z innego źródła (warstwy) danych niż baza danych.

Skończyliśmy już tworzyć naszą strukturę danych, pora teraz więc przyjrzeć się paru pakietom PEAR, które można wykorzystać do generowania dokumentów XML na podstawie przygotowanych przez nas danych.

Tworzenie dokumentów XML za pomocą klasy XML_Util XML_Util to narzędziowa klasa przeznaczona do pracy z dokumentami XML. Dostarcza ona kilku metod wykonujących typowe zadania związane z dokumentami XML. Wszystkie te metody można przywoływać statycznie, dlatego też w naszych skryptach nigdy nie będzie konieczności tworzenia nowej instancji klasy XML_Util, aby korzystać z oferowanych przez nią funkcji. Wszystko, co trzeba zrobić, to zadeklarować użycie tej klasy w naszym kodzie: require_once 'XML/Util.php';

Załączona w ten sposób klasa XML_Util oferuje metody umożliwiające: „ Tworzenie kodu XML i deklaracji typu dokumentu. „ Tworzenie znaczników otwierających i zamykających. „ Tworzenie kompletnych znaczników (wraz z zawartością znacznika) oraz innych elementów XML, takich jak komentarze. „ Zastępowanie jednostek XML pojawiających się w dowolnym łańcuchu. „ Tworzenie atrybutów XML bazując na tablicach asocjacyjnych. „ Wspomaganie programisty w innych pracach związanych z kodem XML. Ponieważ zadanie, które teraz przed nami stoi, to utworzenie dokumentu XML z obiektów PHP, pakiet ten wydaje się idealnym rozwiązaniem. Interfejs API wszystkich metod oferowanych przez pakiet XML_Util jest bardzo prosty, tak więc aby wygenerować znacznik otwierający, wystarczy przywołać metodę createStartElement() i przesłać jej nazwę znacznika XML: $label = XML_Util::createStartElement('label');

Jako że metoda ta wygeneruje tylko łańcuch , czytelnik mógłby zastanawiać się, jaki jest właściwie pożytek z korzystania z klasy XML_Util. Jej zalety ujawniają się, gdy chcemy utworzyć znacznik, który zawierać będzie również atrybuty. Można je przesyłać metodzie createStartElemnt() w postaci tablicy asocjacyjnej:

106

Rozdział 3. • Praca z formatem XML

$attributes = array( 'name' => 'Sun Records', 'location' => 'Nashville' ); $label = XML_Util::createStartElement('label', $attributes);

Ten fragment kodu utworzy znacznik otwierający wraz z atrybutami określonymi w tablicy asocjacyjnej. Klasa XML_Util automatycznie posortuje atrybuty alfabetycznie.

Metoda createStartElement() obsługuje również przestrzenie nazw XML — wystarczy po prostu przesłać jej adres URI przestrzeni nazw jako trzeci parametr. Co więcej, mamy również wpływ na to, w jaki sposób znacznik będzie renderowany. Jeśli bowiem znacznik posiada wiele atrybutów, staje się bardzo długi i traci na tym czytelność kodu. Ponieważ spacje są w formacie XML ignorowane, klasa XML_Util może podzielić znacznik między kilka wierszy, umieszczając każdy z atrybutów w swoim własnym wierszu. Oto przykład kodu wykorzystującego w formatowaniu spacji oraz rozdzielający atrybuty pomiędzy wiele wierszy: $attributes = array( 'name' => 'Sun Records', 'location' => 'Nashville' ); $label = XML_Util::createStartElement('records:label', $attributes, 'http://www.example.com', true);

A tak będzie wyglądał utworzony przez niego znacznik:

Klasa XML_Util dostarcza również metody ceateEndElment() umożliwiającej tworzenie znaczników zamykających: $label = XML_Util::createEndElement('label');

Oczywiście metoda ta nie obsługuje żadnych dodatkowych parametrów znacznika, ponieważ znacznik zamykający nie zawiera niczego poza nazwą znacznika. Jeśli natomiast chcemy utworzyć za jednym zamachem znacznik otwierający i zamykający i dodatkowo jeszcze przesłać zawartość tworzonego znacznika, to należy skorzystać z metody createTag(). Podobnie jak metoda createStartElement(), metoda createTag() pobiera jako dwa pierwsze argumenty nazwę znacznika i tablicę z atrybutami znacznika. Niemniej począwszy od trzeciego argumentu metoda ta zaczyna zachowywać się inaczej. Korzystając z metody createTag() możemy w jej trzecim argumencie przesłać zawartość znacznika: $attributes = array( 'name' => 'Sun Records', 'location' => 'Nashville' ); $tag = XML_Util::createTag('label', $attributes, 'Tag content');

107

PEAR. Programowanie w PHP

Metodzie można również przesyłać parę dodatkowych argumentów, które wpływają na to, w jaki sposób znacznik będzie tworzony. Kolejne argumenty można przekazywać w następującym porządku (jeśli nie chcemy przesyłać żadnej konkretnej wartości, należy użyć wartości null): „ Adres URI przestrzeni nazw, jeśli potrzeba. „ Informację, czy zastępować specjalne jednostki(symbole) XML pojawiające się w zawartości znacznika (true), czy też nie (false). Jest to bardzo użyteczna opcja, jeśli znacznik zawierać będzie wewnątrz kolejne znaczniki i nie chcemy, aby specjalne jednostki XML były opatrywane znakami ucieczki. „ Informację, czy rozdzielać atrybuty między różne wiersze (true), czy też nie (false). Jeśli ostatniemu parametrowi zostanie przypisana wartość true, to metodzie można będzie przesłać jeszcze dwa kolejne argumenty określające głębokość wcięcia i sposób dzielenia wierszy podczas rozdzielania atrybutów między różne wiersze. W 99% przypadków najbardziej odpowiednie będą jednak domyślne wartości tych parametrów. Skoro dowiedzieliśmy się już, jak tworzyć znaczniki XML za pomocą klasy XML_Util, to musimy teraz tylko dowiedzieć się, jak tworzyć deklarację XML, i będziemy gotowi, by przystąpić do tworzenia kompletnego dokumentu XML na podstawie naszego drzewa obiektów. Klasa XML_Util oferuje specjalną metodę tworzącą deklarację XML: $decl = XML_Util::getXMLDeclaration('1.0', 'ISO-8859-1');

Metodzie tej można przesyłać trzy parametry: wersję formatu XML, informacje o sposobie kodowania i znacznik logiczny informujący, czy generowany dokument ma być osobnym dokumentem, czy też nie. Cztery opisane tutaj metody w zupełności wystarczają do utworzenia dokumentu XML. Pozostało nam jedynie przejrzenie dokumentów w kolejnych pętlach foreach i przesłanie odpowiednich właściwości obiektów metodom klasy XML_Util. Jeśli chcielibyśmy przesłać dokument do przeglądarki internetowej, należy użyć instrukcji echo. Oto kompletny skrypt tworzący dokument XML z drzewa obiektów: require_once 'XML/Util.php'; echo XML_Util::getXMLDeclaration('1.0', 'ISO-8859-1'); echo XML_Util::createStartElement('labels') . "\n"; foreach ($labels as $label) { echo XML_Util::createStartElement('label', array('name' => $label->name)) . "\n"; echo XML_Util::createStartElement('artists') . "\n"; foreach ($label->artists as $artist) { echo XML_Util::createStartElement('artist', array('id' => $artist->id)) . "\n"; echo XML_Util::createTag('name', array(), $artist->name) . "\n"; echo XML_Util::createStartElement('records') . "\n"; foreach ($artist->records as $record) {

108

Rozdział 3. • Praca z formatem XML

echo XML_Util::createStartElement('record', array( 'id' => $record->id, 'released' => $record->released ) ) . "\n"; echo XML_Util::createTag('name', array(), $record->name) . "\n"; echo XML_Util::createEndElement('record') . "\n"; } echo XML_Util::createEndElement('records') . "\n"; echo XML_Util::createEndElement('artist') . "\n"; } echo XML_Util::createEndElement('artists') . "\n"; echo XML_Util::createEndElement('label') . "\n"; } echo XML_Util::createEndElement('labels') . "\n";

Po dołączeniu pliku, zawierającego klasę XML_UTIL, tworzymy deklarację XML, która poprzedzać będzie dokument i określać stosowane przez nas kodowanie. Następnie za pomocą metody createStartElement() tworzony jest znacznik otwierający głównego elementu. Później przeglądamy w pętli wszystkie obiekty Label przechowywane w tablicy $labels. Prawdę powiedziawszy, jest tam tylko jeden element, etykieta Sun Records, niemniej po dodaniu dodatkowych obiektów nie będzie potrzeby modyfikowania kodu. Dla każdej etykiety studia utworzymy element etykiety i dodamy właściwość $name obiektu Label do listy atrybutów: echo XML_Util::createStartElement('label', array('name' => $label->name)) . "\n";

Wewnątrz tej pętli będziemy przeglądać wszystkie obiekty artystów Artist, które zostały zapisane we właściwości $artists obiektu Label po tym, jak utworzony został otwierający znacznik . Dla każdego z obiektów Artist tworzymy odpowiadający mu znacznik , a następnie dodajemy do atrybutów właściwość $id. Na koniec, wewnątrz drugiej pętli będziemy musieli tylko przejrzeć wszystkie obiekty Record, które zostały dodane do właściwości $records obiektu Artist, i utworzyć zamykający znacznik . Oczywiście znaczniki te otoczone są przez znacznik . Na końcu każdej pętli tworzone są znaczniki zamykające odpowiadające znacznikom otwierającym, które utworzone zostały przed rozpoczęciem pętli, dzięki czemu mamy gwarancję, że dokument jest prawidłowo sformułowany. Jeśli uruchomimy ten skrypt, zwróci on dokładnie taki sam dokument XML, jaki zaprezentowaliśmy na początku tego rozdziału, z tą tylko różnicą, że znaczniki nie będą justowane za pomocą wcięć. Klasa XML_Util oferuje metody umożliwiające tworzenie pojedynczych znaczników lub dowolnych elementów XML, niemniej nie utworzy za nas kompletnego dokumentu. W dalszej części rozdziału dowiemy się o innych pakietach PEAR, które to potrafią. Zapoznamy się z pakietami, którymi można przesłać praktycznie dowolną strukturę danych, zamiast po prostu łańcuchów lub tablic asocjacyjnych, i które przekształcą otrzymane dane w dokument XML.

109

PEAR. Programowanie w PHP

Dodatkowe funkcje klasy XML_Util Klasa XML_Util oferuje również kilka dodatkowych metod, które okazują się użyteczne podczas pracy z formatem XML. Jeśli generujemy kod XML dynamicznie i nie wiemy, w jaki sposób znaczniki te będą nazywane, klasa XML_Util umożliwia sprawdzenie, czy danego łańcucha można użyć w charakterze nazwy znacznika. $result = XML_Util::isValidName('My tag name'); if (PEAR::isError($result)) { echo 'Niepoprawna nazwa znacznika: ' . $result->getMessage(); } else { echo 'Poprawna nazwa znacznika'; }

Jeśli łańcuch, który przesłaliśmy metodzie, może zostać wykorzystany jako nazwa znacznika w dokumencie XML, to metoda zwróci wartość true (prawda). Jeśli natomiast łańcucha nie można użyć w charakterze nazwy znacznika, ponieważ narusza reguły formatu XML, metoda isValidName() zwróci obiekt błędu PEAR_Error, który zawierać będzie informacje, jaka dokładnie reguła została naruszona. Dlatego jeśli uruchomimy ten skrypt, zwróci on następujący komunikat: No valid tag name: XML names may only contain alphanumeric chars, period, hypen, colon and underscores.

informujący, że łańcuch nie jest prawidłowym znacznikiem XML, bowiem nazwy XML mogą zawierać tylko znaki alfanumeryczne, kropki, myślniki, dwukropki i znaki podkreślenia. Kolejna użyteczna metoda, replaceEntities() umożliwia zastępowanie niedozwolonych znaków pojawiających się w przesłanym jej łańcuchu odpowiadającymi im jednostkami (kodami): echo XML_Util::replaceEntities('Ten tekst zawiera " & \'.');

Po zastosowaniu tej metody na łańcuchu będziemy mogli bez obaw użyć go w dowolnym dokumencie XML. Aby odwrócić działanie tej metody, należy skorzystać z kolejnej metody klasy XML_Util, reverseEntities(). Więcej na temat klasy XML_Util lub zasad działania jej interfejsu można dowiedzieć się z dokumentacji użytkownika, dostępnej w witrynie repozytorium PEAR pod adresem: http://pear.php. ¦net/manual/en/package.xml.xml-util.php.

Tworzenie dokumentów XML za pomocą pakietu XML_FastCreate Pakiet XML_FastCreate (jak zresztą łatwo domyślić się po jego nazwie) służy do bardzo szybkiego i efektywnego tworzenia dokumentów XML. Stosuje jednak zupełnie inne podejście niż pakiet XML_Util. Pakiet XML_FastCreate nie tworzy osobnych fragmentów dokumentu 110

Rozdział 3. • Praca z formatem XML

XML, ale zawsze kompletny, prawidłowo sformułowany dokument. Pakiet XML_FastCreate zawsze upewnia się, czy otrzymamy poprawny dokument XML, podczas gdy pakiet XML_Util sprawdza tylko poszczególne znaczniki, w związku z czym cały czas możemy pominąć domykające znaczniki lub popełnić jakiś błąd tam, gdzie znaczniki zostają zagnieżdżone jeden w drugim. Pakiet XML_FastCreate można wykorzystać do: „ Utworzenia łańcucha, który zawierać będzie dokument XML. „ Utworzenia w pamięci struktury drzewa również zawierającej dokument XML. W obu przypadkach możemy używać do utworzenia dokumentu XML tego samego interfejsu API, bowiem pakiet XML_FastCreate dostarcza dla tych dwóch zadań odrębnych sterowników. Dlatego zamiast tworzyć nową instancję klasy XML_FastCreate za pomocą operatora new, trzeba za każdym razem używać do tego metody fabryki (factory) klasy XML_FastCreate. require_once 'XML/FastCreate.php'; $xml = XML_FastCreate::factory('Text');

W tym przypadku metoda fabryki zwróci sterownik, który bezpośrednio utworzy łańcuch znaków zawierający dokument XML. W przeważającej części dalszych przykładów będziemy korzystać właśnie z tego sterownika, ponieważ jest łatwiejszy w użyciu i bardziej stabilny niż alternatywny sterownik oparty na pakiecie XML_Tree. Czytelnicy, którzy będą mimo to chcieli korzystać ze sterownika bazującego na pakiecie XML_Tree, muszą się liczyć z tym, że kolejne przykłady mogą nie działać tak, jak powinny, ponieważ sterownik ten nie obsługuje niektórych funkcjonalności opisywanych w przykładach. Co więcej, trzeba będzie korzystać z wersji 2.0.0 pakietu XML_Tree, której kod jest nadal w fazie beta (może być poprawiany). Różnica pomiędzy sterownikiem tekstowym a sterownikiem opartym na pakiecie XML_Tree polega na tym, że ten drugi pozwala na modyfikowanie dokumentu tak, jakby był on obiektem, zanim zapiszemy go w postaci łańcucha znaków. Sterownik tekstowy z kolei od razu wygeneruje łańcuch zawierający dokument XML, którego to łańcucha już nie będzie można tak łatwo modyfikować (przynajmniej bez sięgania do wyrażeń regularnych). Teraz gdy mamy już nową instancję klasy XML_FastCreate, kolejnym krokiem jest utworzenie znaczników dokumentu. Okazuje się, że jest to niezmiernie łatwe! Wszystko, co trzeba zrobić, to przywołać metodę o nazwie odpowiadającej nazwie znacznika, który chcemy utworzyć, i przesłać jej tekst, który powinien znaleźć się między znacznikiem otwierającym a znacznikiem zamykającym: $xml->artist('Elvis Presley');

W ten sposób dodaliśmy do dokumentu XML znacznik . Otrzymany dokument można przesłać do standardowego strumienia wyjścia STDOUT używając metody toXML(): $xml->toXML();

Po uruchomieniu tego kodu wyświetli on następujące informacje:

Elvis Presley

111

PEAR. Programowanie w PHP

Czytelnicy zastanawiają się pewnie, skąd pakiet XML_FastCreate wiedział, że musi utworzyć znacznik i użyć metody artist(). Ponieważ dokument XML może zawierać praktycznie dowolny znacznik, pakiet XML_FastCreate musiałby oferować nieskończoną liczbę różnych metod, by móc tworzyć wszystkie możliwe znaczniki. Jak pewnie czytelnicy już zgadli, pakiet XML_FastCreate nie implementuje oczywiście tych wszystkich metod. Zamiast tego korzysta z techniki zwanej przeciążaniem (ang. overloading). Przeciążanie pojawia się dopiero w PHP5, niemniej pakiet XML_FastCreate obsługuje je również dla języka PHP4, jeśli włączymy rozszerzenie umożliwiające przeciążanie (które jest włączane domyślnie we wszystkich wersjach, począwszy od wersji PHP4.3.x). Programiści, którzy chcieliby używać pakietu XML_FastCreate z językiem PHP4, mogą dowiedzieć się więcej na temat rozszerzenia udostępniającego możliwość przeciążania czytając dokumentację języka PHP na ten temat, dostępną pod adresem: http://www.php.net/overload. W kolejnych przykładach skoncentrujemy się na obsłudze przeciążania oferowanej przez PHP5.

Interludium: przeciążanie w PHP5 Aby zrozumieć, jak działa pakiet XML_FastCreate, trzeba pojąć podstawowe zasady działania mechanizmu przeciążania obiektów. Przeciążanie umożliwia przechwytywanie odwołań do niezdefiniowanych metod obiektu. Rozważmy następujący fragment kodu: class Bird { public function fly() { print "Ja latam.\n"; } } $bird = new Bird(); $bird->fly(); $bird->swim();

Jeśli uruchomimy ten skrypt, wyświetli ona następujące informacje: Ja latam. Fatal error: Call to undefined method Bird::swim() in c:\wamp\www\books\ packt\pear\xml\overloading.php on line 10

Jak widać, skrypt ten zakończył pracę sygnalizując fatalny błąd w momencie, gdy próbowaliśmy przywołać metodę swim() (pływać) na obiekcie Bird (Ptak), a metoda ta nie została zaimplementowana. W tym miejscu właśnie z pomocą przychodzi mechanizm przeciążania obiektów: przeciążanie umożliwia nam przechwytywanie odwołań do metod (i sięgania do właściwości w przypadku gdy mamy do czynienia z niezdefiniowaną metodą lub właściwością). Aby przechwycić odwołanie do niezdefiniowanej metody swim(), konieczne będzie zaimplementowanie tzw. magicznej (ang. magic) metody _call(), której trzeba przesłać dwa argumenty: 1. Nazwę oryginalnej przywoływanej metody. 2. Tablicę zawierającą wszystkie argumenty, które pierwotnie przesyłane były wywoływanej metodzie.

112

Rozdział 3. • Praca z formatem XML

Po dodaniu tej metody do klasy Bird klasa będzie wyglądać mniej więcej tak: class Bird { public function fly() { print "Ja latam.\n"; } public function __call($method, $args) { print "Ja nie umiem $method.\n"; } }

Jeśli teraz uruchomimy skrypt ponownie, to zwróci on następujące informacje: Ja latam. Ja nie umiem swim.

Jak widać, zawsze kiedy przywoływać będziemy metodę, która nie została zaimplementowana w klasie — swim (pływać), zamiast niej przywoływana będzie metoda _call(): $bird->playPoker(); $bird->raiseTaxes();

Wynik działania tego kodu będzie taki, jak być powinien — skrypt wyświetli informacje, że nie może użyć metod playPoker (grać w pokera) i raiseTaxes (zbierać podatki): Ja nie umiem playPoker. Ja nie umiem raiseTaxes.

Wracając do XML Tak właśnie działa pakiet XML_FastCreate. Zawsze kiedy przywoływać będziemy jakąś metodę, której nazwa odpowiada nazwie tworzonego przez nas znacznika, język PHP będzie zamiast niej przywoływał metodę _call() i przesyłał jej jako zawartość znacznika nazwę znacznika, który chcemy utworzyć. Pakiet XML_FastCreate umożliwia również zagnieżdżanie znaczników poprzez zagnieżdżanie odwołań do metod: require_once 'XML/FastCreate.php'; $xml = XML_FastCreate::factory('Text'); $xml->artist( $xml->name('Elvis Presley'), $xml->hometown('Memphis') ); $xml->toXML();

Kod ten zwróci dokument XML o następującej strukturze (dodaliśmy wcięcia dla poprawienia czytelności):

113

PEAR. Programowanie w PHP

Elvis Presley Memphis

Dotychczas wszystkie tworzone przez nas znaczniki zawierały tylko tekst, natomiast nie posiadały żadnych atrybutów. Niemniej dodawanie atrybutów do znaczników XML jest niezmiernie proste. Podobnie jak to było w przypadku klasy XML_Util, listę atrybutów znacznika XML należy przesłać w formie tablicy asocjacyjnej. Tablica ta powinna być przesyłana jako pierwszy parametr każdej metodzie, która tworzy znacznik XML. Aby dodać dwa atrybuty do głównego, nadrzędnego znacznika utworzonego przez nas dokumentu XML, należy w naszym kodzie wprowadzić niewielkie zmiany: require_once 'XML/FastCreate.php'; $xml = XML_FastCreate::factory('Text'); $xml->artist( array( 'id' => 56, 'label' => 'Sun Records' ), $xml->name('Elvis Presley'), $xml->hometown('Memphis') ); $xml->toXML();

Utworzony dokument będzie miał teraz w swoim głównym znaczniku dwa atrybuty: id i label.

Elvis Presley Memphis

Kolejną sprawą, która może uderzyć czytelnika, jest fakt, że pakiet XML_FastCreate automatycznie tworzy deklarację dla dokumentu, używając kodowania UTF-8, podczas gdy w poprzednich przykładach wykorzystywaliśmy kodowanie ISO-8859-1. Bez obaw! Pakiet XML_ FastCreate umożliwia również definiowanie innych kodowań. Podczas tworzenia instancji sterownika XML_FastCreate za pomocą metody fabryki factory() możemy przesłać jej w drugim argumencie listę opcji. Jedna z tych opcji pozwala określić kodowanie dokumentu: $options = array( 'encoding' => 'ISO-8859-1' ); $xml = XML_FastCreate::factory('Text', $options);

Określanie kodowania to tylko jedna z możliwych opcji, które można definiować za pomocą metody fabryki. W kolejnej tabeli prezentujemy listę najważniejszych opcji. Opcje te są obsługiwane przez oba sterowniki oferowane przez bieżącą wersję pakietu XML_FastCreate.

114

Rozdział 3. • Praca z formatem XML

Więcej informacji na temat innych opcji, które nie są obsługiwane przez oba sterowniki, można znaleźć w kodzie źródłowym i dołączonej do niego dokumentacji sterowników oraz bazowej klasy. Nazwa opcji

Opis

Wartość domyślna

version

Numer używanej wersji XML.

1.0

encoding

Używane kodowanie XML.

UTF-8

standalone

Czy dokument jest samodzielnym dokumentem, czy nie.

No (nie)

indent

Czy w dokumencie XML stosować wcięcia (wymaga pakietu False (fałsz) XML_Beautifier).

quote

Czy automatycznie zastępować znaki specjalne odpowiadającymi im jednostkami (kodami).

True (prawda)

doctype

Którą deklarację typu dokumentu należy dodać.

Brak wartości domyślnej

exec

Zewnętrzny program, którego należy użyć, by porównać zgodność dokumentu XML z definicją DTD.

Brak wartości domyślnej

file

Plik, w którym należy zapisać wynik sprawdzenia dokumentu XML. Jeśli nie zostanie określony żaden plik, wynik zostanie wyświetlony na ekranie.

Brak wartości domyślnej

Pakiet XML_Beautifier Pakiet XML_Beautifier pomaga formatować kod XML, tak aby był bardziej czytelny dla ludzi. Dokumenty XML nie wymagają estetycznego dzielenia wierszy czy stosowania wcięć. Aplikacja przetwarzająca dokument XML wszystkie potrzebne informacje pobiera ze znaczników otwierających i zamykających. Niemniej właściwe dzielenie wierszy i odpowiednie wcięcia ułatwiają ludziom przeglądanie dokumentów XML. Dlatego tam, gdzie zachodzi potrzeba zaprezentowania dokumentu XML w formie odpowiednio uporządkowanej, z dodaniem formatujących spacji dla komfortu użytkownika, z pomocą przychodzi pakiet XML_Beautifier. Potrafi on odczytać dowolny dokument XML i zastosować na nim odpowiednie reguły formatowania (na podobnej zasadzie jak nasz edytor potrafi estetycznie sformatować kod PHP). Doda w odpowiednich miejscach podział wierszy, wcięcia, automatycznie zawinie zbyt długie wiersze itp. Po wprowadzeniu w PHP5 nowego rozszerzenia DOM, pakiet XML_Beautifier traci trochę na znaczeniu, rozszerzenie to jest bowiem w stanie do pewnego stopnia sformatować dokument XML (choć nie ma możliwości naśladowania wszystkich funkcji pakietu XML_Beautifier).

Teraz wiemy już wszystko, co trzeba, aby utworzyć z przygotowanych wcześniej obiektów dokument XML zawierający etykiety studia. Został nam jeszcze do omówienia tylko jeden element. Kiedy tworzyliśmy znacznik XML przywołując odpowiednią metodę, zawsze ignorowaliśmy wartość zwracaną przez tę metodę. Niemniej warto wiedzieć, że metody tworzące znaczniki zwracają odpowiedni fragment kodu w zależności od sterownika, którego używamy. Kiedy korzystamy ze sterownika Text, metoda zwraca łańcuch, natomiast w przypadku sterownika opartego na XML_Tree metoda zwraca instancję klasy XML_Tree_Node.

115

PEAR. Programowanie w PHP

Tworzenie dokumentu XML Jeśli korzystamy ze sterownika Text, różne znaczniki utworzone przez pakiet XML_FastCreate można łączyć w jeden dokument XML używając standardowych funkcji służących do pracy z łańcuchami, które oferuje język PHP. Programista musi jedynie przejrzeć strukturę obiektów w trzech zagnieżdżonych pętlach: jednej dla etykiet, jednej dla artystów przypisanych do każdej etykiety i jednej dla albumów każdego artysty. Rozwiązanie to jest podobne do techniki, którą stosowaliśmy korzystając z klasy XML_Util, niemniej jest tu jedna istotna różnica: pakiet XML_FastCreate zawsze tworzy kompletny element XML (czyli znacznik otwierający i znacznik zamykający). Dlatego też zawartość znacznika trzeba kompilować (składać) jeszcze przed utworzeniem samego znacznika. Znacznik jest niejako tworzony „od środka”. Pierwszymi utworzonymi znacznikami będą znaczniki albumów , potem znaczniki artysty , a następnie znaczniki etykiety . Na koniec wreszcie, w ostatnich wierszach skryptu utworzymy znacznik etykiet , który otacza wszystkie wcześniej utworzone znaczniki. Natomiast gdy korzystaliśmy z klasy XML_Util, tworzyliśmy znaczniki w takiej samej kolejności, w jakiej miały pojawiać się w dokumencie. Jak widać, kiedy pracujemy z pakietem XML_FastCreate, trzeba najpierw zastanowić się nad odpowiednią kolejnością zagnieżdżania znaczników, co sprawia, że tworzenie dokumentów XML za jego pomocą jest odrobinę trudniejsze, szczególnie jeśli nie jesteśmy przyzwyczajeni do takiego odwróconego porządku. Kompletny skrypt, który tworzy odpowiedni dokument AML z przygotowanego drzewa obiektów za pomocą pakietu XML_FastCreate, wygląda tak: require_once 'XML/FastCreate.php'; // określamy podstawowe opcje dokumentu XML

$options = array( 'encoding' => 'ISO-8859-1', 'standalone' => 'yes' ); // Pobieramy nową instancję sterownika 'Text'

$xml = XML_FastCreate::factory('Text', $options); // Ta zmienna przechowuje wszystkie etykiety w postaci kodu XML

$labelsXML = ''; // Przeglądamy etykiety albumów w tablicy

foreach ($labels as $label) { // Ta zmienna przechowuje wszystkich artystów danego studia (etykiety) jako XML

$artistsXML = ''; // Przeglądamy wszystkich artystów

foreach ($label->artists as $artist) { // Ta zmienna przechowuje wszystkie albumy danego artysty jako kod XML

$records = '';

116

Rozdział 3. • Praca z formatem XML

// Przeglądamy wszystkie albumy (records)

foreach ($artist->records as $record) { $recordAtts = array( 'id' => $record->id, 'released' => $record->released ); // tworzymy i dodajemy jeden znacznik

$records .= $xml->record($recordAtts, $xml->name( $record->name)); } $artistAtts = array('id' => $artist->id); // tworzymy i dodajemy jeden znacznik

$artistsXML .= $xml->artist($artistAtts, $xml->records($records)); } $labelAtts = array('name' => $label->name); // tworzymy i dodajemy jeden znacznik

$labelsXML .= $xml->label($labelAtts, $xml->artists($artistsXML)); } $xml->labels($labelsXML); // Wysyłamy gotowy kod XML do standardowego strumienia wyjścia STDOUT

$xml->toXML();

Dla każdej pętli tworzymy nową zmienną i inicjujemy ją, przesyłając jej pusty łańcuch ($labelsXML, $artistsXML i $recordsXML). Wewnętrzne pętle będą następnie zachowywać wyniki swojego działania w tych zmiennych, a po zakończeniu wewnętrznych pętli wartości zapisane w zmiennych będą wykorzystywane jako zawartość odpowiednich, bardziej zewnętrznych znaczników , lub . Jeśli uruchomimy ten skrypt, otrzymamy taki sam dokument XML, jak w przykładzie, w którym korzystaliśmy z klasy XML_Util. Jeśli mamy w komputerze zainstalowany pakiet XML_Beatifier, to możemy również skorzystać z opcji indent pakietu XML_FastCreate, za której pomocą otrzymamy ładnie sformatowany dokument XML.

Problemy z pakietem XML_FastCreate Mimo iż pakiet XML_FastCreate wydaje się bardziej wszechstronny niż pakiet XML_Util (i tak jest w istocie), należy jednak pamiętać o kilku istotnych problemach pojawiających się podczas korzystania z niego: „ Ponieważ przeciążanie przechwytuje tylko odwołania do nieistniejących metod, istnieje w nim pewna grupa zarezerwowanych słów (nazw metod dostarczanych przez pakiet XML_FastCreate), których nie można używać w charakterze nazw znaczników. Jedną z nich jest metoda xml() wykorzystywana przez przechwytującą metodę _call() do tworzenia znaczników. Dlatego też, jeśli użyjemy w kodzie wywołania w rodzaju na przykład $fastcreate->xml('foo'), to podana metoda

117

PEAR. Programowanie w PHP

nie zostanie przechwycona, bowiem metoda o tej nazwie już istnieje w pakiecie. Aby zatem otrzymać satysfakcjonujący nas efekt, należy zastosować wywołanie $fastcrete->('xml', 'foo');, ponieważ trzeba użyć odpowiedniej sygnatury metody dla metody xml(). „ Pakiet XML_FastCreate zależy w znacznym stopniu od pakietów będących jeszcze w fazie alfa lub beta, czyli w przygotowaniu (takich jak XML_DTD czy XMLTree), co może spowodować problemy z wsteczną kompatybilnością kodu, jeśli któryś z tych pakietów zostanie unowocześniony. W skrajnym przypadku może się nawet okazać, że w pewnym momencie pakiet XML_FastCreate nagle przestanie działać. „ Tworzenie za pomocą sterownika XML_Tree pakietu XML_FastCreate dokumentów XML o dynamicznej strukturze jest bardzo skomplikowane. Sterownik ten zwraca podczas tworzenia znaczników obiekty zamiast łańcuchów, nie możemy więc korzystać z wbudowanych w język PHP funkcji przeznaczonych do pracy z łańcuchami, by za ich pomocą tworzyć większy dokument, który korzystać będzie z dynamicznych nazw znaczników i dynamicznych danych. Sterownik ten nie powinien być używany w dokumentach, które zawierają złożone struktury przygotowywane w trakcie działania programu.

Tworzenie dokumentów XML za pomocą pakietu XML_Serializer Wprawdzie pakiet XML_Serializer służy również do tworzenia dokumentów XML, robi to jednak w zupełnie inny sposób niż omówione wyżej pakiety XML_Util i XML_FastCreate. W przypadku tamtych pakietów musieliśmy tworzyć dokument znacznik po znaczniku, dla każdego przywołując osobną metodę. Natomiast kiedy korzystamy z pakietu XML_Serializer, przywołujemy tylko jedną metodę, która tworzy od razu cały dokument. Pobierze ona odpowiednie informacje z tablicy lub obiektu i przekonwertuje je na dokument XML. Rozwiązanie to nie daje aż tyle swobody, co poprzednie dwa podejścia, niemniej pakiet XML_Serializer ciągle jest jednym z najlepszych pakietów służących do tworzenia dokumentów XML. Potrafi serializować dowolne dane, które prześlemy mu jako dokument XML. Dzięki temu jest w stanie przygotować łańcuch reprezentujący praktycznie dowolne dane w formacie XML. Można go traktować jako ekwiwalent XML wbudowanej w język PHP funkcji serialize(), która pozwala na zapisywanie w postaci łańcucha dowolnych danych, niezależnie od tego, czy są to wielokrotnie zagnieżdżone tablice, czy złożone obiekty drzew. Tak przygotowany łańcuch można zapisać w pliku, w sesji użytkownika lub nawet w bazie danych. Język PHP dostarcza również funkcji unserialize(), mogącej odtworzyć oryginalną strukturę danych z łańcucha, w którym zostały serializowane. W drugiej części tego rozdziału poznamy również klasę XML_Unserializer, która robi dokładnie to samo z dokumentami XML przygotowanymi przez klasę XML_Serializer.

118

Rozdział 3. • Praca z formatem XML

Oto typowy przebieg pracy z pakietem XML_Serializer: „ Załączamy do kodu pakiet XML_Serializer i tworzymy nową instancję klasy. „ Konfigurujemy instancję używając odpowiednich opcji. „ Tworzymy dokument XML. „ Pobieramy dokument i robimy z nim to, do czego był nam potrzebny. Korzystanie z pakietu XML_Serializer w prawdziwych aplikacjach nie będzie ani odrobinę trudniejsze. Ponieważ tworząc dokument XML przywołujemy tylko jedną metodę, będziemy musieli przesłać jej wszystkie informacje, które powinny znaleźć się w dokumencie XML. Aby maksymalnie ułatwić programiście zadanie, pakiet XML_Serializer pozwala na przesyłanie tej metodzie praktycznie dowolnych danych przeznaczonych dla generowanego dokumentu XML. Starczy jednak tej teorii; najlepszym sposobem pokazania, jak działa pakiet XML_Serializer, będzie zaprezentowanie odpowiedniego przykładu: // załączamy klasę

require_once('XML/Serializer.php'); // tworzymy nowy obiekt

$serializer = new XML_Serializer(); // tworzymy dokument XML

$serializer->serialize('To jest łańcuch'); // pobieramy dokument

echo $serializer->getSerializedData();

W tym przykładzie wykonaliśmy po kolei kroki opisane wyżej. Po uruchomieniu kodu otrzymamy następujący wynik: To jest łańcuch

Nie jest to tak złożony dokument, jak wcześniejsze, i prawdopodobnie łatwiej byłoby go utworzyć używając pakietu XML_Util, XML_FastCreate lub nawet narzędzi do sklejania łańcuchów oferowanych przez język PHP. Niemniej kolejny przykład powinien zdecydowanie zmienić nastawienie tych czytelników, którzy nie są do niego przekonani: $data = array( 'artist' => 'Elvis Presley', 'label' => 'Sun Records', 'record' => 'Viva Las Vegas' ); // załączamy klasę

require_once('XML/Serializer.php'); // tworzymy nowy obiekt

$serializer = new XML_Serializer(); // tworzymy dokument XML

$serializer->serialize($data);

119

PEAR. Programowanie w PHP

// pobieramy dokument

echo $serializer->getSerializedData();

W tym przykładzie zmieniły się tylko dwie rzeczy: „ Została utworzona zmienna $data zawierająca tablicę z danymi. „ Zamiast łańcucha z danymi metodzie serialize() przesyłana jest zmienna $data. Reszta skryptu pozostała niezmieniona i nadal działa w taki sam sposób jak poprzedni. Przyjrzyjmy się informacją zwróconym przez ten skrypt:

Elvis Presley Sun Records Viva Las Vegas

Tworzenie tego dokumentu XML byłoby znacznie trudniejsze, gdybyśmy próbowali zrobić to innym sposobem. Gdybyśmy chcieli dodać więcej danych i kolejne, zagnieżdżone znaczniki XML, utworzenie takiego dokumentu za pomocą pakietów XML_Util lub XML_FastCreate byłoby znacznie trudniejsze. W przypadku korzystania z pakietu XML_Serialize zagnieżdżony kod będzie zawsze taki sam, a ponadto będziemy mogli przesłać funkcji serialize() następujące dane, nie zmieniając w kodzie niczego więcej: $data = array( 'artist' => array( 'name' => 'Elvis Presley', 'email' => '[email protected]' ), 'label' => 'Sun Records', 'record' => 'Viva Las Vegas' );

Jak można się było spodziewać, skrypt ten wygeneruje następujący dokument XML:

Elvis Presley [email protected]

Sun Records Viva Las Vegas

Teraz wiemy już, jak działa pakiet XML_Serializer: programista przesyła metodzie serialize() dowolną strukturę danych PHP, a metoda tworzy na podstawie przesłanych jej danych odpowiedni dokument XML. Pakiet XML_Serializer stara się zgadnąć, w jaki sposób dokument powinien zostać utworzony, tj. jako nazwę głównego znacznika wykorzystuje typ danych, klucze tablicy, jako nazwy wewnętrznych znaczników, i zagnieżdża znaczniki w taki sam sposób,

120

Rozdział 3. • Praca z formatem XML

w jaki zagnieżdżone są tablice. Wcześniej wspomniane umożliwiają programiście wpływanie na to, w jaki sposób zgadywanie to będzie działać. Dlatego teraz omówimy najważniejsze opcje pakietu XML_Serializer.

Opcje pakietu XML_Serializer Począwszy od wersji 0.17.0, pakiet XML_Serializer oferuje aż 27 różnych opcji. Dla każdej z tych opcji pakiet XML_Serializer dostarcza stałej zaczynającej się od przedrostka XML_SERIALIZER_ OPTION, po którym następuje nazwa opcji. Wartości opcji można określać na jeden z trzech sposobów: „ Przesłać konstruktorowi klasy XML_Serializer tablicę asocjacyjną zawierającą wybrane opcje i ich wartości. „ Skorzystać z metod setOption() i setOptions() klasy XML_Serializer. „ Przesłać metodzie serialize() jako drugi argument tablicę asocjacyjną zawierającą wybrane opcje i przypisane im wartości. Podczas gdy dwa pierwsze sposoby są w zasadzie równoważne i można ich używać do określania opcji dla wszystkich kolejnych dokumentów XML, ostatni sposób pokryje tylko opcje dokumentu, który będzie tworzony w bieżącym odwołaniu do metody serialize(). W większości przypadków zaleca się definiować opcje za pomocą wielokrotnego przywoływania metody setOption(), ponieważ gwarantuje ona większą czytelność skryptów. Teraz, gdy wiemy już, jak ustawiać opcje pakietu XML_Serializer, pora wrócić do utworzonego przez nas dokumentu i spróbować za pomocą odpowiednich opcji zmodyfikować ostateczny wygląd dokumentu XML. Pierwsza rzecz, która rzuca się w oczy, to brak w utworzonym dokumencie XML deklaracji dokumentu XML. Oczywiście można by ją było dodać ręcznie już po utworzeniu dokumentu przez pakiet XML_Serializer, ale łatwiej jest zlecić to zadanie pakietowi XML_Serializer. Trzeba w tym celu dodać jedynie dwa wiersze kodu: // załączamy klasę

require_once('XML/Serializer.php'); // tworzymy nowy obiekt

$serializer = new XML_Serializer(); // definiujemy opcje

$serializer->setOption(XML_SERIALIZER_OPTION_XML_DECL_ENABLED, true); $serializer->setOption(XML_SERIALIZER_OPTION_XML_ENCODING, 'ISO-8859-1'); // tworzymy dokument XML

$serializer->serialize($data); // pobieramy dokument

echo $serializer->getSerializedData();

121

PEAR. Programowanie w PHP

Teraz nasz dokument będzie posiadał odpowiednią deklarację XML definiującą sposób kodowania wykorzystywany w dokumencie. Następnym naszym krokiem będzie upiększenie odrobinę dokumentu poprzez dodanie odpowiedniego wcięcia justującego znaczniki i zmieniającego nazwę głównego znacznika, ponieważ nazwa array (tablica) niewiele nam mówi. Ponownie wystarczy dodać jedynie dwa wiersze: $serializer->setOption(XML_SERIALIZER_OPTION_INDENT, ' '); $serializer->setOption(XML_SERIALIZER_OPTION_ROOT_NAME, 'artist-info');

Jeśli teraz przyjrzymy się wynikowi działania skryptu, przekonamy się, że nasz dokument XML wygląda obecnie o wiele lepiej:

Elvis Presley [email protected]

Sun Records Viva Las Vegas

Dodawanie atrybutów Dokumenty XML bardzo rzadko zawierają same tylko znaczniki bez atrybutów. Dlatego też warto dowiedzieć się, w jaki sposób za pomocą pakietu XML_Serializer tworzyć znaczniki zawierające atrybuty oraz znaczniki zagnieżdżone i dane znakowe. Jak się przekonamy, jest to równie proste, jak wszystko co do tej pory robiliśmy używając pakietu XML_Serializer. Pakiet XML_Serializer potrafi automatycznie konwertować zmienne skalarne (łańcuchy, wartości logiczne, liczby całkowite itp.) na atrybuty odpowiedniego nadrzędnego znacznika. Wszystko, co trzeba w tym celu zrobić, to włączyć jedną opcję: $serializer->setOption(XML_SERIALIZER_OPTION_SCALAR_AS_ATTRIBUTES, true);

Jeśli dodamy ten kod do naszego skryptu, po czym uruchomimy go ponownie, to otrzymany dokument XML będzie wyglądał zupełnie inaczej:



Jeśli natomiast chcemy tylko przekonwertować wartości łańcuchowe zapisane w tablicy artist znacznika i jednocześnie zachować nienaruszone znaczniki i , to da się to również bez trudu zrobić:

122

Rozdział 3. • Praca z formatem XML

$serializer->setOption(XML_SERIALIZER_OPTION_SCALAR_AS_ATTRIBUTES, array( 'artist' => true ) );

Możemy nawet indywidualnie dla każdego znacznika decydować, którą z wartości chcemy dodać jako atrybut. Jeśli potrzebujemy adresu e-mail przechowywanego w atrybucie, ale jednocześnie chcemy dodać zagnieżdżony znacznik przeznaczony dla nazwiska artysty, to wystarczy zmienić tylko jeden wiersz w naszym skrypcie: $serializer->setOption(XML_SERIALIZER_OPTION_SCALAR_AS_ATTRIBUTES, array( 'artist' => array('email') ) );

Jeśli teraz wykonamy skrypt, zwróci on następujący dokument XML:

Elvis Presley

Sun Records Viva Las Vegas

Kolejna opcja umożliwiająca dodawanie atrybutów do dokumentu XML to ROOT_ATTRIBS. Wraz z opcją tą można przesłać tablicę asocjacyjną określającą atrybuty głównego elementu (znacznika) XML.

Wykorzystywanie tablic indeksowanych Większość artystów w trakcie swojej kariery wydaje więcej niż jedną płytę oraz podpisuje kontrakty z więcej niż jednym studiem nagrań (określonym przez etykietę). Jeśli spróbujemy uwzględnić ten fakt w naszym przykładzie, otrzymamy prawdopodobnie strukturę danych zbliżoną do takiej oto tablicy: $data = array( 'artist' => array( 'name' => 'Elvis Presley', 'email' => '[email protected]' ), 'labels' => array( 'Sun Records', 'Sony Music' ), 'records' => array(

123

PEAR. Programowanie w PHP

'Viva Las Vegas', 'Hound Dog', 'In the Ghetto' ) );

Ponieważ pakiet XML_Serializer potrafi przekształcić na dokument XML praktycznie dowolne dane, możemy przesłać mu również i tę tablicę, mając nadzieję, że przygotuje na jej podstawie jakiś użyteczny kod XML. Jeśli spróbujemy uruchomić dla tej tablicy nasz skrypt, to zwróci on następujący dokument XML:

Elvis Presley

Sun Records Sony Music

Viva Las Vegas Hound Dog In the Ghetto

To, co prawdopodobnie rzuci się w oczy czytelnikowi zaraz po wyświetleniu tego dokumentu na ekranie, jest często pojawiający się znacznik . Osoby znające się trochę na XML-u bez trudu domyślą się, skąd on się wziął. Podczas serializowania tablicy pakiet XML_Serializer używa jako nazw znaczników kluczy tablicy, a jako ich zawartości — przypisanych kluczowi wartości. W tym przykładzie dane zawierają dwie indeksowane tablice, których klucze są kolejnymi liczbami porządkowymi: „0”, „1” i „2”. Jednakże znaczniki , i nie byłyby prawidłowymi znacznikami XML. Ponieważ pakiet XML_Serializer mimo takich niedogodności nadal potrafi przygotować poprawnie sformułowany dokument XML, zamiast używać nieprawidłowych nazw znaczników, skorzysta z domyślnej nazwy znacznika. Oczywiście tę domyślną nazwę znacznika można zmieniać: $serializer->setOption(XML_SERIALIZER_OPTION_DEFAULT_TAG, 'item');

Gdy dodamy ten wiersz do skryptu, to po jego uruchomieniu otrzymamy odrobinę inny dokument XML niż poprzednio, gdyż znaczniki zostaną zastąpione przez znacznik . Pakiet XML_Serializer daje nam jednak jeszcze więcej swobody w wyborze domyślnych znaczników. Najlepszym rozwiązaniem byłoby, gdyby znacznik nagrań zawierał znaczniki dla każdego pojedynczego nagrania, a znacznik etykiet zawierał znacznik dla każdego studia nagrań, z którym artysta podpisał kontrakt. Można to łatwo sprokurować, ponieważ pakiet XML_Serializer umożliwia definiowanie różnych domyślnych nazw znacznika w zależności od kontekstu. Zamiast łańcucha za-

124

Rozdział 3. • Praca z formatem XML

wierającego domyślny znacznik trzeba po prostu opcji DEFAULT_TAG przesłać tablicę asocjacyjną. Klucze tej tablicy asocjacyjnej powinny definiować znaczniki nadrzędne, a wartości określać domyślną nazwę znacznika dla znaczników utworzonych w obrębie danego znacznika nadrzędnego: $serializer->setOption(XML_SERIALIZER_OPTION_DEFAULT_TAG, array( 'labels' => 'label', 'records' => 'record' ) );

Po tej modyfikacji otrzymamy następujący dokument XML:

Elvis Presley

Sun Records Sony Music

Viva Las Vegas Hound Dog In the Ghetto

Wiemy już, jak korzystać z najważniejszych opcji oferowanych przez pakiet XML_Serializer. Zanim jednak przystąpimy do implementowania skryptu, który utworzy dokument XML z przygotowanego wcześniej drzewa obiektów, warto zapoznać się z innymi opcjami oferowanymi przez pakiet XML_Serialize, które zestawiliśmy w tabeli: Nazwa opcji

Opis

Wartość domyślna

INDENT

Łańcuch używany do justowania znaczników za pomocą wcięć.

Pusty

LINEBREAKS

Łańcuch używany do dzielenia wierszy.

\n

XML_DECL_ENABLED

Określa, czy dodawać do tworzonego dokumentu deklarację XML. false

XML_ENCODING

Kodowanie dokumentu, jeśli opcji XML_DECL_ENABLED zostanie przypisana wartość true.

UTF-8

DOCTYPE_ENABLED

Informuje, czy dodawać do dokumentu deklarację typu dokumentu.

false

DOCTYPE

Nazwa pliku z deklaracją dokumentu. Wykorzystywana tylko wtedy, gdy opcji DOCTYPE_ENABLED zostanie przypisana wartość true.

Bez wartości domyślnej

125

PEAR. Programowanie w PHP

Nazwa opcji

Opis

Wartość domyślna

ROOT_NAME

Nazwa głównego znacznika dokumentu.

Zależy od serializowanych danych

ROOT_ATTRIBS

Atrybuty znacznika dokumentu.

Pusta tablica

NAMESPACE

Przestrzeń nazw wykorzystywana w dokumencie.

Bez wartości domyślnej

ENTITIES

Określa, czy kodować specjalne jednostki XML w danych znakowych i atrybutach.

true (prawda)

RETURN_RESULT

Określa, czy po udanej serializacji danych funkcja serialize() powinna zwracać pełny wynik serializacji, czy tylko wartość true.

false

CLASSNAME_AS ¦_TAGNAME

Określa, czy podczas serializowania obiektów należy używać nazwy klasy jako nazwy znacznika.

false

DEFAULT_TAG

Nazwa domyślnego znacznika. Opcja używana zazwyczaj podczas serializowania tablic indeksowanych. Nazwę można podać za pomocą łańcucha lub tablicy asocjacyjnej, by określić domyślne znaczniki w zależności od ich znacznika nadrzędnego.

XML_Serializer_Tag

TYPEHINTS

Określa, czy dodawać do znacznika informacje o typie.

false

ATTRIBUTE_TYPE

Nazwa atrybutu, który przechowuje informacje o typie, jeśli włączona jest opcja podpowiedzi TYPEHINTS.

_type

ATTRIBUTE_CLASS

Nazwa atrybutu, który przechowuje nazwę klasy, jeśli włączona jest opcja podpowiedzi TYPEHINTS.

_class

ATTRIBUTE_KEY

Nazwa atrybutu, który przechowuje nazwę klucza tablicy, jeśli włączona jest opcja TYPEHINTS.

_originalKey

SCALAR_AS ¦_ATTRIBUTES

Określa, czy do atrybutów powinny być dodawane wartości false skalarne (łańcuchy, liczby całkowite itp.).

PREPEND ¦_ATTRIBUTES

Łańcuch znaków poprzedzający nazwy atrybutów.

Bez wartości domyślnej

INDENT_ATTRIBUTES Łańcuch wykorzystywany do tworzenia wcięć justujących Bez wartości domyślnej atrybuty, kiedy każdy atrybut wyświetlany jest w pojedynczym wierszu. Opcji tej można przypisać wartość _auto. IGNORE_NULL

Opcja informująca, czy podczas serializowania obiektów i tablic ignorować wartości null.

false

TAGMAP

Tablica asocjacyjna umożliwiająca mapowanie kluczy i nazw właściwości na różne nazwy znaczników.

Bez wartości domyślnej

MODE

Określa, jakiego trybu należy używać podczas serializowania tablic indeksowanych: XML_SERIALIZER_MODE_DEFAULT czy XML_SERIALIZER_MODE_SIMPLEXML.

DEFAULT

126

Rozdział 3. • Praca z formatem XML

Nazwa opcji

Opis

Wartość domyślna

ATTRIBUTES_KEY

Wszystkie wartości przechowywane pod kluczem określonym przez tę opcję będą serializowane jako atrybuty.

Bez wartości domyślnej

CONTENT_KEY

Wszystkie wartości przechowywane pod kluczem określonym przez tę opcję będą traktowane jako dane znakowe, a nie dane służące do tworzenia kolejnego znacznika. Opcji tej należy używać w połączeniu z opcją ATTRIBUTES_KEY.

Bez wartości domyślnej

COMMENT_KEY

Wszystkie wartości przechowywane pod kluczem określonym przez tę opcję będą konwertowane na komentarze XML.

Bez wartości domyślnej

ENCODE_FUNC

Nazwa funkcji PHP lub metody stosowanej przed serializacją na każdej z serializowanych wartości.

Bez wartości domyślnej

Tworzenie dokumentu XML z drzewa obiektów Skoro zapoznaliśmy się z działaniem pakietu XML_Serializer, pora powrócić do zadania, które sobie wyznaczyliśmy, i utworzyć dokument XML z wcześniej utworzonych obiektów zawierających informacje o studiach nagraniowych (będących etykietami albumu), artystach oraz nagranych przez nich albumach. Ponieważ pakiet XML_Serializer akceptuje jako dane wykorzystywane do tworzenia dokumentu XML dowolną zmienną PHP, nasze zadanie najprościej można będzie zrealizować przesyłając zmienną $labels, która zawierać będzie jeden lub więcej obiektów Label. Dodatkowo określimy kilka opcji, które będą nam potrzebne: // załączamy klasę

require_once('XML/Serializer.php'); // tworzymy nowy obiekt

$serializer = new XML_Serializer(); // konfigurujemy deklarację XML

$serializer->setOption(XML_SERIALIZER_OPTION_XML_DECL_ENABLED, true); $serializer->setOption(XML_SERIALIZER_OPTION_XML_ENCODING, 'ISO-8859-1'); // konfigurujemy układ dokumentu

$serializer->setOption(XML_SERIALIZER_OPTION_INDENT, ' '); $serializer->setOption(XML_SERIALIZER_OPTION_LINEBREAKS, "\n"); // tworzymy dokument XML

$serializer->serialize($labels); // pobieramy dokument

echo $serializer->getSerializedData();

Kod ten utworzy następujący dokument XML, który nareszcie wygląda prawie tak, jak byśmy chcieli:

127

PEAR. Programowanie w PHP



Sun Records

1 Elvis Presley

SUN 209 That's All Right (Mama) & Blue Moon Of Kentucky July 19, 1954

SUN 210 Good Rockin' Tonight September, 1954



2 Carl Perkins

SUN 224 Gone, Gone, Gone July 19, 1954





Niemniej pozostało jeszcze kilka problemów do rozwiązania: „ Główny element w dokumencie powinien nazywać się . „ Wystąpienia znacznika powinny zostać zastąpione znacznikami , i . „ Niektóre ze znaczników (takie jak , i ) powinny zostać zastąpione odpowiadającymi im elementami. Wiemy już jednak z poprzednich przykładów, jak rozwiązać te problemy, używając odpowiednich opcji:

128

Rozdział 3. • Praca z formatem XML

„ Główny element można zmienić za pomocą opcji ROOT_NAME. „ Znaczniki można zastąpić przy użyciu opcji DEFAULT_TAG, przesyłając jej odpowiednią tablicę definiującą nowe nazwy znaczników. „ Za pomocą opcji SCALAR_AS_ATTRIBUTES można określić, które informacje powinny być serializowane jako atrybuty, a nie jako znaczniki. Oto kompletny skrypt, w którym wszystkie opcje zostały już odpowiednio zdefiniowane. Zmiany w stosunku do poprzedniej wersji skryptu zostały wytłuszczone: // załączamy klasę

require_once('XML/Serializer.php'); // tworzymy nowy obiekt

$serializer = new XML_Serializer(); // konfigurujemy deklarację XML

$serializer->setOption(XML_SERIALIZER_OPTION_XML_DECL_ENABLED, true); $serializer->setOption(XML_SERIALIZER_OPTION_XML_ENCODING, 'ISO-8859-1'); // konfigurujemy układ strony

$serializer->setOption(XML_SERIALIZER_OPTION_INDENT, ' '); $serializer->setOption(XML_SERIALIZER_OPTION_LINEBREAKS, "\n"); // konfigurujemy nazwy znaczników

$serializer->setOption(XML_SERIALIZER_OPTION_ROOT_NAME, 'labels'); $tagNames = array( 'labels' => 'label', 'artists' => 'artist', 'records' => 'record' ); $serializer->setOption(XML_SERIALIZER_OPTION_DEFAULT_TAG, $tagNames); $attributes = array( 'label' => array('name'), 'artist' => array('id'), 'record' => array('id', 'released') ); $serializer->setOption(XML_SERIALIZER_OPTION_SCALAR_AS_ATTRIBUTES, $attributes); $result = $serializer->serialize($labels); echo $serializer->getSerializedData();

Przesyłanie obiektów metodzie _sleep() Ostatni przykład udowodnił, że pakiet XML_Serializer jest w stanie sprawnie pracować nie tylko z tablicami, ale również z obiektami. Pobierze wszystkie publiczne właściwości obiektu i serializuje je dokładnie w taki sam sposób, jakby były wartościami przechowywanymi w tablicy.

129

PEAR. Programowanie w PHP

Niemniej w niektórych przypadkach może to nie być najwłaściwsze rozwiązanie. Rozważmy następujący przykład kodu: class UrlFetcher { public $url = null; public $html = null; public function __construct($url) { $this->url = $url; $this->html = file_get_contents($this->url); } } $pear = new UrlFetcher('http://pear.php.net'); $serializer = new XML_Serializer(); $serializer->setOption(XML_SERIALIZER_OPTION_XML_DECL_ENABLED, true); $serializer->setOption(XML_SERIALIZER_OPTION_XML_ENCODING, 'ISO-8859-1'); $serializer->setOption(XML_SERIALIZER_OPTION_INDENT, ' '); $serializer->serialize($pear); echo $serializer->getSerializedData();

Jeśli utworzymy nowy obiekt klasy UrlFetcher, obiekt ten pobierze zawartość HTML spod adresu URL zdefiniowanego w konstruktorze. Jeśli prześlemy ten obiekt pakietowi XML_ Serializer, wydobędzie on z obiektu wszystkie publiczne właściwości i doda je do przygotowywanego dokumentu XML. Dokument ten będzie wyglądać mniej więcej tak:

http://pear.php.net ...dla przejrzystości usunęliśmy większosć kodu HTML...

W tym przypadku jednak raczej nie zależy nam na tym, aby pakiet XML_Serializer umieszczał w dokumencie XML cały kod pobrany z dość rozbudowanej witryny pear.php.net. Można tego uniknąć korzystając z techniki, która może być znana czytelnikom z serializacji obiektów za pomocą funkcji serialize() języka PHP. Jeśli obiekt, który będzie serializowany przez pakiet XML_Serialize, implementuje metodę _sleep(), to będzie można ją przywołać i użyć zwróconą przez nią wartość w procesie serializacji. Metoda _sleep() powinna zwrócić tablicę zawierającą właściwości obiektu, które powinny zostać dołączone do przygotowywanego dokumentu. Aby zapobiec serializacji właściwości $html, należy teraz wprowadzić jedynie drobną zmianę w klasie UrlFetcher:

130

Rozdział 3. • Praca z formatem XML

class UrlFetcher { public $url = null; public $html = null; public function __construct($url) { $this->url = $url; $this->html = file_get_contents($this->url); } public function __sleep() { return array('url'); } }

Po wprowadzeniu tej zmiany skrypt przygotuje następujący dokument XML:

http://pear.php.net

Dodawanie informacji o typie Ostatnią z użytecznych funkcji pakietu XML_Serializer, o której warto tu wspomnieć, jest możliwość dodawania do znaczników XML informacji o typie. Funkcje tę włącza się za pomocą pojedynczej opcji: $serializer = new XML_Serializer(); // konfigurujemy deklarację XML

$serializer->setOption(XML_SERIALIZER_OPTION_XML_DECL_ENABLED, true); $serializer->setOption(XML_SERIALIZER_OPTION_XML_ENCODING, 'ISO-8859-1'); $serializer->setOption(XML_SERIALIZER_OPTION_TYPEHINTS, true); // konfigurujemy układ strony

$serializer->setOption(XML_SERIALIZER_OPTION_INDENT, ' '); $serializer->setOption(XML_SERIALIZER_OPTION_LINEBREAKS, "\n"); $serializer->setOption(XML_SERIALIZER_OPTION_DEFAULT_TAG, $tagNames); $result = $serializer->serialize($labels); echo $serializer->getSerializedData();

Przypisując opcji TYPEHINTS wartość true, informujemy pakiet XML_Serializer, aby do ujętych w znacznik danych dołączał jako atrybut informacje o typie oraz oryginalną nazwę klucza tablicy lub właściwości, jeśli nie może być użyta jako nazwa znacznika.

131

PEAR. Programowanie w PHP

Utworzony w ten sposób dokument (gdy metodzie serialize() przesyłana jest tablica obiektów Label) będzie wyglądał tak:

Sun Records

1 Elvis Presley

SUN 209 That's ... Kentucky July 19, 1954

SUN 210 Good Rockin' Tonight September, 1954



2 Carl Perkins

SUN 224 Gone, Gone, Gone July 19, 1954



132

Rozdział 3. • Praca z formatem XML



Możliwość dodawania informacji o typie okazuje się szczególnie przydatna, gdy zależy nam na możliwości przekonwertowania otrzymanych danych XML z powrotem na taką samą strukturę danych, z jakiej został utworzony. Dzięki niej możemy wykorzystywać pakiet XML_Serializer (i odwracający jego działanie pakiet XML_Unserializer, który omówimy w dalszej części tego rozdziału), jako wygodne zastępstwo dla funkcji serialize() i unserialize(). W tej części rozdziału omówiliśmy trzy różne pakiety umożliwiające tworzenie dokumentów XML. Pojawia się jednak pytanie, który z nich najlepiej pasować będzie do konkretnej sytuacji? „ W przypadku gdy mamy już wszystkie dane zebrane w jakiejś dużej strukturze danych, najlepszym rozwiązaniem z uwagi na bogactwo oferowanych opcji będzie pakiet XML_Serializer. „ Jeśli tworzymy strukturę z danych, które wyliczane są w czasie przygotowywania dokumentu, to najlepszym rozwiązaniem będzie zapewne pakiet XML_FastCreate. Można go również wykorzystać do tworzenia dokumentów XML w sposób programistyczny. Taka była zresztą intencja twórców tego pakietu. „ Z pakietu XML_Util należy natomiast korzystać wtedy, gdy chcemy przygotować bardzo mały dokument XML lub tylko fragment dokumentu.

Tworzenie aplikacji Mozilli za pomocą pakietu XML_XUL Dotychczas tworzyliśmy tylko dokumenty XML w zdefiniowanym przez nas samych formacie. Niemniej istnieją oczywiście aplikacje XML, które stworzyły pewne standardy formatów XML zaakceptowane przez konsorcjum W3C. Repozytorium PEAR dostarcza kilku pakietów, które ułatwiają tworzenie dokumentów XML dla tych aplikacji; jednym z nich jest pakiet XML_XUL.

Dokumenty XUL XUL to skrót od XML User Interface Language (oparty na XML język do tworzenia interfejsu użytkownika); jest on częścią projektu Mozilla. Dokładną specyfikację języka XUL v1.0 (wersja 1.0) można znaleźć w witrynie internetowej Mozilli, pod adresem: http://www.mozilla.org/projects/ ¦xul/xul.html. Język XUL wykorzystywany jest przez aplikacje Mozilli (takie jak Firefox czy Thunderbird) do definiowania, jaką strukturę powinien mieć interfejs użytkownika. Język XUL można łączyć z językiem JavaScript, arkuszami CSS i szkieletem RDF, by tworzyć interaktywne aplikacje, które będą mogły sięgać do wielu różnych źródeł danych. Prawdę powiedziawszy, obecnie praktycznie każdy dodatek dla przeglądarki Firefox lub programu pocztowego Thunderbird jest przygotowany za pomocą języków XUL i JavaScript. Przy użyciu języka XUL

133

PEAR. Programowanie w PHP

znacznie łatwiej niż za pomocą HTML tworzy się rozbudowane interfejsy użytkownika, ponieważ język XUL został zaprojektowany właśnie w tym celu, podczas gdy język HTML miał pierwotnie służyć tylko publikowaniu w sieci WWW informacji posiadających określoną strukturę. Dlatego też język HTML oferuje znaczniki umożliwiające porządkowanie tekstu w akapitach, listach i statycznych tabelach HTML, natomiast język XUL udostępnia znaczniki pozwalające na tworzenie sortowalnych siatek danych, pól wyboru koloru czy drzew podobnych do drzewa katalogów w programie Windows Explorer (Eksplorator Windows). Zakończmy już jednak ten wykład; przyjrzyjmy się dokumentowi XUL:

























134

Rozdział 3. • Praca z formatem XML









Miejsce na inne informacje.



Ponieważ język XUL jest zasadniczo odmianą języka XML, dokument ten zaczyna się od deklaracji XML. Po niej następuje kolejna deklaracja, która służy do załączania arkuszy stylów spod adresu URL chrome://global/skin/. Należy wyjaśnić, że chrome to specjalny protokół wykorzystywany wtedy, gdy zachodzi konieczność sięgania do wewnętrznych danych Mozilli. W tym przypadku służy do załączania arkusza stylów, który użytkownik wybrał dla swojej instalacji oprogramowania Mozilla, dzięki czemu wygląd aplikacji będzie idealnie współgrał z wyglądem przeglądarki internetowej. Po tej deklaracji pojawia się element główny całego dokumentu. W większości przypadków będzie to element okna . Wewnątrz elementu zagnieżdżonych zostało kilka innych elementów, takich jak i . Jeśli otworzymy ten dokument w przeglądarce Firefox lub Mozilla, powinniśmy zobaczyć następujący widok:

Rysunek 3.1. Kontrolka listy przygotowana za pomocą XUL

135

PEAR. Programowanie w PHP

Oczywiście dokładny układ zależeć będzie od motywu (ang. theme), który wykorzystujemy w naszej instalacji przeglądarki Mozilla lub Firefox. Jeśli spróbujemy kliknąć w obrębie tego okna, przekonamy się, że zakładki u góry list oraz element drzewa są w pełni funkcjonalne i możemy ukrywać poszczególne kolumny elementu drzewa. Spróbujmy wyobrazić sobie implementowanie takiego interfejsu za pomocą języka HTML, arkuszy stylów CSS lub języka JavaScript. Ile godzin pracy by nam to zajęło! Przykład ten pokazuje zalety XUL w stosunku do języka XML — XUL jest znakomitym narzędziem do tworzenia interfejsów użytkownika dla aplikacji WWW. Niemniej język XUL ma również swoją ciemniejszą stronę: „ Język XUL działać będzie tylko w aplikacjach przygotowanych w ramach projektu Mozilla. Ani użytkownicy przeglądarek Microsoft Internet Explorer ani Opera nie będą w stanie korzystać z naszych aplikacji. „ Język XUL (podobnie jak większość aplikacji XML) jest dość rozbudowany i zawiera wiele wielokrotnie zagnieżdżonych dokumentów XML.

Tworzenie dokumentów XUL za pomocą pakietu XML_XUL Repozytorium PEAR dostarcza pakietu oprogramowania, który pozwala rozwiązać drugie ze wspomnianych problemów: za pomocą pakietu XML_XUL można utworzyć dokument XUL, korzystając z łatwego w użyciu interfejsu API języka PHP. Interfejs API pakietu XML_XUL przypomina standardowy interfejs DOM-API — za pomocą pakietu budujemy w pamięci drzewo obiektów, które następnie możemy przenosić i modyfikować do czasu, aż osiągniemy pożądany rezultat. Gdy już będziemy zadowoleni z przygotowanego drzewa, możemy je serializować tworząc kod XML, który następnie można wysłać do przeglądarki. Różnica w stosunku do modelu DOM (ang. Document Object Model, model obiektów dokumentu) polega na tym, że tym razem mamy do dyspozycji nie jedną klasę, jak w modelu DOM, ale kilka różnych klas dla różnych elementów interfejsu (ang. widgets) oferowanych przez język XUL. Klasy dostarczają pomocniczych metod, które umożliwiają na przykład dodawanie nowej zakładki do kart z zakładkami za pomocą pojedynczego wywołania metody, bez konieczności budowania na własną rękę całego drzewa obiektów. Podstawowe kroki wykonywane podczas tworzenia skryptu za pomocą pakietu XML_XUL zawsze są takie same: 1. Załączenie głównej klasy XML_XUL. 2. Utworzenie nowego dokumentu. 3. Utworzenie nowych elementów i złożenie w pamięci drzewa obiektów. 4. Serializacja dokumentu XUL i wysłanie go do przeglądarki. Skomplikowana procedura? Prawdę powiedziawszy, nie jest tak uciążliwa, jak by się mogło wydawać. Oto nasz pierwszy skrypt korzystający z pakietu XML_XUL: require_once 'XML/XUL.php'; // tworzymy nowy dokument

$doc = XML_XUL::createDocument(); // łączymy go z arkuszem stylów wybranym przez użytkownika

$doc->addStylesheet('chrome://global/skin/');

136

Rozdział 3. • Praca z formatem XML

// tworzymy nowe okno

$win = $doc->createElement('window',array( 'title'=> 'Simple XUL' ) ); // dodajemy je do dokumentu

$doc->addRoot($win); // tworzymy kolejny element

$desc = $doc->createElement('description', array(), 'To w istocie jest XUL.'); $win->appendChild($desc); header('Content-type: application/vnd.mozilla.xul+xml'); $doc->send();

Wykonaliśmy w nim właśnie czynności opisane wyżej. Dołączyliśmy główną klasę i utworzyliśmy nowy dokument korzystając z metody XML_XUL::createDocument(). Następnie zamiast dostarczać naszego własnego arkusza stylów CSS, utworzyliśmy wewnętrzny arkusz stylów CSS za pomocą metody addStylesheet(). Potem przystąpiliśmy do tworzenia elementów interfejsu i komponowania ich w drzewo obiektów (jest to prawdę powiedziawszy dość malutkie drzewko). Wszystkie elementy dodawane do dokumentu muszą być tworzone za pomocą metody createElement(), której przesyła się następujące parametry: „ Nazwę elementu, która będzie również nazwą tworzonego znacznika. „ Tablicę asocjacyjną zawierającą atrybuty elementu. „ Zawartość elementu. „ Wartość logiczną określającą, czy należy zastępować jednostki XML pojawiające się w treści dokumentu (domyślnie parametr ten ma wartość true, co włącza zastępowanie). Metoda ta zwraca instancję podklasy XML_XUL_Element. Aby dowiedzieć się, jakie elementy interfejsu obsługuje pakiet XML_XUL, należy zajrzeć do folderu XML/XUL/Element w katalogu naszej instalacji repozytorium PEAR. Drzewo elementów buduje się dodając do odpowiedniego już istniejącego elementu, za pomocą jego metody appendChild(), nowy element potomny. Gdy już zakończymy budowanie drzewa, należy przygotować odpowiedni nagłówek, aby przeglądarka Firefox wiedziała, jak powinna traktować dane, i wysłać go do przeglądarki za pomocą metody send(). Jeśli teraz otworzymy skrypt w przeglądarce, powinien ukazać się nasz pierwszy, dynamicznie utworzony dokument XUL. Jeśli przyjrzymy się kodowi źródłowemu dokumentu, łatwo wyłowimy niezbędny kod XUL:



137

PEAR. Programowanie w PHP

To w istocie jest XUL.

Bez trudu zauważymy tutaj elementy i , utworzone za pomocą metody createElemnt(). Wspomnieliśmy już, że pakiet XML_XUL umożliwia tworzenie dokumentów XUL w łatwy sposób z poziomu kodu PHP, bez konieczności sięgania po model DOM. Spróbujmy teraz wprowadzić pierwsze usprawnienia: require_once 'XML/XUL.php'; // tworzymy nowy dokument

$doc = XML_XUL::createDocument(); // łączymy się z arkuszem stylów określonym przez użytkownika

$doc->addStylesheet('chrome://global/skin/'); // tworzymy nowe okno

$win = $doc->createElement('window',array( 'title'=> 'Simple XUL' ) ); // dodajemy je do dokumentu

$doc->addRoot($win); $win->addDescription('To w istocie jest XUL.'); header( 'Content-type: application/vnd.mozilla.xul+xml' ); $doc->send();

Różnica pomiędzy tym przykładem a poprzednim polega na tym, że nowy element do okna dodajemy za pomocą metody $win->addDescription(), zamiast tworzyć i dodawać element ręcznie. Metoda ta jest obsługiwana przez wszystkie klasy reprezentujące elementy, ponieważ dodawanie zawartości tekstowej (opisu) jest jedną z częściej wykonywanych czynności. Następnym krokiem jest utworzenie drzewa podobnego do tego wyświetlanego we wcześniejszym przykładzie. Główny elementem potrzebnym do tego będzie klasa XML_XUL_Element_Tree, którą tworzy się dokładnie tak samo jak każdy inny element: $tree = $doc->createElement('Tree', array( 'flex' => 1, 'height' => 200 ) );

Aby zakończyć tworzenie drzewa, będziemy musieli utworzyć zagnieżdżone elementy i , które definiować będą kolumny drzewa. Za pomocą pakietu ZML_XUL robi się to o wiele prościej. Klasa XML_XUL_Element_Tree udostępnia metodę, która utworzy je dla nas:

138

Rozdział 3. • Praca z formatem XML

$tree->setColumns(3, array( 'id' => 'id', 'label' => 'Id', 'flex' => 1, 'primary' => 'true' ), array( 'id' => 'name', 'label' => 'Name', 'flex' => 1 ), array( 'id' => 'email', 'label' => 'E-Mail', 'flex' => 1 ) );

W pierwszym argumencie określamy liczbę kolumn, które chcemy utworzyć, a w następnych argumentach przesyłamy tablice atrybutów dla każdej z nich. Po utworzeniu tej podstawowej struktury możemy zacząć dodawać do drzewa odpowiednie dane, używając do tego celu metody addItem() elementu Tree: $sun = $tree->addItem(array('SUN', 'Sun Records', '[email protected]'));

Przywołując tę metodę, musimy przesłać jej tablicę zawierającą wartości dla każdej kolumny. Możemy przesłać również wartość łańcuchową, która używana będzie jako etykieta, lub tablicę asocjacyjną zawierającą wszystkie atrybuty danej kolumny. Metoda ta zwróci instancję klasy XML_XUL_Element_Treeitem, którą można zachować w zmiennej do późniejszego użytku. Na przykład możemy do tego elementu bezpośrednio dodawać elementy potomne, ponieważ nie budujemy prostej tabeli, a rekurencyjną strukturę drzewa: $sun->addItem(array('elvis', 'Elvis Presley', '[email protected]')); $sun->addItem(array('carl', 'Carl Perkins', '[email protected]'));

Oczywiście nadal mamy możliwość dodawania do drzewa kolejnych elementów najwyższego poziomu lub nawet głębszego zagnieżdżania drzewa, przywołując w tym celu metodę addItem() na wartościach zwróconych przez wcześniejsze odwołania do metody addItem(). Gdy już zbudujemy drzewo, dodamy je do okna: $win->appendChild($tree);

Jeśli otworzymy gotowy skrypt w oknie przeglądarki kompatybilnej z projektem Mozilla, zobaczymy interaktywny element kontrolny w formie drzewa. Podstawowa różnica między tym drzewem a drzewem z pierwszego przykładu jest taka, że nasze drugie drzewo zostało zbudowane dynamicznie za pomocą języka PHP, tak więc możemy korzystać z dowolnych zasobów oferowanych przez język PHP, gdy chcemy zapełnić drzewo danymi.

139

PEAR. Programowanie w PHP

Tworzenia karty z zakładkami W tym podrozdziale dowiemy się, jak dodawać do naszego przykładu kontrolkę z zakładkami. Robi się to podobnie jak w przypadku kontrolki drzewa — korzystamy z elementu XML_XUL_ Element_Tabbox, który tworzy się dokładnie tak samo jak każdy inny element: $tabbox = &$doc->createElement('Tabbox', array('height' => 500)); $win->appendChild($tabbox);

Po utworzeniu elementu tabbox dodajemy go do głównego okna. Ten nowo utworzony obiekt posiada metodę addTab(), która umożliwia tworzenie nowych zakładek: $tab1 = $tabbox->addTab('Labels');

Do obiektu zwróconego przez metodę addTab() można dodawać dowolne elementy potomne. Potomek tego elementu będzie wykorzystywany jako zawartość utworzonej zakładki. Metodzie addTab() można przesłać kilka parametrów: „ Etykietę zakładki. „ Element XML_XUL_Element, który można wykorzystać jako zawartość zakładki. „ Tablicę zawierającą atrybuty zakładki. „ Tablicę zawierającą atrybuty panelu zakładek. Skoro dowiedzieliśmy się już, jak za pomocą XML_XUL tworzyć karty z zakładkami, możemy teraz zaimplementować skrypt, który tworzy kod XUL przedstawiony na początku tego podrozdziału. require_once 'XML/XUL.php'; // tworzymy nowy dokument

$doc = XML_XUL::createDocument(); // łączymy go z arkuszem stylów dostarczonym przez użytkownika

$doc->addStylesheet('chrome://global/skin/'); // tworzymy nowe okno

$win = $doc->createElement('window',array( 'title'=> 'Simple XUL' ) ); // dodajemy je do dokumentu

$doc->addRoot($win); // Tworzymy karty z zakładkami i dodajemy je do okna

$tabbox = &$doc->createElement('Tabbox', array('height' => 500)); $win->appendChild($tabbox); // Tworzymy nowe drzewo

140

Rozdział 3. • Praca z formatem XML

$tree = &$doc->createElement('Tree', array( 'flex' => 1, 'height' => 200 ) ); // Określamy etykiety kolumn

$tree->setColumns(3, array( 'id' => 'id', 'label' => 'Id', 'flex' => 1, 'primary' => 'true' ), array( 'id' => 'name', 'label' => 'Name', 'flex' => 1 ), array( 'id' => 'email', 'label' => 'E-Mail', 'flex' => 1 ) ); // dodajemy nowy element do drzewa

$sun = $tree->addItem(array('SUN', 'Sun Records', 'info@sun-records. com')); // dodajemy do utworzonego elementu dwa nowe elementy niższego rzędu

$sun->addItem(array('elvis', 'Elvis Presley', '[email protected]')); $sun->addItem(array('carl', 'Carl Perkins', '[email protected]')); // dodajemy nowy element do drzewa

$tree->addItem(array('SONY', 'Sony Records', '[email protected]')); // dodajemy nową zakładkę do etykiety, zawartością zakładki będzie drzewo

$tabbox->addTab('Labels', $tree, array(), array('height' => 200)); // dodajemy kolejną zakładkę, jednak bez zawartości

$tab2 = $tabbox->addTab('Misc'); // do drugiej zakładki dodajemy przykładową zawartość tekstową

$tab2->addDescription('Tutaj umieść jakąś zawartość.'); header( 'Content-type: application/vnd.mozilla.xul+xml' ); $doc->send();

141

PEAR. Programowanie w PHP

W większości przypadków tworzenie kodu XUL za pomocą języka PHP i pakietu XML_XUL będzie prostsze niż pisanie kodu XUL ręcznie — cały przykładowy kod XUL prezentowany w tej książce został utworzony przy użyciu języka PHP. Pakiet XML_XUL umożliwia odczytywanie już istniejących dokumentów XUL, modyfikowanie ich i zapisywanie z powrotem w pliku lub wyświetlanie w przeglądarce WWW. Co więcej, pakiet XML_XUL udostępnia informacje debugowania ułatwiające analizowanie drzew obiektów przechowywanych w pamięci. Na koniec wreszcie warto wspomnieć, że pakiet XML_XUL oferuje klasy pozwalające na tworzenie ponad 70 różnych elementów nawigacyjnych XUL.

Przetwarzanie dokumentów XML W pierwszej części tego rozdziału dowiedzieliśmy się, jak tworzyć dokumenty XML z dowolnego źródła danych, używając różnych pakietów PEAR. Niemniej tworzenie kodu XML nie będzie miało sensu, jeśli użytkownik po drugiej stronie nie będzie mógł przetwarzać dokumentów XML, które przygotowaliśmy. Dlatego też w drugiej części tego rozdziału dowiemy się, które z pakietów repozytorium PEAR można wykorzystać do przetwarzania dokumentów XML. Potrzeba przetwarzania dokumentów XML pojawić się może w wielu różnych sytuacjach, ponieważ język XML staje się coraz popularniejszym narzędziem do tworzenia oprogramowania. Oto kilka typowych scenariuszy, w których może zajść konieczność odczytywania i pobierania informacji z dokumentów XML: „ Odczytywanie plików konfiguracyjnych w formacie XML. „ Importowanie do aplikacji danych, które zostały wyeksportowane przez inną aplikację właśnie w formacie XML. „ Wyświetlanie w naszej witrynie WWW zawartości, która została syndykowana przez inną aplikację lub witrynę WWW. „ Akceptowanie żądań usług WWW. „ Analiza odpowiedzi nadesłanej przez usługę WWW. Dwa ostatnie scenariusze zostaną dokładniej omówione w następnym rozdziale, istnieje jednak wiele zastosowań dokumentów XML niezwiązanych z usługami WWW. Repozytorium PEAR przychodzi tutaj programistom z pomocą. Zanim jednak przyjrzymy się pakietom PEAR odpowiedzialnym za analizę kodu XML, warto powiedzieć najpierw kilka słów o obsłudze języka XML w PHP. W PHP4 istniał tylko jeden pewny sposób pracy z kodem XML, oparte na parserze expat rozszerzenie xml. Rozszerzenie to umożliwiało analizowanie dokumentów XML korzystając z interfejsu API SAX. Interfejs SAX, którego nazwa jest skrótem od: Simple API to XML (Prosty interfejs API dla języka XML), oparty jest na zdarzeniach. Kiedy korzystamy z interfejsu SAX API, po prostu definiujemy kilka funkcji lub metod obsługujących różne zdarzenia, które mogą zajść podczas analizowania dokumentu. Mogą to być takie zdarzenia, jak otwieranie znaczni-

142

Rozdział 3. • Praca z formatem XML

ków i zamykanie znaczników, otwieranie i zamykanie danych znakowych, przetwarzania instrukcji czy przetwarzanie komentarzy XML. Po zarejestrowaniu wszystkich niezbędnych funkcji zwrotnych należy przesłać dokument do parsera, który zanalizuje go znak po znaku, stopniowo przesuwając swój wewnętrzny kursor od początku do końca dokumentu. O analizowaniu dokumentów XML opartym na interfejsie SAX opowiemy dokładniej w dalszej części rozdziału, gdy omawiać będziemy pakiet XML_Parser. Język PHP5 oferuje natomiast cztery rozszerzenia, które ułatwiają przetwarzanie danych XML: „ rozszerzenie ext/xml, kompatybilne z wersją używaną w PHP4. „ rozszerzenie ext/dom, zgodne z modelem DOM, tak jak określony jest w standardzie W3C. „ rozszerzenie ext/simplexml, stosujące zupełnie nowe podejście, unikatowe dla języka PHP. „ rozszerzenie ext/xmlreader, parser XML będący czymś pośrednim pomiędzy interfejsem SAX a modelem DOM. Przyglądając się tym interfejsom API, można nabrać mylnego przekonania, że zastosowanie pakietów z repozytorium PEAR do przetwarzania kodu XML nie niesie za sobą żadnych korzyści. Nie jest to jednak prawdą. Wszystkie wspomniane interfejsy API są interfejsami API niskiego poziomu, natomiast repozytorium PEAR oferuje kilka pakietów, które działają na wyższym poziomie programowania i dlatego za ich pomocą praca z dokumentami XML staje się znacznie łatwiejsza. Na kolejnych stronach omówimy trzy różne pakiety: XML_Parser, XML_Unserializer i XML_ RSS. Wszystkie te pakiety oparte są na interfejsie API SAX, dlatego też będą działać zarówno w PHP4, jak i w PHP5. Różnica pomiędzy nimi polega na tym, że pakiet XML_Parser umożliwia odczytywanie dowolnego dokumentu XML, podczas gdy pakiet XML_Unserializer potrafi przetwarzać dowolny dokument XML, natomiast pakiet XML_RSS przeznaczony jest wyłącznie do analizy danych RSS.

Analizowanie danych XML za pomocą pakietu XML_Parser Pakiet XML_Parser jest obiektowym obudowaniem funkcji analizujących kod XML, dostępnych w języku PHP. Dokumentację tych funkcji można znaleźć w witrynie WWW języka PHP pod adresem: http://www.php.net/xml. Funkcje te umożliwiają przetwarzanie dowolnego dokumentu XML za pomocą interfejsu API SAX. Kiedy korzystamy z interfejsu API SAX, parser stopniowo przesuwa swój wewnętrzny kursor analizując dane dokumentu i jednocześnie w tym samym czasie dzieli dokument na odpowiednie tokeny (wykonuje jego tokenizację). Oto przykłady możliwych tokenów: „ Znaczniki otwierające lub zamykające (puste znaczniki traktowane są w ten sam sposób jak znaczniki otwierające i zamykające, pomiędzy którymi nie ma żadnych danych). „ Dane znakowe.

143

PEAR. Programowanie w PHP

„ Instrukcje przetwarzające, takie jak lub . „ Zewnętrzne jednostki odwołujące się do innych dokumentów XML. „ Deklaracje notacji. „ Nieanalizowane deklaracje jednostek. „ Inne elementy dokumentów XML, takie jak deklaracja typu dokumentu lub komentarze XML. Podczas przesuwania kursora przez dokument parser będzie dla każdego odnalezionego tokenu uruchamiać odpowiednie zdarzenie. Nasza aplikacja powinna obsługiwać te zdarzenia za pomocą odpowiednich funkcji zwrotnych PHP (ściślej: funkcji, metod lub metod statycznych) i pobierać informacje, które chcemy wydobyć z dokumentu. Przed przystąpieniem do analizy dokumentu konieczne będzie zarejestrowanie funkcji zwrotnych dla wszystkich tokenów, które chcemy obsługiwać. Dlatego też typowy kod PHP korzystający z funkcji rozszerzenia xml będzie wyglądać mniej więcej tak: // pobieramy nowy zasób parsera

$xml_parser = xml_parser_create(); // rejestrujemy funkcje zwrotne dla znaczników otwierających i zamykających. // Oryginalne implementacje funkcji startElement() i endElement() // zostaną zignorowane

xml_set_element_handler($xml_parser, "startElement", "endElement"); // otwieramy plik, który chcemy analizować

if (!($fp = fopen($file, "r"))) { die("could not open XML input"); } // odczytujemy plik i przesyłamy odczytane dane do parsera, który zmieni je w tokeny // Jeśli pojawi się błąd (np. dokument jest nieprawidłowo sformatowany, // to kończymy wykonywanie skryptu)

while ($data = fread($fp, 4096)) { if (!xml_parse($xml_parser, $data, feof($fp))) { die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser))); } } // zwalniamy zasób parsera

xml_parser_free($xml_parser);

Podczas korzystania z tych funkcji będziemy używać tego samego kodu wielokrotnie w różnych miejscach naszej aplikacji, ponieważ zawsze konieczne będzie „pobranie” zasobu parsera, zarejestrowanie funkcji zwrotnych, otworzenie plików lub innych strumieni danych i na koniec wreszcie zwolnienie przygotowanego parsera.

144

Rozdział 3. • Praca z formatem XML

Zaczynamy pracę z pakietem XML_Parser Pakiet XML_Parser powstał po to, aby umożliwić programiście wielokrotne wykorzystywanie kodu tworzonego na potrzeby analizy dokumentów XML za pomocą interfejsu API SAX. Ponadto oferuje kilka wygodnych funkcji i pozwala na korzystanie z funkcji interfejsu SAX w sposób właściwy dla programowania obiektowego, które jest najwłaściwszym rozwiązaniem w przypadku dużych aplikacji. Aby nauczyć się, jak korzystać z pakietu XML_Parser, skorzystamy z następującego przykładowego dokumentu XML:

/usr/share/php/myapp /tmp/myapp /var/www/skins/myapp

mysql://user:pass@localhost/myapp myapp_

mysql://root:@localhost/myapp myapp_testing_

Dokument ten mógł zostać na przykład skopiowany z aplikacji, która korzysta z plików konfiguracyjnych bazujących na formacie XML. Dane konfiguracyjne zostały podzielone na różne sekcje, konfigurujące różne części aplikacji. W tym przykładzie mamy sekcje konfigurujące foldery wykorzystywane podczas załączania plików szablonów, plików tymczasowych oraz sekcję konfigurującą dostęp do bazy danych. Ta ostatnia sekcja konfigurująca dostęp do bazy danych pojawia się w pliku konfiguracyjnym dwukrotnie i atrybut environment został dodany do obu tych sekcji. Takie osobne sekcje można wykorzystywać do przechowywania w tym samym pliku osobnych konfiguracji na potrzeby różnych środowisk: testowania, publikowania i udostępniania zawartości online. Na kolejnych stronach za pomocą pakietu XML_Parser zaimplementujemy skrypt odczytujący konfigurację, który będzie mógł zanalizować zaprezentowany tu plik, uwzględniając jednocześnie aktualnie wykorzystywane środowisko. Z pakietu XML_Parser korzysta się odrobinę inaczej niż z innych pakietów PEAR, których używaliśmy wcześniej. Zamiast tworzyć nową instancję obiektu klasy XML_Parser, przygotujemy nową klasę rozszerzającą bazową klasę XML_Parser i utworzymy instancję obiektu właśnie tej klasy. W nowej klasie trzeba będzie zaimplementować tylko różne funkcje obsługi dla każdego z tokenów, które chcemy analizować. Wszystkie pozostałe czynności niezbędne do przeprowadzenia analizy dokumentu (takie jak pobranie parsera, otwarcie plików, obsługa błędów itd.) wykonywane są automatycznie przez klasę bazową. Aby pakiet XML_Parser 145

PEAR. Programowanie w PHP

mógł przywoływać różne funkcje zwrotne, które przygotujemy dla różnych tokenów, należy podczas implementowania funkcji zwrotnej w przygotowywanej przez nas klasie stosować się do jego konwencji nazewniczej. W przedstawionej tutaj tabeli prezentujemy wszystkie możliwe funkcje zwrotne wraz z odpowiednimi dla nich nazwami. Sygnatury metod są dokładnie takie same, jak to zostało opisane w dokumentacji języka PHP. Token

Nazwa funkcji zwrotnej

znacznik otwierający

startElement

znacznik zamykający

endElemnt

dane znakowe

cdatHandler

zewnętrzne jednostki

entityrefHandler

instrukcje przetwarzania

piHandler

nieanalizowane deklaracje jednostek

unparsedHandler

deklaracje notacji

notationHandler

wszystkie pozostałe tokeny

deafultHandler

Implementowanie funkcji zwrotnych Teraz, kiedy już znamy nazwy funkcji zwrotnych, zaimplementowanie naszej pierwszej klasy, która analizować będzie dokument XML, nie powinno być niczym trudnym. Przyjrzyjmy się następującemu kodowi klasy: // załączamy klasę bazową

require_once 'XML/Parser.php'; // tworzymy klasę rozszerzającą klasę XML_Parser

class ConfigReader extends XML_Parser { /** * zajmujemy się znacznikami otwierającymi * * @param zasób parsera * @param łańcuch z nazwą znacznika * @param tablica atrybutów */

public function startHandler($parser, $name, $attribs) { echo "Znaleziono element początkowy $name\n"; } /** * zajmujemy się danymi znakowymi * * @param zasób parsera * @param łańcuch z danymi znakowymi */

146

Rozdział 3. • Praca z formatem XML

public function cdataHandler($parser, $cData) { $cData = trim($cData); if ($cData === '') { return; } echo "...data '$cData' found\n"; } /** * obsługujemy znaczniki zamykające * * @param zasób parsera * @param łańcuch z nazwą znacznika */

public function endHandler($parser, $name) { echo "Znaleziono element końcowy $name\n"; } } // Tworzymy nową instancję klasy

$config = new ConfigReader(); // podajemy nazwę analizowanego pliku

$config->setInputFile('config.xml'); // analizujemy plik i przechwytujemy błędy

$result = $config->parse(); if (PEAR::isError($result)) { echo 'Analiza zakończona niepowodzeniem: ' . $result->getMessage(); } $config->free();

W naszym przykładzie musimy obsłużyć tylko trzy różne typy tokenów: znaczniki otwierające, znaczniki zamykające i dane znakowe ujęte w te znaczniki. Dlatego też musimy zaimplementować w naszej klasie rozszerzającej bazową klasę XML_Handler tylko trzy metody: startElement() dla elementu otwierającego, endElement() dla elementu zamykającego i cDataHandler() dla danych znakowych. Nasz pierwszy przykład, by zapoznać się z działaniem pakietu XML_Parser, prezentować będzie tylko pewne informacje debugowania. Zaraz po zaimplementowaniu nowej klasy ConfigReader tworzymy jej nową instancję. Ponieważ klasa ta jest rozszerzeniem klasy XML_Parser, oferuje więc już wiele użytecznych metod służących do analizy i przetwarzania kodu XML. Jedną z nich jest metoda setInputFile(), dająca programiście możliwość przesłania pliku (lub jakiegokolwiek innego strumienia), który chcemy zanalizować. Aby rozpocząć właściwe przetwarzanie, będziemy musieli przywołać metodę parse(). Metoda ta zwraca albo wartość true (prawda), jeśli dokument można zanalizować, albo instancję klasy PEAR_Error (błędu PEAR), jeśli podczas przetwarzania wystąpią jakieś problemy. Jeśli prześlemy klasie nasz przykładowy dokument XML, na ekranie powinny pojawić się następujące informacje:

147

PEAR. Programowanie w PHP

Znaleziono element początkowy CONFIGURATION Znaleziono element początkowy SECTION Znaleziono element początkowy INCLUDES ...data '/usr/share/php/myapp' Znaleziono element końcowy INCLUDES Znaleziono element początkowy CACHE ...data '/tmp/myapp' Znaleziono element końcowy CACHE Znaleziono element początkowy TEMPLATES ...data '/var/www/skins/myapp' Znaleziono element końcowy TEMPLATES Znaleziono element końcowy SECTION Znaleziono element początkowy SECTION Znaleziono element początkowy DSN ...data 'mysql://user:pass@localhost/myapp' Znaleziono element końcowy DSN Znaleziono element początkowy PREFIX ...data 'myapp_' Znaleziono element końcowy PREFIX Znaleziono element końcowy SECTION Znaleziono element początkowy SECTION Znaleziono element początkowy DSN ...data 'mysql://root:@localhost/myapp' Znaleziono element końcowy DSN Znaleziono element początkowy PREFIX ...data 'myapp_testing_' Znaleziono element końcowy PREFIX Znaleziono element końcowy SECTION Znaleziono element końcowy CONFIGURATION

Już na pierwszy rzut oka widać, że zwrócone dane nie wyglądają dokładnie tak, jak oczekiwaliśmy. Co prawda funkcje zwrotne obsługujące znaczniki otwierające i zamykające oraz dane są przywoływane w tym samym porządku, w jakim elementy te pojawiały się w źródłowym dokumencie, niemniej wszystkie nazwy znaczników zostały przekonwertowane na wielkie litery. Jest to domyślne zachowanie pakietu XML_Parser, ale można je łatwo wyłączyć, dodając do implementowanej klasy ConfigReader kolejną właściwość: // create a class that extends XML_Parser

class ConfigReader extends XML_Parser { /** * wyłączamy zamianę na wielkie litery */

public $folding = false; /* ... reszta kodu pozostaje taka sama ... */

}

Po przypisaniu tej właściwości wartości false, pakiet XML_Parser nie będzie zmieniał wielkości znaków w nazwach znaczników przed przesłaniem ich do funkcji zwrotnych.

148

Rozdział 3. • Praca z formatem XML

Dodawanie kodu do funkcji zwrotnych Teraz, gdy wiemy już, jak działa pakiet XML_Parser, możemy wykorzystać go do zaimplementowania potrzebnego nam skryptu odczytującego konfigurację. Ponieważ w trakcie analizy pliku konfiguracyjnego musimy zapisywać informacje o stanie, zaczniemy od dodania do naszej klasy kilku właściwości. /** * Klasa odczytująca pliki konfiguracyjne XML */

class ConfigReader extends XML_Parser { /** * wyłączamy zmienianie liter na duże */

public $folding = false; /** * sekcje, które zostały już zanalizowane */

private $sections = array(); /** * wybrane środowisko */

private $environment; /** * tymczasowe przechowywanie danych w trakcie analizy */

private $currentSection = null; private $currentData = null; }

Właściwość $sections zostanie później wykorzystana do przechowywania opcji konfiguracyjnych. Właściwość $environment będzie przechowywać informacje o środowisku, w którym będziemy korzystać z naszego parsera odczytującego konfigurację. Natomiast ostatnie dwie właściwości będą używane do tymczasowego przechowywania bieżącej sekcji, gdy kursor i bieżące dane znakowe znajdują się w obrębie znacznika . W następnej kolejności zaimplementujemy konstruktor, któremu przesyłać będziemy w momencie instancjacji obiektu parsera dane o wybranym środowisku: /** * Tworzymy nowy analizator konfiguracji ConfigReader * * @param string, łańcuch określający wykorzystywane środowisko */ public function __construct($environment = 'online') { parent::__construct();

149

PEAR. Programowanie w PHP

$this->environment = $environment; $this->folding = false; }

Konstruktor wymaga przesłania mu jako parametru łańcucha, którego wartość zostanie zapisana we właściwości $environment. Teraz, gdy określiliśmy już wszystkie właściwości i przygotowaliśmy konstruktor klasy, zajmiemy się implementowaniem logiki samych funkcji zwrotnych. Pierwsza będzie funkcja zwrotna dla znaczników otwierających: /** * zajmujemy się znacznikami otwierającymi * * @param resource zasób parsera * @param string łańcuch z nazwą znacznika * @param array tablica atrybutów */ public function startHandler($parser, $name, $attribs) { switch ($name) { case 'configuration': break; case 'section': // sprawdzamy, czy zostało określone właściwe środowisko if (!isset($attribs['environment']) || $attribs['environment'] == $this->environment) { // zapisujemy nazwę sekcji $this->currentSection = $attribs['name']; // tworzymy pustą tablicę dla tej sekcji $this->sections[$this->currentSection] = array(); } break; default: $this->currentData = ''; break; } }

Technika wykorzystana w kodzie tej funkcji jest dość powszechnie stosowana podczas implementowania parserów kodu XML bazujących na interfejsie SAX. Instrukcja przełącznika switch wykorzystywana jest do wykonywania różnych akcji w zależności od nazwy znacznika. Jeśli wykryty zostanie otwierający znacznik , parser go zignoruje. Jeśli wykryty zostanie znacznik , parser sprawdza, czy w znaczniku została określona wartość atrybutu definiującego środowisko i czy jest identyczna ze środowiskiem określonym w konstruktorze. Jeśli tak jest w istocie, to nazwa sekcji zapisywana jest we właściwości obiektu, a we właściwości $sections tworzona jest nowa tablica dla tej sekcji. Jeśli natomiast oba środowiska się nie zgadzają, to właściwości $currentSection przypisujemy wartość null i ignorujemy wszystkie znaczniki wewnątrz tej sekcji.

150

Rozdział 3. • Praca z formatem XML

Jeśli wykryty zostanie jakikolwiek inny znacznik, to bieżącym danym znakowym (currentData) przypisujemy pusty łańcuch. Po zaimplementowaniu funkcji zwrotnej obsługującej znaczniki otwierające przygotujemy kolejną funkcję zwrotną obsługującą dane: /** * obsługujemy dane znakowe * * @param resource, zasób parsera * @param string, łańcuch danych znakowych */ public function cDataHandler($parser, $cData) { if (trim($cData) === '') { return; } $this->currentData .= $cData; }

Ta akurat funkcja obsługi jest dość prosta: jeśli dane składają się tylko ze spacji, to są ignorowane; w przeciwnym wypadku dodajemy je do właściwości $currentData. Ostatnia funkcja zwrotna, którą musimy zaimplementować, to metoda obsługująca znaczniki zamykające: /** * obsługujemy znaczniki zamykające * * @param resource, zasób parsera * @param string, łańcuch z nazwą znacznika */ public function endHandler($parser, $name) { switch ($name) { case 'configuration': break; // koniec sekcji, znacznik , oczyszczamy bieżącą sekcję case 'section': $this->currentSection = null; break; default: if ($this->currentSection == null) { return; } // zapisujemy bieżące dane w konfiguracji $this->sections[$this->currentSection][$name] = trim( $this->currentData); break; } }

151

PEAR. Programowanie w PHP

Tak jak poprzednio, zamykający znacznik jest ignorowany, ponieważ pełni on tylko funkcję pojemnika, w którym zawarty jest dokument. W momencie natrafienia na zamykający znacznik po prostu repetujemy właściwość $currentSection, ponieważ jesteśmy już poza sekcją. Wszelkie pozostałe znaczniki będą traktowane jak dyrektywy konfiguracyjne, a tekst zawarty wewnątrz tych znaczników (który przechowywać będziemy we właściwości $currentData) będzie wykorzystywany jako wartość tej dyrektywy. Dlatego też zapiszemy tę wartość w tablicy $sections, używając nazwy bieżącej sekcji i nazwy znacznika zamykającego, z wyjątkiem jedynie sytuacji, gdy bieżącej sekcji przypisana będzie wartość null.

Sięganie do opcji konfiguracyjnych Ostatnią rzeczą, którą musimy zrobić, jest dodanie metody umożliwiającej sięganie do danych zebranych podczas analizowania dokumentu XML: /** * Pobieramy opcję konfiguracyjną * * @param łańcuch z nazwą sekcji * @param łańcuch z nazwą opcji * @return opcja mieszanej konfiguracji lub fals, jeśli nie została określona */

public function getConfigurationOption($section, $value) { if (!isset($this->sections[$section])) { return false; } if (!isset($this->sections[$section][$value])) { return false; } return $this->sections[$section][$value]; }

Metodzie tej można przesłać jako parametr nazwę sekcji lub też nazwę opcji konfiguracyjnej. Następnie sprawdza, czy dana sekcja lub opcja została zdefiniowana w dokumencie XML, i zwraca jej wartość. W przeciwnym wypadku zwraca wartość null. Teraz wreszcie nasz parser odczytujący konfigurację jest gotowy do pracy: $config = new ConfigReader('online'); $result = $config->setInputFile('config.xml'); $result = $config->parse(); printf("Folder buforowania : %s\n", $config->getConfigurationOption('paths', 'cache')); printf("Połączenie z bazą danych : %s\n",$config->getConfigurationOption('db', 'dsn'));

152

Rozdział 3. • Praca z formatem XML

Po uruchomieniu skrypt ten zwróci wartości konfiguracyjne przechowywane w pliku XML dla środowiska online: Folder buforowania : /tmp/myapp Połączenie z bazą danych : mysql://user:pass@localhost/myapp

Nasz pierwszy rzeczywiście użyteczny parser kodu XML jest już gotowy. Jak widać, pakiet XML_Parser znacznie ułatwił pisanie kodu. Warto jednak wiedzieć, że to jeszcze nie wszystko, co pakiet ten ma nam do zaoferowania!

Unikanie dziedziczenia W poprzednim przykładzie rozszerzaliśmy klasę XML_Parser. W prostym programie nie jest to wielkim problemem, jeśli jednak pracujemy nad większą infrastrukturą witryny lub aplikacją WWW, to wygodniej byłoby, gdyby wszystkie nasze klasy rozszerzały klasę bazową, dzięki czemu mogłyby oferować pewien zestaw wspólnych funkcji. Niestety nie da się zmienić klasy XML_Parser, tak by rozszerzała naszą klasę bazową, co wygląda na poważną wadę pakietu XML_Parser. Na szczęście, jeśli korzystamy z pakietu XML_Parser w wersji 1.2.0 lub nowszej, rozszerzanie klasy XML_Parser nie jest konieczne. Prezentowany dalej kod implementuje klasę parsera ConfigReader bez wywodzenia klasy XML_Parser. Oprócz instrukcji extends usunęliśmy również właściwość $folding i odwołanie do metody parent::_construct() w konstruktorze. /** * Klasa odczytująca pliki konfiguracyjne XML */

class ConfigReader { /** * wybrane środowisko */

private $environment; /** * sekcje, które zostały już rozpatrzone */

private $sections = array(); /** * tymczasowe przechowywanie danych w trakcie analizy */

private $currentSection = null; private $currentData = null; /** * Tworzymy nowy analizator ConfigReader * * @param łańcuch z wykorzystywanym środowiskiem */

public function __construct($environment = 'online')

153

PEAR. Programowanie w PHP

{ $this->environment = $environment; } // Tu powinny pojawić się funkcje obsługi, // ale pominęliśmy je dla oszczędności papieru

}

Ponieważ nasza klasa nie jest już rozszerzeniem klasy XML_Parser, więc nie dziedziczy też potrzebnych nam funkcji umożliwiających analizę kodu XML. Nadal jednak jest w stanie współpracować z pakietem XML_Parser. Kolejny przykład kodu ilustruje, jak można wykonać analizę tego samego co poprzednio dokumentu XML za pomocą klasy parsera ConfigReader, bez konieczności rozszerzania klasy XML_Parser: $config = new ConfigReader('online'); $parser = new XML_Parser(); $parser->setHandlerObj($config); $parser->folding = false; $parser->setInputFile('XML_Parser-001.xml'); $parser->parse(); printf("Folder buforowania : %s\n", $config->getConfigurationOption('paths', 'cache')); printf("Połaczenie z bazą danych : %s\n", $config->getConfigurationOption('db', 'dsn'));

Zamiast tworzyć jak poprzednio jeden obiekt, tworzymy tym razem dwa obiekty: ConfigReader i instancję klasy XML_Parser. Ponieważ klasa XML_Parser nie oferuje funkcji zwrotnych umożliwiających obsługiwanie danych XML, prześlemy parserowi instancję klasy ConfigReader, której będzie następnie używał do przywoływania funkcji obsługi. Jest to jedyna nowa metoda wykorzystywana w tym przykładzie. Musimy tylko wyłączyć właściwość $folding (przypisując jej wartość false), aby pakiet XML_Parser nie konwertował znaczników na duże litery. Następnie wystarczy przesłać parserowi nazwę pliku i rozpocząć proces analizy. Wynik skryptu będzie dokładnie taki sam jak w poprzednim przykładzie, tym razem jednak napisaliśmy parser bez rozszerzania klasy XML_Parser.

Inne funkcje pakietu XML_Parser Chociaż poznaliśmy już najważniejsze funkcje pakietu XML_Parser, nadal jednak ma on wiele do zaoferowania. Oto krótkie podsumowanie funkcji, których nie będziemy szczegółowo omawiać. „ Pakiet XML_Parser potrafi konwertować dane z jednego kodowania na inne. Oznacza to, że możemy odczytywać dokument zakodowany w formacie UTF-8 i automatycznie podczas analizowania dokumentu przekonwertować dane znakowe na format ISO-8859-1.

154

Rozdział 3. • Praca z formatem XML

„ Pakiet XML_Parser umożliwia programiście obywanie się bez instrukcji switch. Przesyłając konstruktorowi jako drugi argument wartość func, zmieniamy tryb analizy na tak zwany tryb function (funkcji). W tym trybie pakiet XML_Parser nie będzie przywoływał metod startElement() i endElement(). Zamiast nich używać będzie do otwierania znaczników metod xmltag_$nazwaznacznika() i _xmltag_$nazwaznacznika(), gdzie $nazwaznacznika będzie nazwą aktualnie obsługiwanego przez niego znacznika. „ Pakiet XML_Parser oferuje ponadto również klasę XML_Parser_Simple, która implementuje już dla nas metody startElement() i cDataHandler(). Metody te zapisują po prostu dane i przesyłają zebrane informacje metodzie endElement(). W ten sposób możemy za jednym zamachem obsługiwać wszystkie dane związane z konkretnym znacznikiem.

Przetwarzanie kodu XML za pomocą pakietu XML_Unserializer Mimo iż pakiet XML_Parser ułatwia przetwarzanie dokumentów XML, nadal sporo pracy spada na programistę. W większości przypadków jednak zależy nam tylko na pobraniu informacji zawartych w dokumencie XML i przekonwertowaniu go na odpowiednią strukturę danych PHP (taką jak tablica czy kolekcja obiektów). Wówczas właśnie z pomocą przychodzi nam pakiet XML_Unserializer. Pakiet ten jest odwrotnością powiązanego z nim pakietu XML_Serializer. Podczas gdy pakiet XML_Serialzer tworzy kod XML ze struktury danych PHP, pakiet XML_Unserializer tworzy strukturę danych z kodu XML. Jeśli mamy już zainstalowany pakiet XML_Serializer, nie będziemy musieli instalować kolejnego pakietu, ponieważ pakiet XML_Unserializer jest częścią tego samego pakietu PEAR. Z pakietu XML_Unserializer korzysta się w podobny sposób jak z pakietu XML_Serializer, gdyż wykonujemy właściwe te same kroki (z jedną drobną różnicą): „ Załączanie pakietu XML_Unserializer i utworzenie nowej instancji. „ Skonfigurowania instancji za pomocą odpowiednich opcji. „ Odczytanie dokumentu XML. „ Pobranie danych, by zrobić z nimi to, na czym nam zależy. Przyjrzyjmy się bardzo prostemu przykładowi: // załączamy klasę

require_once 'XML/Unserializer.php'; // tworzymy nowy obiekt

$unserializer = new XML_Unserializer(); // przygotowujemy trochę kodu XML

$xml = Array ( [0] => Elvis Presley [1] => Carl Perkins ) )

Jak łatwo zauważyć, pakiet XML_Unserializer przekonwertował dokument XML na zbiór zagnieżdżonych tablic. Główna tablica zawiera tylko jedną wartość przechowywaną pod kluczem artist. Pakiet użył właśnie tego klucza, ponieważ dokument XML na pierwszym poziomie zagnieżdżania zawiera dwa znaczniki . Wartość artist również jest tablicą; tym razem nie jest to jednak tablica asocjacyjna, a tablica indeksowana. Zawiera ona nazwiska dwóch artystów, przechowywane w dokumencie XML. Dlatego prawie wszystkie dane przechowywane w dokumencie będą dostępne w utworzonej tablicy. Jedyną brakującą informacją jest główny znacznik dokumentu: . Użyliśmy tej informacji jako nazwy zmiennej PHP, która przechowuje tablicę; można to jednak zrobić tylko wtedy, gdy wiadomo, jakie informacje będą przechowywane w dokumencie XML. Niemniej, gdybyśmy tego nie wiedzieli, pakiet XML_Unserializer dostarcza metody umożliwiającej sięganie do tych informacji: echo $unserializer->getRootName();

Jak można było oczekiwać, kod ten wyświetli nazwę głównego znacznika właśnie przetworzonego dokumentu XML: artists

Jak widać, zamiast implementować nową klasę, możemy użyć pakietu XML_Unserializer i za jego pomocą pobrać wszystkie informacje z dokumentu XML, jednocześnie zachowując strukturę tych informacji. I to wszystko przy użyciu tylko czterech wierszy kodu! Spróbujmy więc zastosować pakiet XML_Unserializer na pliku konfiguracyjnym XML, który poprzednio analizowaliśmy używając pakietu XML_Parser, by przekonać się, jakie wyniki otrzymamy. Ponieważ dokument XML zachowywany jest w osobnym pliku, warto skorzystać z metody file_get_contents(), by zapisać kod XML w zmiennej. Nie jest to jednak konieczne,

156

Rozdział 3. • Praca z formatem XML

ponieważ pakiet XML_Unserializer może przetwarzać dowolne dane obsługiwane przez pakiet XML_Parser. Aby poinformować pakiet XML_Unserialize, że powinien traktować dane przesłane metodzie unserialize(), jak by były nazwą pliku, a nie dokumentem XML, wystarczy tylko przesłać tej metodzie dodatkowy parametr: require_once 'XML/Unserializer.php'; $unserializer = new XML_Unserializer(); $unserializer->unserialize('config.xml', true); $config = $unserializer->getUnserializedData(); print_r($config);

Po uruchomieniu skrypt ten zwróci następującą tablicę: Array ( [section] => Array ( [0] => Array ( [includes] => /usr/share/php/myapp [cache] => /tmp/myapp [templates] => /var/www/skins/myapp ) [1] => Array ( [dsn] => mysql://user:pass@localhost/myapp [prefix] => myapp_ ) [2] => Array ( [dsn] => mysql://root:@localhost/myapp [prefix] => myapp_testing_ ) ) )

Jeśli przyjrzymy się dokumentowi XML z przykładów wykorzystujących do analizy pakiet XML_Parser, przekonamy się, że pakiet XML_Unserializer wydobył wszystkie informacje, które były przechowywane między znacznikami XML. Nasz przykładowy plik konfiguracyjny definiował kilka sekcji i jak widać, wszystkie dyrektywy konfiguracyjne, które znajdowały się w dokumencie XML, można znaleźć w tablicy, którą otrzymaliśmy. Niemniej brakuje nazw i środowisk dla każdej z sekcji. Informacja ta była przechowywana w atrybutach znaczników , które zostały zignorowane przez pakiet XML_Unserializer.

157

PEAR. Programowanie w PHP

Analizowanie atrybutów Oczywiście można to zmienić. Podobnie jak pakiet XML_Serializer, pakiet XML_Unserializer pozwala programiście wpływać na przebieg przetwarzania za pomocą odpowiednich opcji. Opcje te definiuje się dokładnie w taki sam sposób, jak w pakiecie XML_Serializer: „ Przesyłając tablicę ze zdefiniowanymi opcjami konstruktorowi klasy lub metodzie setOptions(). „ Przesyłając tablicę z opcjami jako parametr metodzie unserialize(). „ Lub definiując pojedynczą opcję za pomocą metody setOption(). Jeśli chcemy analizować również atrybuty, trzeba w kodzie wprowadzić tylko niewielką zmianę: require_once 'XML/Unserializer.php'; $unserializer = new XML_Unserializer(); // analizujemy również atrybuty

$unserializer->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_PARSE, true); $unserializer->unserialize('XML_Parser-001.xml', true); $config = $unserializer->getUnserializedData(); print_r($config);

Dodaliśmy do skryptu jeden wiersz, przypisujący opcji ATTRIBUTES_PARSE pakietu XML_Serializer wartość true (prawda). Oto jak zmiana ta wpłynęła na dane zwracane przez skrypt: Array ( [section] => Array ( [0] => Array ( [name] => paths [includes] => /usr/share/php/myapp [cache] => /tmp/myapp [templates] => /var/www/skins/myapp ) [1] => Array ( [name] => db [environment] => online [dsn] => mysql://user:pass@localhost/myapp [prefix] => myapp_ ) [2] => Array ( [name] => db

158

Rozdział 3. • Praca z formatem XML

[environment] => stage [dsn] => mysql://root:@localhost/myapp [prefix] => myapp_testing_ ) ) )

Teraz utworzona tablica zawiera także dyrektywy konfiguracyjne, jak również metainformacje dla każdej sekcji, które przechowywane są w atrybutach. Niemniej dyrektywy konfiguracyjne i metainformacje zostały przemieszane, co może spowodować problemy podczas korzystania z dyrektyw lub , ponieważ zostaną ona zapisane na wartościach przechowywanych w atrybutach. Na szczęście, tak jak poprzednio, problem ten można rozwiązać wprowadzając do kodu tylko drobną modyfikację: require_once 'XML/Unserializer.php'; $unserializer = new XML_Unserializer(); // analizujemy również atrybuty

$unserializer->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_PARSE, true); // przechowujemy atrybuty w osobnej tablicy

$unserializer->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_ARRAYKEY, '_meta'); $unserializer->unserialize('config.xml', true); $config = $unserializer->getUnserializedData(); print_r($config);

Definiując opcję ATTRIBUTES_ARRAYKEY polecamy pakietowi XML_Unserializer, by zapisywał atrybuty w osobnej tablicy i nie mieszał ich ze znacznikami. Oto wynik działania skryptu po wprowadzeniu poprawek: Array ( [section] => Array ( [0] => Array ( [_meta] => Array ( [name] => paths ) [includes] => /usr/share/php/myapp [cache] => /tmp/myapp [templates] => /var/www/skins/myapp ) [1] => Array (

159

PEAR. Programowanie w PHP

[_meta] => Array ( [name] => db [environment] => online ) [dsn] => mysql://user:pass@localhost/myapp [prefix] => myapp_ ) [2] => Array ( [_meta] => Array ( [name] => db [environment] => stage ) [dsn] => mysql://root:@localhost/myapp [prefix] => myapp_testing_ ) ) )

Teraz możemy bez trudu wydobyć z dokumentu XML wszystkie opcje konfiguracyjne bez konieczności pisania nowego parsera dla każdego nowego formatu XML. Programiści mający obsesję na punkcie programowania obiektowego mogą się jednak skarżyć, że z obiektowego interfejsu oferowanego przez pakiet XML_Parser dla opcji konfiguracyjnych korzystało się znacznie wygodniej niż z prostych tablic PHP. Czytelnicy, którzy tak właśnie myślą, powinni przeczytać następny podrozdział.

Mapowanie kodu XML na obiekty Domyślnie pakiet XML_Unserializer konwertuje złożone struktury XML (tj. każdy znacznik, który zawiera inne zagnieżdżone w nim znaczniki lub atrybuty) na odpowiednią tablicę asocjacyjną. Domyślne to zachowanie można zmienić definiując następującą opcję: $unserializer->setOption(XML_UNSERIALIZER_OPTION_COMPLEXTYPE, 'object');

Jeśli dodamy ten wiersz kodu do skryptu, wynik jego działania zmieni się odrobinę: stdClass Object ( [section] => Array ( [0] => stdClass Object ( [_meta] => Array ( [name] => paths

160

Rozdział 3. • Praca z formatem XML

) [includes] => /usr/share/php/myapp [cache] => /tmp/myapp [templates] => /var/www/skins/myapp ) // ...pozostałe sekcje zostały usunięte...

)

Zamiast tablic asocjacyjnych pakiet XML_Unserializer utworzy instancję standardowej klasy stdClass, która jest zawsze definiowana w języku PHP i nie oferuje żadnych metod. Mimo iż uzyskujemy teraz obiektowy dostęp do dyrektyw konfiguracyjnych, podejście to nie jest jednak wiele lepsze od korzystania z tablic, ponieważ nadal będziemy zmuszeni do pisania kodu w następujący sposób: %echo $config->section[0]->templates;

Cóż, przynajmniej przypomina to sposób korzystania z modułu SimpleXML, który wielu ludzi uważa za znakomite narzędzie do pracy z kodem XML. Na nasze potrzeby ten sposób sięgania do danych nie będzie odpowiedni. Na szczęście to jeszcze nie wszystko, co — jak to pokaże następny przykład — pakiet XML_Unserializer ma nam do zaoferowania. Pakiet XML_Unserializer potrafi również używać różnych klas dla różnych znaczników. Dla każdego znacznika będzie sprawdzać, czy została już zdefiniowana klasa o tej samej nazwie i zamiast standardowej klasy stdClass będzie tworzył instancję tejże klasy odpowiadającej nazwą znacznikowi. Podczas określania właściwości klas sprawdzać będzie, czy dla każdej właściwości została zdefiniowana metoda umożliwiająca przypisywanie wartości tejże właściwości. Metody pozwalające na przypisywanie wartości właściwościom (ang. setters) zawsze mają nazwy zaczynające się od przedrostka „set”, po którym następuje nazwa właściwości. Reasumując, możemy zaimplementować klasy oferujące odpowiednie funkcjonalności, pozwalając pakietowi XML_Unserializer automatycznie przygotować je dla nas i zdefiniować wszystkie właściwości tych klas odpowiednio do danych zawartych w dokumencie XML. W naszym przykładzie z plikiem konfiguracyjnym potrzebować będziemy dwóch klas: jednej dla konfiguracji i jednej dla każdej sekcji konfiguracji. Oto przykład kodu implementującego te klasy: /** * Klasa umożliwiająca dostęp do konfiguracji */

class configuration { /** * Będzie przechowywać sekcję */

private $sections = null; /** * wybrane środowisko */

private $environment = 'online'; /**

161

PEAR. Programowanie w PHP

* Metoda definiująca znacznik sekcji */

public function setSection($section) { $this->sections = $section; } /** * Określamy środowisko konfiguracji * * Będzie przywoływana nie przez XML_Unserializer, * ale przez użytkownika. */

public function setEnvironment($environment) { $this->environment = $environment; } /** * Pobieramy opcję konfiguracyjną * * @param string łańcuch z nazwą sekcji * @param string łańcuch z nazwą opcji * @return mieszana opcja konfiguracji lub false, jeśli nie została określona */

public function getConfigurationOption($section, $value) { foreach ($this->sections as $currentSection) { if ($currentSection->getName() !== $section) { continue; } if (!$currentSection->isEnvironment($this->environment)) { continue; } return $currentSection->getValue($value); } return null; } }

Kod implementujący klasę configuration jest dość prosty: mamy właściwość przechowującą wszystkie sekcje konfiguracji oraz właściwość, która przechowuje aktualnie wybrane środowisko. Ponadto mamy odpowiadające im nazwami metody umożliwiające przypisywanie właściwościom wartości oraz jedną metodę pozwalającą na pobieranie wartości konfiguracji. Jedynym, co może zaskoczyć czytelnika w tej implementacji klasy configuration, jest to, że metoda umożliwiająca określanie sekcji nosi nazwę setSection(), a nie setSections(). Dzieje się tak dlatego, że znacznik miał nazwę (słowo „sekcje” w nazwie znacznika występuje w liczbie pojedynczej, a nie mnogiej). Kolejnym krokiem jest zaimplementowanie klasy section:

162

Rozdział 3. • Praca z formatem XML

/** * Klasa przechowująca informacje na temat pojedynczej sekcji */

class section { /** * przechowuje metainformacje */

private $meta = null; /** * definiuje metainformacje */

public function setMeta($meta) { if (!isset($meta['name'])) { throw new Exception('Sections require a name.'); } $this->meta = $meta; } /** * Pobieramy nazwę sekcji */

public function getName() { return $this->meta['name']; } /** * poszukujemy określonego środowiska */

public function isEnvironment($environment) { if (!isset($this->meta['environment'])) { return true; } return ($environment === $this->meta['environment']); } /** * Pobieramy wartość z sekcji */

public function getValue($name) { if (isset($this->$name)) { return $this->$name; } return null; } }

163

PEAR. Programowanie w PHP

Podobnie jak poprzednia klasa, również ta jest głównie pojemnikiem dla informacji przechowywanych w danej sekcji oraz oczywiście posiada odpowiednie metody do definiowania i pobierania wartości właściwości (ang. setter i getters). Teraz gdy już obie klasy zostały zaimplementowane, możemy pokazać pakietowi XML_Unserializer, jak ma z nich korzystać: require_once 'XML/Unserializer.php'; $unserializer = new XML_Unserializer(); // analizuje również atrybuty

$unserializer->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_PARSE, true); // przechowuje atrybuty w osobnej tablicy

$unserializer->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_ARRAYKEY, 'meta'); // używamy obiektów zamiast tablic

$unserializer->setOption(XML_UNSERIALIZER_OPTION_COMPLEXTYPE, 'object'); $unserializer->setOption(XML_UNSERIALIZER_OPTION_TAG_AS_CLASSNAME, true); $unserializer->unserialize('config.xml', true); $config = $unserializer->getUnserializedData(); printf("Folder buforowania : %s\n", $config->getConfigurationOption( 'paths', 'cache')); printf("Połączenie z bazą danych : %s\n", $config->getConfigurationOption('db', 'dsn')); $config->setEnvironment('stage'); print "\nZmieniono środowisko:\n"; printf("Folder buforowania : %s\n", $config->getConfigurationOption( 'paths', 'cache')); printf("Połączenie z bazą danych : %s\n", $config->getConfigurationOption('db', 'dsn'));

Tak jak poprzednio, zmiana pojedynczej opcji pozwala zupełnie zmienić działanie pakietu XML_Unserializer. Po uruchomieniu tego skryptu otrzymamy następujące dane: Folder buforowania : /tmp/myapp Połączenie z bazą danych : mysql://user:pass@localhost/myapp Zmieniono środowisko: Folder buforowania : /tmp/myapp Połączenie z bazą danych : mysql://root:@localhost/myapp

Jest tylko jeden problem, który może zakłócić działanie naszego nowego programu analizującego konfigurację. Jeśli plik konfiguracyjny zawierałby tylko jedną sekcję, to metoda configuration::setSection() przywoływana byłaby przesyłając jej pojedynczą instancję obiektu sec-

164

Rozdział 3. • Praca z formatem XML

tion, a nie tablicę zawierającą kilka obiektów section. To oczywiście wywołałoby błąd w momencie, gdy program próbowałby przeglądać w pętli nieistniejącą tablicę. Aby temu zapobiec, trzeba albo automatycznie tworzyć w tym przypadku tablicę podczas implementowania metody setSection(), albo zdać się na pakiet XML_Unserializer: $unserializer->setOption(XML_UNSERIALIZER_OPTION_FORCE_ENUM, array('section'));

Po tej poprawce pakiet XML_Unserializer będzie tworzył indeksowaną tablicę nawet wtedy, gdy znacznik pojawi się tylko raz. Ponieważ wiemy już, w jaki sposób definiuje się opcje dla pakietu XML_Unserializer, warto rzucić okiem na kolejną tabelę zestawiającą wszystkie opcje dostępne w pakiecie XML_Unserializer. Nazwa opcji

Opis

Wartość domyślna

COMPLEXTYPE

Definiuje, w jaki sposób należy dokonywać deserializacji znaczników, które nie zawierają żadnych danych znakowych. Może przyjmować wartości array (tablica) lub object (obiekt).

array

ATTRIBUTE_KEY

Definiuje nazwę atrybutu, który użyty zostanie jako nazwa klucza tablicy.

_originalKey

ATTRIBUTE_TYPE

Definiuje nazwę atrybutu, który będzie podstawą dla serializowanych informacji o typie.

_type

ATTRIBUTE_CLASS

Definiuje nazwę atrybutu, z którego pobrana zostanie _class nazwa klasy, jeśli znacznik serializowany będzie jako obiekt.

TAG_AS_CLASSNAME

Określa, czy należy jako nazwy klasy użyć nazwy znacznika.

flase

DEFAULT_CLASS

Nazwa domyślnej klasy wykorzystywanej podczas tworzenia obiektów.

stdClass

ATTRIBUTES_PARSE

Określa, czy należy analizować atrybuty (true), czy też je ignorować (false).

false

ATTRIBUTES_PREPEND

Określa łańcuch, który należy dodawać na początku nazw atrybutów.

pusty łańcuch

ATTRIBUTES_ARRAYKEY

Określa nazwę klucza lub właściwości, pod którymi w osobnej tablicy będą przechowywane wszystkie atrybuty. Wartość false wyłącza tę opcję.

false

CONTENT_KEY

Określa nazwę klucza lub właściwości, pod którymi przechowywane będą dane znakowe ze znacznika, jeśli zawiera on nie tylko dane znakowe.

_content

TAG_MAP

Definiuje tablicę asocjacyjną zawierającą informacje, które nazwy znaczników należy zmienić na inne (podane w tablicy) nazwy.

pusta tablica

165

PEAR. Programowanie w PHP

Nazwa opcji

Opis

Wartość domyślna

FORCE_ENUM

Definiuje tablicę z nazwami znaczników, które powinny być automatycznie traktowane tak, jakby w dokumencie znacznik pojawiał się więcej niż jeden raz. Dzięki temu będą dla nich automatycznie przygotowywane tablice indeksowane.

pusta tablica

ENCODING_SOURCE

Określa kodowanie źródłowego dokumentu. Informacja ta zostanie przesłana pakietowi XML_Parser.

null

ENCODING_TARGET

Określa kodowanie docelowego dokumentu. Informacja ta zostanie przesłana pakietowi XML_Parser.

null

DECODE_FUNC

Określa funkcję zwrotną PHP, która zostanie zastosowana na danych znakowych i wartościach atrybutów.

null

RETURN_RESULT

Określa, czy metoda unserialize() powinna zwracać wyniki, czy tylko wartość true (prawda) po udanej deserializacji.

false

WHITESPACE

Określa, w jaki sposób powinny być traktowane spacje w dokumencie. Możliwe wartości opcji to: XML_..._WHITESPACE_KEEP (zachowaj spacje), XML_..._WHITESPACE_TRIM (przytnij spacje) lub XML_..._WHITESPACE_NORMALIZE (znormalizuj spacje).

XML_..._WHITESPACE ¦_TRIM

IGNORE_KEYS

Lista znaczników, których zawartość będzie pusta tablica automatycznie przesyłana do znacznika nadrzędnego, zamiast tworzyć nowy znacznik.

GUESS_TYPES

Określa, czy włączać automatyczne zgadywanie typów dla danych znakowych i atrybutów.

false

Deserializacja etykiet W przykładach ilustrujących działanie pakietu XML_Serializer tworzyliśmy dokument XML oparty na strukturze zbudowanej z obiektów. W naszym ostatnim przykładzie korzystania z pakietu XML_Unserializer zatoczymy koło i utworzymy tę samą strukturę danych z dokumentu XML. Oto kod, który nam do tego posłuży: require_once 'XML/Unserializer.php'; $unserializer = new XML_Unserializer(); // Nie ignorujemy atrybutów

$unserializer->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_PARSE, true);

166

Rozdział 3. • Praca z formatem XML

// Niektóre złożone znaczniki powinny być obiektami, ale wyliczenia // powinny być przechowywane w tablicach

$types = array( '#default' => 'object', 'artists' => 'array', 'labels' => 'array', 'records' => 'array' ); $unserializer->setOption(XML_UNSERIALIZER_OPTION_COMPLEXTYPE, $types); // Zawsze tworzymy indeksowane tablice dla etykiet studiów, artystów i albumów

$unserializer->setOption(XML_UNSERIALIZER_OPTION_FORCE_ENUM, array('label', 'artist', 'record')); // nie dodajemy zagnieżdżonych kluczy dla etykiet studiów, artystów i albumów

$unserializer->setOption(XML_UNSERIALIZER_OPTION_IGNORE_KEYS, array('label', 'artist', 'record')); // analizujemy plik

$unserializer->unserialize('first-xml-document.xml', true); print_r($unserializer->getUnserializedData());

Gdy uruchomimy ten skrypt, na ekranie pojawi się kilka komunikatów ostrzegawczych podobnych do tego: Warning: Missing argument 1 for Record::__construct() in c:\wamp\www\ books\packt\pear\xml\example-classes.php on line 48

Dzieje się tak dlatego, że w klasach Label, Artist i Record zaimplementowaliśmy konstruktory, które wymagać będą, aby w momencie tworzenia nowych instancji przesłać im pewne parametry. Pakiet XML_Unserializer nie będzie przesyłać tych parametrów odpowiedniemu konstruktorowi, dlatego też musimy wprowadzić pewne poprawki w kodach naszych klas: class Label { ... public function __construct($name = null) { $this->name = $name; } ... } class Artist { ... public function __construct($name = null) { $this->name = $name; } ... } class Record { ... public function __construct($id = null, $name = null, $released = null) {

167

PEAR. Programowanie w PHP

$this->id = $id; $this->name = $name; $this->released = $released; } }

Dzięki uczynieniu argumentów konstruktorów opcjonalnymi pozbywamy się ostrzeżeń. Mimo to pakiet XML_Unserializer będzie definiował wszystkie właściwości obiektów po utworzeniu ich instancji. Dzięki temu, jeśli teraz uruchomimy skrypt, otrzymamy wynik, jakiego oczekiwaliśmy — kompletne drzewo obiektów zostanie odtworzone i jak widać, nie ma potrzeby pisania w tym przypadku specjalnego parsera.

Inne dodatkowe funkcje Przekonaliśmy się, że za pomocą pakietu XML_Unserializer można tworzyć naprawdę wspaniałe skrypty, i to pisząc zaledwie kilka wierszy kodu. Niemniej nadal nie poznaliśmy jeszcze wszystkich możliwości pakietu XML_Unserializer. Pakiet ten pozwala nam również: „ Mapować nazwy znaczników na nazwy klas, określając odpowiednią tablicę asocjacyjną. „ Skorzystać z opcji zgadywania typów, dzięki której pakiet będzie automatycznie konwertował dane na odpowiednie wartości logiczne, liczby całkowite lub liczby zmiennoprzecinkowe. „ Wykorzystać pakiety XML_Serializer i XML_Unserializer jako poręczne narzędzia zastępujące standardowe funkcje serialize() i unserialize(). „ Stosować na wszystkich danych znakowych i wartościach atrybutów odpowiednie funkcje zwrotne języka PHP. „ Usuwać lub, przeciwnie, zachowywać wszystkie spacje w dokumencie.

Pakiet XML_Parser a pakiet XML_Unserializer Jeśli zachodzi konieczność pobrania informacji z dokumentu XML, zanim przystąpimy do pisania własnego parsera, warto sprawdzić, czy pakiet XML_Unserializer poradzi sobie z tym zadaniem. W ponad 90% sytuacji pakiet XML_Unserializer okazuje się najlepszym narzędziem. Nawet jeśli pierwsza próba zakończy się niepowodzeniem, w większości przypadków wystarczy tylko użyć odpowiednich opcji. Z pakietu XML_Parser natomiast należy korzystać w następujących sytuacjach: „ Jeśli nasz dokument XML jest bardzo złożony i nie stosuje się do żadnych reguł. Pakiet XML_Unserializer może w takiej sytuacji nie być w stanie wybrać odpowiednich informacji, natomiast pakiet XML_Parser poradzi sobie z takim nietypowym dokumentem, choć oczywiście będzie to wymagać więcej pracy.

168

Rozdział 3. • Praca z formatem XML

„ Jeśli musimy tylko pobrać fragment dokumentu XML, to pakiet XML_Parser może z tym zadaniem poradzić sobie szybciej niż pakiet XML_Unserializer, ponieważ będziemy mogli zignorować resztę dokumentu XML. „ W przypadku przetwarzania dużych dokumentów XML pakiet XML_Parser może być bardziej efektywny, ponieważ wymaga mniejszych zasobów pamięci niż pakiet XML_Unserializer. Dzieje się tak dlatego, że pakiet XML_Unserializer przechowuje wszystkie dane pobrane z dokumentu w pamięci. Natomiast pakiet XML_Parser przechowuje informacje wydobyte z dokumentu XML podczas jego przetwarzania w bazie danych, dzięki czemu podczas analizy nie obciąża aż tak bardzo pamięci.

Analizowanie danych RSS za pomocą pakietu XML_RSS Skrót RSS można rozwinąć jako: „ Rich Site Summary. „ RDF Site Summary. „ Really Simple Syndication. Wszystkie te narzędzia odnoszą się do pobierania danych z innych witryn WWW. Nas interesuje trzecie z narzędzi, Really Simple Syndication, które jak sama nazwa sugeruje, służy do łatwej syndykacji, czyli oferowania w naszej witrynie zawartości oferowanej przez inne witryny WWW. Technologia RSS jest zazwyczaj wykorzystywana przez blogi (web logi) lub witryny zbierające (inaczej mówiąc: agregujące) wiadomości z innych witryn. Ponieważ RSS jest aplikacją XML, do obsługi treści RSS możemy skorzystać również z już omówionych pakietów. Niemniej repozytorium PEAR oferuje specjalny pakiet, który służy właśnie do pobierania informacji z dokumentów RSS i za którego pomocą praca z zawartością RSS staje się dziecinnie prosta. Używając pakietu XML_RSS można wyświetlać w swojej witrynie nagłówki wpisów lub artykułów z ulubionych blogów, z wykorzystaniem mniej niż dziesięciu wierszy kodu. Można również przygotować w naszej witrynie listę najnowszych wersji naszych ulubionych pakietów, pakietów napisanych przez ulubionych programistów lub odpowiednich kategorii PEAR. Witryna PEAR oferuje różnego rodzaju materiały (ang. feed; tak powszechnie nazywa się adresy URL oferujące różne dokumenty RSS), które zawierają albo informacje o wszystkich publikowanych pakietach, albo tylko o tych ostatnio publikowanych pakietach, ostatnich publikacjach w danej kategorii lub określonego programisty. Listę wszystkich dostępnych materiałów (feedów) w witrynie PEAR i odpowiadających im adresów URL można znaleźć pod adresem: http://pear.php.net/feeds/. W kolejnych przykładach będziemy pracować z feedami dostarczającymi informacji o najnowszych publikacjach w danej kategorii XML. Ten feed można znaleźć pod adresem: http://pear.php.net/feeds/cat_xml.rss. Jeśli otworzymy ten adres URL w naszej przeglądarce lub też ściągniemy feed, zobaczymy dokument XML o następującej strukturze:

169

PEAR. Programowanie w PHP



http://pear.php.net/ [email protected] [email protected] en-us





PEAR: Latest releases in category xml The latest releases in the category xml

XML_Serializer 0.16.0 http://pear.php.net/package/XML_Serializer/ download/0.16.0/

XML_Serializer: - introduced constants for all options (this helps avoiding typos in the option names) - deprecated option 'tagName' is no longer supported, use XML_SERIALIZER_OPTION_ROOT_NAME (or rootName) instead - implement Request #3762: added new ignoreNull option to ignore properties that are set to null when serializing objects or arrays - fixed bug with encoding function - use new header comment blocks XML_Unserializer: - fix bug #4075 (allow tagMap option to influence any kind of value) 2005-06-05T09:26:53-05:00

XML_SVG 1.0.0 http://pear.php.net/package/XML_SVG/download/1.0.0/ PHP5 compatible copy() method. 2005-04-13T19:33:56-05:00

XML_FastCreate 1.0.0 http://pear.php.net/package/XML_FastCreate/download/1.0.0/

170

Rozdział 3. • Praca z formatem XML

BugFix PHP5 ; scripts/example added ; stable release. 2005-03-31T10:41:23-05:00



Dokument ten zawiera informacje o dwóch sprawach. Po pierwsze, globalne informacje o kanale (ang. channel), który oferuje dany feed. Po drugie — sam feed. Informacje te zawierają tytuł i opis feedu, adres URL witryny, która dostarcza feedu, informacje o języku, w którym został zapisany, oraz o tym, kto opublikował i utworzył ten materiał. Następnie feed zawiera kilka elementów, które opisują pozycje wiadomości zawartych w materiale. W tym przypadku są to wiadomości o nowym, opublikowanym oprogramowaniu. Każda z tych pozycji jest ujęta w znacznik i przechowuje następujące informacje: „ Tytuł. „ Opis. „ Adres URL strony, która zawiera dalsze informacje na temat tej pozycji. „ Datę opublikowania tych informacji. Sięganie do tych informacji z użyciem pakietu XML_RSSS będzie bardzo proste. Wystarczy wykonać następujące czynności: 1. Załączyć pakiet XML_RSS do naszego kodu i utworzyć nową instancję (nowy obiekt) klasy XML_RSS. 2. Przetworzyć feed (materiał) RSS. 3. Pobrać informacje z obiektu XML_RSS. Oto prosty skrypt, który pobiera informacje z kanału i wyświetla je w formacie HTML. require_once 'XML/RSS.php'; $rss = new XML_RSS('http://pear.php.net/feeds/cat_xml.rss'); $rss->parse(); $channel = $rss->getChannelInfo(); print "Dane kanału
\n"; printf("Tytuł: %s
\n", $channel['title']); printf("Opis: %s
\n", $channel['description']); printf("Łącze: %s
\n", $channel['link'], $channel['link']);

Po otwarciu tego skryptu w przeglądarce zobaczymy następujące informacje: Dane kanału Tytuł: PEAR: Latest releases in category xml

171

PEAR. Programowanie w PHP

Opis: The latest releases in the category xml Łącze: http://pear.php.net/

Aby utworzyć listę z informacjami o najnowszych publikacjach pakietów związanych z językiem XML w repozytorium PEAR, wystarczy zmodyfikować ten skrypt tylko odrobinę: require_once 'XML/RSS.php'; $rss = new XML_RSS('http://pear.php.net/feeds/cat_xml.rss'); $rss->parse(); $channel = $rss->getChannelInfo(); print 'Dane z kanału
'; printf('Tytuł: %s
', $channel['title']); printf('Opis: %s
', $channel['description']); printf('Łącze: %s
', $channel['link'], $channel['link']); print '
    '; $items = $rss->getItems(); foreach ($items as $item) { $date = strtotime($item['dc:date']); printf('
  • %s (%s)
  • ', $item['link'], $item['title'], date('Y-m-d', $date)); } print '
';

Kod ten wyświetli nieuporządkowaną listę najnowszych pakietów zaraz pod ogólnymi informacjami o kanale. Naprawdę jednak wspaniałe w tym wszystkim jest to, że za pomocą prawie identycznego skryptu można wyświetlić informacje na temat najnowszych publikacji dowolnego programisty PEAR — wystarczy po prostu zastąpić adres URL obecnego feedu adresem na przykład http://pear.php.net/feeds/user_schst.rss. Tego samego skryptu można również użyć, by wyświetlić materiały (feed) z dowolnej innej witryny lub bloga. Aby na przykład wyświetlić najnowsze wiadomości z bloga blog.php-tools.net, wystarczy użyć adresu URL http://blog.php-tools.net/feeds/index.rss2 i zobaczymy w przeglądarce najnowsze wiadomości z bloga PAT. Niemniej konieczne będzie wprowadzenie w skrypcie jednej drobnej poprawki, ponieważ wersja 2 RSS używa znacznika zamiast . Aby móc odczytywać i wyświetlać materiały przygotowane w obu wersjach RSS, konieczne będzie wprowadzenie do skryptu drobnej poprawki: $items = $rss->getItems(); foreach ($items as $item) { if (isset($item['dc:date'])) { $date = strtotime($item['dc:date']); } elseif ($item['pubDate']) { $date = strtotime($item['pubDate']); }

172

Rozdział 3. • Praca z formatem XML

printf('
  • %s (%s)
  • ', $item['link'], $item['title'], date('Y-m-d', $date)); }

    Mimo iż feedy repozytorium PEAR nie obsługują tej funkcji, mamy jednak możliwość przechowywania informacji o obrazach, które powinny być wyświetlane razem z danym feedem. Pakiet XML_RSS oferuje specjalną metodę, umożliwiającą wydobywanie tych informacji: $images = $rss->getImages(); foreach ($images as $image) { $size = getimagesize($image['url']); printf('
    ', $image['url'], $size[0], $size[1], $image['title']); }

    Po dodaniu do skryptu tego fragmentu kodu w oknie przeglądarki pod listą wiadomości powinien pojawić się odpowiedni obrazek. Jak się przekonaliśmy, integrowanie wiadomości RSS z naszą witryną okazuje się bardzo proste, jeśli tylko korzystamy z pakietu XML_RSS z repozytorium PEAR.

    Podsumowanie W tym rozdziale dowiedzieliśmy się, jak korzystać z pakietów PEAR ułatwiających pracę z kodem XML. Pakiety XML_Util, XML_FastCreate i XML_Serializer służą do tworzenia dokumentów XML abstrahując od poprawności sformułowania kodu XML i justowania znaczników za pomocą odpowiednich wcięć. Pakiet XML_XUL umożliwia tworzenie za pomocą języka PHP interfejsów aplikacji internetowych działających pod przeglądarkami z rodziny Mozilla, takimi jak Firefox. Dzięki temu można przy zachowaniu logiki biznesowej standardowej aplikacji WWW korzystać z przyjaznego interfejsu użytkownika opartego na języku XUL. W drugiej części rozdziału nauczyliśmy się, jak pisać parser oparty na interfejsie SAX, który umożliwi odczytywanie pliku konfiguracyjnego w formacie XML i automatycznie ignoruje nieistotne dla nas części dokumentu XML. Dowiedzieliśmy się też, jak za pomocą pakietu XML_Unserializer przekształcać praktycznie dowolny dokument XML na tablice lub obiekty. Dzięki temu możemy sięgać do informacji zawartych w dokumencie XML bez konieczności posiadania jakiejkolwiek wiedzy na temat przebiegu samego procesu analizy dokumentu. Wreszcie na koniec wykorzystaliśmy pakiet XML_RSS, by za jego pomocą wyświetlić informacje RSS w dowolnej aplikacji opartej na języku PHP.

    173

    PEAR. Programowanie w PHP

    174

    4 Usługi WWW Aplikacje WWW odgrywają coraz większą rolę we współczesnych infrastrukturach informatycznych. Dawniej najważniejszą dziedziną programowania było tworzenie aplikacji działających na poszczególnych komputerach, obecnie natomiast coraz więcej firm przenosi swe aplikacje do sieci WWW, aby można je było kontrolować z dowolnego komputera podłączonego do sieci za pomocą większości współczesnych przeglądarek. W ten sposób pracownicy nie są „przywiązani” do swego biurka w firmie, ale mogą korzystać z firmowych aplikacji praktycznie z dowolnego miejsca na świecie. Nadal jednak aplikacje te muszą posiadać zdolność do komunikowania się z innymi aplikacjami, ponieważ żadna firma nie może sobie pozwolić na zaprojektowanie i zaimplementowanie od podstaw wszystkich komponentów oprogramowania, które mogą być przydatne w firmie. Dlatego też bardzo często nowe, firmowe aplikacje WWW, zwykle pisane w języku PHP, muszą funkcjonować w heterogenicznym (niejednorodnym) środowisku i umieć komunikować się z różnymi aplikacjami napisanymi w innych językach programowania, takich jak C/C++, Perl, Java lub nawet w języku COBOL. W przeszłości programiści, by umożliwić komunikację między aplikacjami, zazwyczaj korzystali ze standardu CORBA lub modelu COM. Obecnie jednak wraz z triumfem internetu pojawiło się również wiele nowoczesnych usług WWW. Korzystają one zazwyczaj z solidnych i powszechnie uznawanych protokołów, takich jak HTTP, otwartych standardów, takich jak język XML, i dostępnych dla wszystkich aplikacji, takich jak serwery WWW. Wszystko zaczęło się od bardzo prostego, bazującego na języku XML, protokołu XML-RPC. Nazwa XML-RPC jest skrótem od XML Remote Procedure Cal (zdalne wywoływanie procedur za pomocą XML) i był to pierwszy popularny protokół dla usług WWW. Nadal zresztą jest wykorzystywany przez wiele firm i aplikacji. Z protokołu XML-RPC wyewoluował protokół SOAP, który choć zawdzięcza wiele XML-RPC, jest jednak bardziej wszechstronny, a zatem bardziej skomplikowany. Protokół SOAP jest obecnie obsługiwany przez prawie wszystkie języki programowania, w tym oczywiście przez język PHP.

    PEAR. Programowanie w PHP

    Ponieważ protokoły te są często zbyt skomplikowane lub też zbyt statyczne dla potrzeb niektórych firm, tworzą one często swoje własne, prywatne protokoły, zazwyczaj oparte na standardzie XML. Protokoły te nierzadko są do siebie bardzo zbliżone i dlatego określa się je zbiorczym terminem usług REST (opartych na protokole REST, Representational State Transfer), czyli usług, które co prawda nie korzystają z żadnego z oficjalnych protokołów zaprojektowanych do komunikacji między różnymi aplikacjami, ale nadal oparte są na protokole HTTP i języku XML. W tym rozdziale opowiemy o dostępnych w repozytorium PEAR pakietach ułatwiających pracę z różnymi usługami WWW.

    Korzystanie z usług WWW Większość ludzi rozpoczyna od korzystania z usług WWW oferowanych przez kogoś innego. Są dwa podstawowe powody przemawiające za sięganiem po cudzą usługę WWW: 1. Musimy opierać się na danych na temat klienta, których nie da się zdobyć wysyłając po prostu zapytanie do bazy danych. Przyczyną mogą być względy bezpieczeństwa lub konieczność korzystania ze źródła danych nieobsługiwanego przez język PHP. Bardzo często powodem może być również chęć skorzystania z logiki biznesowej, którą już ktoś w naszej firmie zaimplementował, na przykład za pomocą języka Java. 2. Chcemy skorzystać z usługi oferowanej przez inną firmę. Na przykład jeśli chcemy zintegrować z naszą witryną usługę przeszukiwania internetu, po co pisać taką usługę samodzielnie, skoro możemy zapłacić za znakomitą usługę przeszukiwania oferowaną przez Google? Zapewne wyjdzie to taniej niż ręczne implementowanie wszystkich funkcji dostarczanych przez wyszukiwarkę Google. To samo dotyczy sytuacji, gdy na przykład chcemy przygotować aukcję internetową, sprzedawać przez internet książki itp. Istnieją już w sieci firmy, które oferują najlepsze i dopracowane rozwiązania dla wielu różnych aplikacji WWW. Używając ich usług możemy skorzystać z zalet zaimplementowanych przez nie mechanizmów, jednocześnie nie tracąc nic z tożsamości naszej firmy. W pierwszej części tego rozdziału skorzystamy z usług WWW, które oparte są na standardowych protokołach, takich jak XML-RPC czy SOAP, posługując się w tym celu oczywiście odpowiednimi pakietami PEAR. Potem przyjrzymy się usługom obsługiwanym przez pakiet Google_Package, który ułatwia znacznie pracę usługom internetowym Google. Pamiętajmy, że Google należy do firm oferujących usługi oparte na protokole SOAP. Gdy już zapoznamy się z technikami pracy ze standardowymi protokołami, przyjrzymy się pakietowi Services_Ebay zapewniającemu łatwy i wygodny w użyciu interfejs API dla usług WWW oferowanych przez dom aukcyjny eBay. Pakiet ten jest unikatową mieszanką typowych usług REST i protokołu SOAP. Na koniec wreszcie zapoznamy się z dwoma pakietami PEAR nie należącymi do kategorii usług WWW w repozytorium PEAR. Ułatwiają one natomiast korzystanie z usług WWW

    176

    Rozdział 4. • Usługi WWW

    opartych na protokole REST. Za pomocą tych dwóch pakietów będziemy mogli wykorzystać w swojej witrynie praktycznie dowolne usługi WWW oparte na protokole REST, nawet jeśli implementacja właściwego dla nich pośrednika proxy nie jest dostępna w repozytorium PEAR.

    Korzystanie z usług WWW opartych na XML-RPC XML-RPC to skrót od XML Remote Procedure Call (zdalne wywoływanie procedur za pomocą XML), będący nazwą standardu zaprojektowanego przez firmę Userland Software. Jest to protokół przywołujący funkcje działające na różnych serwerach, korzystający z protokołu HTTP, by zakodować odpowiednie zdalne wywołanie funkcji, a następnie zwracający wyniki w formacie XML. Aby korzystać z usługi XML-RPC, konieczne jest przygotowanie kodu XML zawierającego nazwę metody oraz wszystkie argumenty funkcji i wysłanie go do serwera usługi za pośrednictwem żądania Post protokołu HTTP. Serwer przeanalizuje nadchodzący kod XML, przywoła odpowiednią metodę i utworzy dokument XML zawierający wynik jej działania. Po czym odeśle go do programu wysyłającego żądanie. Nasz skrypt będzie musiał następnie przetworzyć otrzymany od serwera kod XML i wydobyć z niego odpowiednie informacje zwrócone przez wywoływaną za pomocą XML-RPC funkcję. Firma Userland Software oferuje bardzo prostą usługę testową, z której skorzystamy w kolejnych przykładach. Usługa ta zwraca nazwę stanu w USA w zależności od liczby całkowitej, którą prześlemy usłudze. Metoda oferowana przez tę usługę to examples.getStateName() i aby ją przywołać, trzeba przygotować następujący dokument XML:

    examples.getStateName

    15



    Jeśli serwer usługi otrzyma taki dokument XML, to odkoduje go i przywoła metodę examples.getStateName(), przesyłając jej jako argument wartość 15. Po wywołaniu metody usługa XML-RPC utworzy nowy dokument zawierający informacje o wartości zwróconej przez metodę i odeśle go z powrotem do klienta, który wysłał żądanie:

    Iowa



    177

    PEAR. Programowanie w PHP

    Jak widać, liczbie 15 odpowiada stan Iowa. To już w zasadzie cała wiedza niezbędna do pracy z protokołem XML-RPC. Posiadając wiadomości na temat tworzenia i przetwarzania dokumentów XML, prezentowane w rozdziale 3., czytelnik byłby już prawdopodobnie w stanie napisać samodzielnie klienta XML-RPC. Nie ma jednak takiej potrzeby, ponieważ repozytorium PEAR oferuje łatwą i wygodną w użyciu implementację protokołu XML-RPC. Co więcej, jest ona już prawdopodobnie zainstalowana na komputerze, ponieważ PEAR począwszy od wersji 1.4.0 wykorzystuje protokół XML-RPC w komunikacji między instalatorem PEAR na naszym komputerze a repozytorium PEAR w internecie. Dlatego wystarczy zapewne po prostu załączyć ten pakiet do kodu i będziemy mogli korzystać z niego w naszych aplikacjach. Skrypt sięgający do przykładowej usługi oferowanej przez firmę Userland można napisać za pomocą oferowanego przez repozytorium PEAR pakietu XML_RPC, używając mniej niż dziesięciu wierszy kodu (jeśli nie liczyć dokumentujących komentarzy i obsługi błędów): require_once 'XML/RPC.php'; // tworzymy nowego klienta

    $client = new XML_RPC_Client('/RPC2', 'betty.userland.com'); // kodujemy parametry komunikatu

    $params = array( new XML_RPC_Value(15, 'int') ); // kodujemy wywołanie metody w XML

    $message = new XML_RPC_Message('examples.getStateName', $params); // wysyłamy komunikat XML-RPC

    $response = $client->send($message); // Sprawdzamy, czy nie pojawił się błąd

    if ($response->faultCode()) { echo "Nie mogę skorzystać z usługi XML-RPC.\n"; echo $response->faultString(); exit(); } // pobieramy wartość zwracaną

    $value = $response->value(); // dekodujemy obiekt XML_RPC_Value na prostą zmienną PHP

    $stateName = XML_RPC_decode($value); echo "Stan to $stateName\n";

    Tak jak w przypadku każdego skryptu sięgającego do zasobów PEAR, zaczynamy pracę od załączenia pakietu, z którego chcemy skorzystać. Następnie tworzymy nowego klienta usługi, do której zamierzamy sięgać. Konstruktor klasy XML_RPC_Client będzie wymagał przesłania mu przynajmniej dwóch parametrów:

    178

    Rozdział 4. • Usługi WWW

    „ Ścieżki do usługi na serwerze (w tym przypadku zlokalizowanej w katalogu /RPC2). „ Nazwy hosta usługi (w tym przypadku betty.userland.com). Konstruktorowi można przesłać również kolejne parametry, jeśli usługa nie jest zlokalizowana pod portem 80 lub też jeśli zamierzamy sięgać do usługi poprzez odpowiedniego pośrednika proxy. Parametry, których należy użyć, by korzystać za pomocą pakietu XML_RPC z pośrednika proxy, można znaleźć w jego dokumentacji, dostępnej pod adresem: http://pear.php.net/manual/ en/package.webservices.xml-rpc.php. Potem będziemy musieli przygotować kod XML definiujący wywołanie metody, które wyślemy do serwera. W tym celu trzeba wykonać następujące czynności: 1. Najpierw utworzyć indeksowaną tablice zawierającą argumenty funkcji zdefiniowane w postaci obiektów XML_RPC. W naszym prostym przykładzie potrzebujemy tylko jednego argumentu, mianowicie liczby całkowitej, dla której chcemy pobrać nazwę stanu. Konstruktor klasy XML_RPC_Value pozwala na przesłanie mu dwóch parametrów: wartości, którą ma zakodować, i typu tejże wartości. Jeśli nie podamy typu, to domyślnie użyje typu string (łańcucha znaków). 2. Nowo utworzona tablica zostanie następnie wykorzystana, by zakodować samo odwołanie do metody. W tym celu zostanie utworzona nowa instancja obiektu komunikatu XML_RPC_Message. Konstruktor tej klasy wymaga przesłania mu dwóch parametrów: nazwy metody, którą ma wywołać, i tablicy obiektów XML_RPC_Value zawierającej argumenty używane podczas wywoływania metody. Podsumowując, kompletny dokument XML służący do zdalnego wywoływania metody tworzy się za pomocą jedynie dwóch wierszy kodu: // kodujemy parametry komunikatu

    $params = array(new XML_RPC_Value(15, 'int')); // kodujemy wywołanie metody w XML

    $message = new XML_RPC_Message('examples.getStateName', $params);

    Aby wysłać taki komunikat XML-RPC do usługi, należy skorzystać z metody call() klienta, której jako argument przesyła się tylko obiekt XML_RPC_Message. Metoda ta zwróci instancję obiektu odpowiedzi XML_RPC_Response() reprezentującą dokument zwrócony klientowi przez serwer usługi. Obiekt ten pozwala łatwo sprawdzić, czy podczas zdalnego wywoływania procedury wystąpił jakiś błąd. Jeśli metoda faultCode() tego obiektu zwróci wartość inną niż zero, oznaczać to będzie, że coś poszło nie tak. W tym przypadku będziemy mogli skorzystać z metody faultString(), by pobrać zrozumiały dla człowieka opis błędu, który wystąpił podczas wywoływania procedury. Jeśli jednak żaden błąd się nie pojawi, to będziemy mogli skorzystać z metody value(), aby pobrać wartość zwróconą przez odpowiedź serwera. Warto jednak pamiętać, że jest to instancja obiektu XML_RPC_Value, która zawiera rzeczywistą wartość, jak również informacje o typie tej wartości. Jeśli spróbujemy wyświetlić ten obiekt na ekranie, to zobaczymy nie tylko nazwę stanu, ale również dodatkową informację w rodzaju Object id #4. Konieczne będzie pobranie

    179

    PEAR. Programowanie w PHP

    z tego obiektu samej tylko wartości i przekonwertowanie jej na zwykłą wartość PHP. Dopiero wtedy będziemy mogli swobodnie z niej korzystać. Klasa XML_RPC oferuje funkcję XML_ RPC_decode(), która to dla nas zrobi. Teraz wartość zwrócona przez funkcję będzie, tak jak oczekiwaliśmy, łańcuchem zawierającym nazwę stanu. Dlatego gdy uruchomimy nasz skrypt, zwróci on następującą informację: Stan to Iowa

    Za pomocą narzędzi repozytorium PEAR korzystanie z usług WWW jest znacznie prostsze niż można by sądzić. Zaprezentowany przez nas przykład nie jest zbyt użyteczny i w prawdziwym życiu czytelnicy korzystać będą zapewne z bardziej złożonych usług opartych na protokole XML-RPC. Jako że instalator PEAR wykorzystuje protokół XML-RPC do komunikowania się z witryną PEAR, można by sądzić, że tej samej techniki dało by się użyć podczas komunikowania z witryną innej usługi. Czytelnicy, którzy tak myślą, mają zupełną rację. Można to bez trudu zrobić za pomocą pakietu XML_RPC. Jeśli mamy zainstalowane oprogramowanie repozytorium PEAR w wersji 1.4.0 lub nowszej, to program instalacyjny będzie w stanie poinformować nas, jakie metody XML-RPC oferuje dla każdego kanału usługa PEAR. Wystarczy tylko wpisać polecenie pear channel-info pear.php.net i otrzymamy informacje podobne do tych zaprezentowanych poniżej: Channel pear.php.net Information: ================================= Name and Server pear.php.net Alias pear Summary PHP Extension and Application Repository Validation Package Name PEAR_Validate Validation Package default Version Server Capabilities =================== Type Version/REST type Function Name/REST base xmlrpc 1.0 logintest xmlrpc 1.0 package.listLatestReleases xmlrpc 1.0 package.listAll xmlrpc 1.0 package.info xmlrpc 1.0 package.getDownloadURL xmlrpc 1.1 package.getDownloadURL xmlrpc 1.0 package.getDepDownloadURL xmlrpc 1.1 package.getDepDownloadURL xmlrpc 1.0 package.search xmlrpc 1.0 channel.listAll rest REST1.0 http://pear.php.net/rest/

    Wytłuszczone wiersze informują, jakie metody XML-RPC oferuje ta usługa. Musimy tylko wiedzieć, gdzie usługa jest zlokalizowana, abyśmy mogli utworzyć nowego klienta. Dla witryny PEAR odpowiednia usługa zlokalizowana jest pod następującym adresem: http://pear.php.net/ xmlrpc.php. Dlatego, jeśli chcemy przeszukać witrynę PEAR w poszukiwaniu pakietu, który zawiera w nazwie litery „XML”, wystarczy skorzystać z następującego skryptu:

    180

    Rozdział 4. • Usługi WWW

    require_once 'XML/RPC.php'; $client = new XML_RPC_Client('/xmlrpc.php', 'pear.php.net'); $params = array(new XML_RPC_Value('XML', 'string')); $message = new XML_RPC_Message('package.search', $params); $response = $client->send($message); if ($response->faultCode()) { echo "Nie mogę skorzystać z usługi XML-RPC.\n"; echo $response->faultString(); exit(); } $value = $response->value(); $packages = XML_RPC_decode($value); foreach ($packages as $packageName => $packageInfo) { echo "$packageName\n"; echo "

    {$packageInfo['summary']}

    \n"; }

    W tym przykładzie skorzystaliśmy z metody package.search() oferowanej przez usługę WWW działającą w witrynie PEAR. Metoda ta zwraca tablicę asocjacyjną, którą można łatwo przejrzeć w pętli foreach(), tak jakby dane zostały nam dostarczone nie przez zdalną metodę, a jakieś lokalne źródło. Po uruchomieniu skrypt zwróci następujące informacje: XML_Beautifier: Class to format XML documents. XML_CSSML: The PEAR::XML_CSSML package provides methods for creating cascading style sheets (CSS) from an XML standard called CSSML. XML_FastCreate: Fast creation of valid XML with DTD control. XML_fo2pdf: Converts a xsl-fo file to pdf/ps/pcl/text/etc with the help of apache-fop XML_HTMLSax: A SAX parser for HTML and other badly formed XML documents XML_image2svg: Image to SVG conversion XML_NITF: Parse NITF documents. XML_Parser: XML parsing class based on PHP's bundled expat XML_RPC: PHP implementation of the XML-RPC protocol XML_RSS: RSS parser XML_SVG: XML_SVG API XML_Transformer: XML Transformations in PHP XML_Tree: Represent XML data in a tree structure XML_Util: XML utility class. XML_Wddx: Wddx pretty serializer and deserializer

    181

    PEAR. Programowanie w PHP

    Oczywiście wyniki uruchomienia skryptu mogą się odrobinę różnić od tych zaprezentowanych tutaj, ponieważ zasób pakietów dostępnych w repozytorium PEAR zmienia się bardzo często. Jest to właśnie jedna z podstawowych zalet usług WWW — zawsze otrzymujemy najbardziej aktualne dane.

    Sięganie do interfejsu API Google Google to obecnie jedna z najbardziej popularnych witryn internetowych oferujących swoje funkcje w formie usług WWW. Chociaż interfejs API Google jest oficjalnie nadal w wersji beta, mimo to należy do najczęściej używanych usług WWW. Więcej na temat usługi WWW Google można znaleźć w witrynie Google pod adresem: http://www.google.com/apis/. Aby móc sięgać do interfejsu API Google, konieczne będzie utworzenie odpowiedniego konta Google. Po zarejestrowaniu się w ten sposób w witrynie Google otrzymamy odpowiedni klucz API (ang. API key) Google, który będziemy musieli podawać w każdym żądaniu wysyłanym do usługi WWW. Konto to pozwala na wysyłanie do interfejsu API Google za darmo 1000 żądań przeszukiwania za darmo. Ponieważ Google oferuje usługę opartą na protokole SOAP, możemy skorzystać z nowego rozszerzenia SOAP dodanego w PHP5, by za jego pomocą odpytywać usługę WWW Google. Aby na przykład odszukać za pomocą interfejsu API wyszukiwarki Google wydawnictwo Packt przy użyciu frazy „Packt Publishing”, należy skorzystać z następującego kodu: // Nasz osobisty klucz API

    $myGoogleKey = 'TUPODAJEMYNASZKLUCZ'; $google = new SoapClient('http://api.google.com/GoogleSearch.wsdl'); $result = $google->doGoogleSearch( // Klucz licencji $myGoogleKey, 'Packt Publishing', // fraza wyszukiwania 0, // pierwszy wynik 10, // Liczba zwracanych wyników false, // nie zwracamy podobnych wyników '', // ograniczamy do tematów true, // filtrujemy zawartość dla dorosłych '', // filtr językowy '', // kodowanie danych wejściowych, ignorowane '' // kodowanie danych wyjściowych, ignorowane ); // Wyświetlamy tytuły pierwszych dziesięciu stron

    $i = 1; foreach ($result->resultElements as $entry) { printf("%02d. %s\n", $i++, $entry->title); }

    182

    Rozdział 4. • Usługi WWW

    Nowe rozszerzenie SOAP potrafi tworzyć klienty proxy na podstawie dowolnego dokumentu WSDL, który prześlemy konstruktorowi. Dlatego też możemy bez trudu przywoływać na tym obiekcie metody dostarczane przez Google. Niemniej jednak korzystanie z klienta nie jest takie proste, jak mogłoby być, gdyż zawsze musimy definiować wszystkie dziesięć parametrów, choć w większości przypadków nie będziemy ich wszystkich potrzebować. Oznacza to, że będziemy musieli pamiętać porządek parametrów, w przeciwnym bowiem razie usługa WWW Google zareaguje na nasze zapytanie przesyłając informacje o błędzie SOAP. Jest to niezmiernie irytujące, ponieważ ostatnie dwa parametry (kodowanie danych wejściowych i danych zwracanych) nie są używane przez usługę WWW, a mimo to protokół SOAP nadal wymaga ich przesyłania. Pakiet Services_Google dostępny w repozytorium PEAR w wygodny sposób obudowuje rozszerzenie SOAP języka PHP, co bardzo ułatwia pracę z interfejsem API Google. Za pomocą pakietu Services_Google kod służący do wysyłania tego samego co poprzednio zapytania staje się znacznie bardziej czytelny: require_once 'Services/Google.php'; $myGoogleKey = 'GetYourOwnKey'; $google = new Services_Google($myGoogleKey); $google->queryOptions['limit'] = 10; $google->search('Packt Publishing'); $i = 1; foreach($google as $entry) { printf("%02d. %s\n", $i++, $entry->title); }

    Po załączeniu klasy Services_Google musimy przesłać nasz klucz API i potem będziemy mogli natychmiast zacząć sięgać do usługi WWW Google, korzystając z metod oferowanych przez klasę Services_Google. Bez trudu można ograniczyć zasięg każdego wyszukiwania odwołując się do właściwości queryOptions nowo utworzonego obiektu. W następnej kolejności przywołujemy metodę search() i przesyłamy jej jako jedyny argument poszukiwaną frazę. Niemniej użycie tej metody nie przywoła automatycznie usługi WWW. Zostanie ona przywołana dopiero wtedy, gdy zaczniemy sięgać do wyników wyszukiwania. Klasa Services_Google implementuje interfejs Iterator, który umożliwia przeglądanie obiektu podobnie jak tablicy w pętli foreach, co zresztą widzieliśmy w tym przykładzie. Oznacza to, że wyniki zapytania będą przesyłane nam właśnie wtedy, kiedy będziemy ich potrzebować. Podsumowując, po uruchomieniu tego skryptu otrzymamy następujące informacje: 01. 02. 03. 04. 05.

    Packt Publishing Book Store Asterisk User Training for Busy Programmers Building Websites with Mambo Learning eZ publish 3

    183

    PEAR. Programowanie w PHP

    06. 07. 08. 09. 10.

    Building Websites with OpenCms Content Management with Plone BPEL Building Online Stores with osCommerce: Beginner Edition Packt Publishing | Gadgetopia

    Oprócz parametru limit, przed skorzystaniem z metody search() można jeszcze określić kilka opcji zapytania. Kolejna tabela zawiera przegląd dostępnych opcji. Ci z czytelników, którzy znają dobrze usługę WWW Google, bez trudu zauważą, że prawie takie same parametry można przesyłać metodzie doGoogleSearch() usługi. Nazwa opcji

    Opis

    Wartość domyślna

    start

    Liczba pierwszych wyników, które chcemy pobrać.

    0

    maxResults

    Maksymalna liczba wyników pobieranych na raz.

    10

    limit

    Maksymalna liczba wszystkich pobieranych wyników.

    false (fałsz)

    filter

    Określa, czy ignorować, czy nie, podobne wyniki.

    true (prawda)

    restricts

    Określa, czy zawężać wyniki do jakiegoś tematu.

    pusty łańcuch

    safeSearch

    Określa, czy ignorować treści przeznaczone tylko dla dorosłych.

    true

    language

    Określa język, do którego należy zawęzić poszukiwania.

    pusty łańcuch

    Jest tu jednak jedna opcja, która nie wywodzi się z usługi WWW Google. Opcja limit pozwala na ograniczenie liczby wyników zwracanych przez obiekt Services_Google. Metoda doGoogleSearch() usługi WWW jest natomiast w stanie zwracać tylko 10 stron wyników w każdym jej przywołaniu. Aby pobrać kolejne 10 stron, trzeba będzie przywołać usługę WWW jeszcze raz. Podczas korzystania z klasy Services_Google kolejne przywołania usługi WWW wykonywane są automatycznie, kiedy przeglądamy zbiór wyników. Wystarczy po prostu zwiększyć wartość opcji limit, na przykład do 20. Przetwarzanie wyników nadal będzie wykonywane w pojedynczej pętli foreach. W chwili gdy pisaliśmy tę książkę, Google dostarczało również usługę sugerującą właściwą pisownię i możliwość pobierania strony z pamięci podręcznej (ang. cache) Google. Oczywiście klasa Services_Google oferuje również kod obudowujący także te usługi. Oto przykład sięgania do usługi sprawdzania pisowni: require_once 'Services/Google.php'; $myGoogleKey = 'GetYourOwnKey'; $google = new Services_Google($myGoogleKey); $suggestion = $google->spellingSuggestion('HTTP portocrol'); echo $suggestion;

    184

    Rozdział 4. • Usługi WWW

    Jak można było oczekiwać, ten prosty skrypt zwróci informację, że poprawna pisownia to HTTP protocol (protokół HTTP). Również pobieranie stron z pamięci podręcznej Google nie jest niczym trudnym, co udowadnia niniejszy kod: require_once 'Services/Google.php'; $myGoogleKey = 'GetYourOwnKey'; $google = new Services_Google($myGoogleKey); $html = $google->getCachedPage('http://pear.php.net/'); echo $page;

    Jeśli uruchomimy ten skrypt w naszej przeglądarce, powinniśmy zobaczyć typowy wynik działania pamięci podręcznej Google — oryginalną stronę oraz nagłówek pamięci podręcznej Google, który zawiera informację o buforowanych w pamięci danych. Jak widać, praca z interfejsem API Google jest znacznie prostsza, gdy korzystamy z pakietu Services_Google.

    Korzystanie z usług WWW opartych na REST SOAP jest niezmiernie złożonym protokołem, dlatego też wiele nowszych usług opartych jest na prostszym protokole o nazwie REST. W kolejnej części rozdziału opowiemy, jak korzystać z tych usług za pomocą PHP i repozytorium PEAR.

    Przeszukiwanie blogów za pomocą Services_Technorati Podczas gdy zwykłe wyszukiwarki, takie jak Google, umożliwiają odszukiwanie odpowiednich słów kluczowych w dowolnej witrynie WWW, jest sporo wyszukiwarek, które koncentrują się na określonych, mniejszych częściach globalnej sieci. Jedną z takich wyspecjalizowanych wyszukiwarek jest Technorati (http://www.technorati.com), która poszukuje słów kluczowych tylko w internetowych blogach. Oczywiście wyszukiwarka Technorati oferuje również interfejs API pozwalający na korzystanie z tej usługi w naszej witrynie. Inaczej zresztą nie wspominalibyśmy o niej w tym rozdziale. Zanim jednak przyjrzymy się bliżej temu interfejsowi API, kilka słów o tym, jak działa wyszukiwarka Technorati. Właściciel bloga może się bez trudu zarejestrować za darmo w wyszukiwarce Technorati i zgłosić (ang. claim) swój blog. Przez zgłoszenie bloga sprawiamy, że pojawia się on w indeksie wyszukiwarki Technorati, która będzie okresowo indeksować wszystkie wpisy w naszym blogu. Oprócz tego będzie również wykrywać w naszym blogu łącza do wszystkich innych zarejestrowanych blogów i vice versa. Dane te służyć będą do obliczania rankingu naszego bloga w katalogu blogów Technorati. Co więcej, wyszukiwarka Technorati oferuje prosty kod JavaScript, który można bez trudu dodać do naszego bloga, by dołączyć do niego różne użyteczne łącza związane z Technorati. Witryna Technorati oferuje precyzyjne instrukcje, jakie czynności krok po kroku należy wykonać, by zgłosić swój blog. 185

    PEAR. Programowanie w PHP

    Teraz, gdy wiemy już, czym jest Technorati, możemy przyjrzeć się interfejsowi API oferowanemu przez tę wyszukiwarkę. Podczas projektowania jej interfejsu API autorzy wyszukiwarki podjęli decyzję, że nie będą korzystać ani z protokołu XML-RPC, ani z protokołu SOAP, ale zdefiniują własny protokół oparty na standardzie XML. Niemniej w warstwie transportowej nadal korzystają z protokołu HTTP. Rozwiązanie zastosowane przez Technorati nosi nazwę protokołu lub modelu REST i coraz bardziej zyskuje na popularności, ponieważ jest on łatwiejszy w użyciu niż protokoły XML-RPC lub SOAP i w większości przypadków oferuje wszystkie funkcje potrzebne interfejsowi API usługi WWW. Klienty i serwery oparte na REST omówimy w dalszej części tego rozdziału. Na razie nie musimy się zajmować wewnętrznym mechanizmem działania REST, ponieważ repozytorium PEAR oferuje już implementację klienta dla usługi Technorati. Pakiet, który należy zainstalować, to Services_Technorati, ponadto potrzebne nam będą pakiety HTTP_Request i XML_Serializer. Te ostatnie prawdopodobnie zresztą będą już zainstalowane, ponieważ są to dość często używane pakiety. Po zainstalowaniu klienta konieczne będzie zarejestrowanie się w programie Technorati developers pod adresem http://www.technorati.com/developers/devprogram.html, aby uzyskać swój osobisty klucz API (ang. API key). Rejestracja w programie developers (programie dla programistów) jest prosta: jeśli mamy już standardowe konto w witrynie Technorati, to wystarczy tylko odpowiedzieć na kilka pytań na temat naszych planów odnośnie do korzystania z interfejsu API i zgodzić się na warunki (ang. terms and conditions) związane z udziałem w programie developers. Po zakończeniu procesu rejestracji będziemy mogli pobrać nasz klucz API pod adresem: http://www.technorati.com/developers/apikey.html. Więcej informacji na temat interfejsu API oraz programu developers można znaleźć w encyklopedycznej dokumentacji, dostępnej w witrynie pod adresem: http://developers.technorati.com/wiki. Jeśli jednak zamierzamy korzystać z interfejsu API za pośrednictwem pakietu Services_Technorati, to możemy pominąć informacje zawarte w dokumentacji, ponieważ pakiet oferuje nam prostą i intuicyjną metodę sięgania do usługi WWW Technorati. Pakietu Services_Technorati używa się w skryptach PHP prawie tak samo jak każdego innego pakietu PEAR: 1. Załączamy pakiet. 2. Tworzymy instancję klasy (obiekt), z której chcemy korzystać. 3. Używamy metod dostarczanych przez obiekt. Oto bardzo prosty przykład kodu poszukującego we wszystkich zarejestrowanych blogach terminu „Packt publishing” (wydawnictwo Packt): /** * Korzystamy z pakietu Services_Technorati */

    require_once 'Services/Technorati.php'; // Tu podajemy nasz klucz API

    $myApiKey = 'TUTAJTWOJKLUCZ'; // Tworzymy nową instancję opartą na naszym kluczu

    186

    Rozdział 4. • Usługi WWW

    $technorati = Services_Technorati::factory($myApiKey); // Korzystamy z usługi, otrzymamy z powrotem tablicę asocjacyjną

    $result = $technorati->search('Packt Publishing'); // Przeglądamy w pętli zbiór wyników

    $position = 1; foreach ($result['document']['item'] as $entry) { printf("%02d. %s\n%s\n\n", $position++, $entry['title'], $entry['excerpt']); }

    Jeśli uruchomimy ten skrypt, to zwróci on mniej więcej takie informacje: 01. An Evening with Joomla's Mitch Pirtle Last night BostonPHP hosted an evening with Mitch Pirtle of Joomla! fame at our our Boston office ... with the initiative. Packt publishing, who sells Building Websites With Mambo, is planning on publishing a similar Joomla! book. I have not recently talked to the Mambo team, which has reloaded 02. Permanent Link to Building a Custom Module for DotNetNuke 3.0This sample chapter from Packt Publ ishing "Building Websites with VB.NET and DotNetNuke 3.0" illustrates how to build and use a custom module 03. Packt Publishing December 2005 Newsletter The latest Packt Publishing newsletter is available online:

    Oczywiście wyniki, które otrzymają czytelnicy, będą się zapewne różnić, ponieważ bez wątpienia nim książka ta trafi na półki, w blogach pojawią się nowe wpisy na temat wydawnictwa Packt. Przykład ten pokazał dobitnie, że aby korzystać z usługi WWW Technorati, nie musimy nic wiedzieć na temat wewnętrznych zasad działania jej interfejsu API, ponieważ zgrabnie obudowuje ją dla nas pakiet Services_Technorati. Oczywiście pakiet ten oferuje nie tylko możliwość przeglądania pozycji w blogach. Jeśli na przykład znamy nazwę użytkownika usługi Technorati, to możemy bez trudu zdobyć z usługi Technorati bardziej dokładne informacje o tym użytkowniku, co ukazuje kolejny przykład: /** * Korzystamy z pakietu Services_Technorati */

    require_once 'Services/Technorati.php'; // Tu należy podać nasz klucz API

    $myApiKey = 'NASZKLUCZAPI';

    187

    PEAR. Programowanie w PHP

    // Tworzymy nową instancję bazującą na naszym kluczu

    $technorati = Services_Technorati::factory($myApiKey); // Pobieramy informacje na temat dowolnego użytkownika Technorati

    $result = $technorati->getInfo('schst'); print_r($result);

    Wystarczy zastąpić użytą tu nazwę użytkownika schst nazwą, którą podaliśmy podczas rejestrowania się w witrynie Technorati, aby otrzymać informacje podobne do tych przedstawionych tutaj, tyle że oczywiście odnoszące się do naszej nazwy użytkownika: Array ( [version] => 1.0 [document] => Array ( [result] => Array ( [username] => schst [firstname] => Stephan [lastname] => Schmidt [thumbnailpicture] => http://www.technorati.com/progimages/ photo.jpg?uid=132607&mood=default ) [item] => Array ( [weblog] => Array ( [name] => a programmer's best friend [url] => http://blog.php-tools.net [rssurl] => http://blog.php-tools.net/feeds/index.rss2 [atomurl] => http://blog.php-tools.net/ feeds/atom.xml [inboundblogs] => 0 [inboundlinks] => 0 [lastupdate] => 2005-10-17 17:51:48 GMT [rank] => [lat] => 0 [lon] => 0 [lang] => 26110 ) ) ) )

    188

    Rozdział 4. • Usługi WWW

    Informacje te można na przykład wykorzystać, by przygotować stronę WWW z informacjami o profilu wyświetlającą listę wszystkich blogów, które dany użytkownik posiada w naszej witrynie. Mimo iż rejestracja w programie developers jest darmowa, to liczba odwołań do interfejsu API, które możemy wykonać każdego dnia, jest ograniczona. Na szczęście nie musimy zliczać odwołań, które wykonaliśmy tego dnia, by przekonać się, ile nam ich jeszcze pozostało. Zamiast tego możemy po prostu za pomocą odpowiedniej metody uzyskać te informacje od interfejsu API: $result = $technorati->keyInfo(); $callsMade = (int)$result['document']['result']['apiqueries']; $callsMax = (int)$result['document']['result']['maxqueries']; $callsLeft = $callsMax - $callsMade; echo "Wykonano już {$callsMade} z {$callsMax} dozwolonych dziennie wywołań API. Pozostało jeszcze {$callsLeft} wywołań API";

    Kod ten zwróci liczbę zapytań, które do tej pory wykonaliśmy, oraz liczbę zapytań, które możemy wykonać w ciągu dnia. Wywołania metody keyInfo() nie liczą się jako zapytania, dlatego też możemy ich wykonać w ciągu dnia tyle, ile chcemy.

    Technorati cosmos Ostatnia funkcja witryny Technorati, którą się zajmiemy, to Technorati cosmos. Funkcja cosmos (kosmos) próbuje powiązać ze sobą różne blogi, analizując wszystkie wpisy w blogu i próbując przygotować łącza z tego blogu do innych powiązanych blogów i na odwrót. Kolejny przykład pokazuje, w jaki sposób można sięgać do funkcji cosmos używając klasy Services_Technorati obudowującej interfejs API usługi WWW: /** * Korzystamy z pakietu Services_Technorati */

    require_once 'Services/Technorati.php'; // tu podajemy swój klucz API

    $myApiKey = 'NASZKLUCZAPI'; // Tworzymy nową instancję korzystając z naszego klucza

    $technorati = Services_Technorati::factory($myApiKey); // Określamy opcje dalszego wyszukiwania // Ograniczając limit do 10 łącz

    $options = array('limit' => 10); // Odszukujemy blogi odwołujące się do witryny PEAR

    $result = $technorati->cosmos('http://pear.php.net', $options); // Wyświetlamy parę podstawowych informacji

    print "Szukamy blogów z łączami do adresu http://pear.php.net\n\n";

    189

    PEAR. Programowanie w PHP

    printf("Odnalezionych blogów: %d\n", $result['document']['result']['i nboundblogs']); printf("Odnalezionych łącz: %d\n", $result['document']['result']['i nboundlinks']); // Przeglądamy odnalezione łącza

    print "\nPierwszych dziesięć łącz:\n\n"; foreach ($result['document']['item'] as $link) { printf("Łącze w blogu %s to %s\n", $link['weblog']['name'], $link['linkurl']); }

    Jeśli uruchomimy ten skrypt, zwróci on mniej więcej takie informacje: Szukamy blogów z łączami do adresu http://pear.php.net Odnaleziono blogów: 590 Odnaleziono łącz: 1837 Pierwszych dziesięć łącz: Łącze w blogu satoru 120% do http://pear.php.net/manual/ja/ Łącze w blogu satoru 120% do http://pear.php.net/ Łącze w blogu minfish.jp/blog do http://pear.php.net/package/ Spreadsheet_Excel_Writer ... Łącze w blogu Ciro Feitosa | Desenvolvedor Web to http://pear.php.net/ packages.php?catpid=7&catname=Database

    Jeśli więc interesuje nas szczególnie jakiś temat, usługa Technorati pomoże nam odnaleźć strony, które mogą zawierać więcej informacji związanych z tym tematem, dostarczając adresy URL blogów, dotyczących poszukiwanego przez nas tematu. W tym przykładzie przesłaliśmy metodzie cosmos() drugi parametr określający limit przeglądanych wyników wyszukiwania. Większość metod oferowanych przez klasę Services_Technorati pozwala na przesyłanie im jako parametru dodatkowej tablicy asocjacyjnej zawierającej kilka opcji. Listę wszystkich dostępnych metod i właściwych dla nich opcji można znaleźć w dokumentacji PEAR, dostępnej pod adresem: http://pear.php.net/manual/en/package.webservices. services-technorati.php.

    Sięganie do usługi WWW księgarni Amazon Kolejną witryną WWW oferującą usługi WWW oparte na własnym, bazującym na języku XML protokole jest witryna Amazon.com. Internetowa księgarnia Amazon stara się poprawić wyniki sprzedaży oferując program stowarzyszeniowy, który umożliwia wszystkim zainteresowanym umieszczanie łącz do produktów dostarczanych przez witrynę Amazon. Jeśli jakiś klient zakupi dany produkt, odnajdując go dzięki łączu w stowarzyszonej witrynie, to partner księgarni Amazon dostaje odpowiednią premię pieniężną, zależną od ceny produktu.

    190

    Rozdział 4. • Usługi WWW

    Aby jeszcze bardziej wspomagać sprzedaż dzięki łączom u partnerów, Amazon oferuje interfejs API usługi WWW, dzięki któremu partnerzy witryny Amazon mogą dodawać do swoich witryn kilka użytecznych funkcji związanych z jej usługą WWW.

    Tworzenie konta w witrynie Amazon Aby skorzystać z tej usługi, konieczne będzie zarejestrowanie się jako partner (ang. associate) księgarni Amazon pod adresem: http://www.amazon.com/gp/browse.html/?node=3435371. Po zarejestrowaniu się jako partner możemy oczywiście zacząć zarabiać, niemniej jeśli chcemy korzystać z usługi WWW (a to właśnie jest naszym celem), to konieczne będzie jeszcze utworzenie odpowiedniego konta usługi WWW zaglądając pod adres: http://www.amazon.com/gp/ browse.html/?node=3435361. Witryna Amazon odeśle nam e-mail zawierający identyfikator subskrypcji, który umożliwi identyfikowanie naszego konta, gdy odwoływać się będziemy do usługi. Należy zachować ten token identyfikacyjny w bezpiecznym miejscu, aby nie zaginął. Gdy już będziemy mieli identyfikator subskrypcji, wystarczy tylko zainstalować pakiet Services_ Amazon oraz wszystkie pakiety potrzebne mu do prawidłowego działania i będziemy mogli zacząć korzystać z usługi WWW księgarni Amazon. Pakiet ten dostarcza dwóch klas, których można użyć, by sięgać do usługi: „ Services_Amazon „ Services_AmazonECS4 Różnica pomiędzy nimi polega na tym, że klasa Services_Amazon implementuje wersję 3.0 usługi WWW witryny Amazon, podczas gdy klasa Services_AmazonECS4 implementuje nową wersję 4.0 interfejsu API tej usługi, która oferuje znacznie większe możliwości. Ponadto klasa Services_AmazonECS4 dostarcza wielu różnych zaawansowanych funkcji, takich jak zintegrowane buforowanie wyników wyszukiwania w pamięci podręcznej, które umożliwia poprawienie wydajności aplikacji współpracującej z witryną Amazon. Ponieważ starsza wersja usługi WWW nie zawiera żadnych funkcji, które zostałyby zarzucone w nowej wersji, skoncentrujemy się tutaj wyłącznie na klasie Services_AmazonECS4. Aby przygotować pakiet do działania, wystarczy zaledwie parę wierszy kodu: /** * Korzystamy z klasy Services_AmazonECS4 */

    require_once 'Services/AmazonECS4.php'; // Nasz identyfikator subskrypcji

    $subscriptionId = 'IDENTYFIKATORSUBSKRYPCJI'; // Nasz identyfikator partnera Amazon

    $accociateId = 'schstnet-20'; // tworzymy nowego klinta przesyłająć // identyfikatory subskrypcji i partnera Amazon

    $amazon = new Services_AmazonECS4($subscriptionId, $accociateId);

    191

    PEAR. Programowanie w PHP

    $amazon->setLocale('US');

    Gdy już załączymy odpowiednią klasę, należy utworzyć instancję nowego obiektu i przesłać jej identyfikator subskrypcji usługi WWW. Jako opcjonalny parametr można również przesłać swój identyfikator partnera Amazon (ang. associate ID). Po przesłaniu obiektowi tego identyfikatora wszystkie zwracane adresy URL będą automatycznie zawierać nasz identyfikator partnera Amazon, dzięki czemu będziemy mogli go wykorzystać w naszej aplikacji. Wszystkie zamówienia uruchamiane przez te łącza będą zaliczane na nasze konto w witrynie Amazon i otrzymamy oczywiście za nie pieniądze. Po utworzeniu instancji obiektu za pomocą metody setLocals() określana jest lokalna witryna Amazon, która wykorzystywana będzie podczas wykonywania kolejnych odwołań do interfejsu usługi WWW Amazon. W kolejnej tabeli prezentujemy listę wszystkich dostępnych kodów lokalnych witryn i odpowiadających im internetowych sklepów Amazon. Kod

    Witryna Amazon

    US

    amazon.com (USA)

    UK

    amazon.co.uk (Wielka Brytania)

    DE

    amazon.de (Niemcy)

    JP

    amazon.co.jp (Japonia)

    FR

    amazon.fr (Francja)

    CA

    amazon.ca (Kanada)

    Po przygotowaniu klienta usługi możemy zacząć przywoływać jej metody.

    Przeszukiwanie witryny Amazon.com Zaczniemy od prostego wyszukiwania w witrynie Amazon.com określonych słów kluczowych, tak jak pokazuje to kolejny przykład: $options = array(); $options['Keywords'] = 'PEAR'; $result = $amazon->ItemSearch('Books', $options);

    Aby odszukać określone towary, możemy skorzystać z metody ItemSearch(), która pozwala na przesłanie jej dwóch parametrów — indeksu przedmiotu, który chcemy odszukać, i tablicy asocjacyjnej zawierającej opcje stosowane podczas przeszukiwania. W tym przykładzie użyjemy opcji tylko po to, by podać słowa kluczowe użyte w wyszukiwaniu. Metoda ItemSearch() zwróci obiekt PEAR_Error, jeśli przeszukiwanie zakończy się niepowodzeniem. W razie sukcesu natomiast zwróci tablicę zawierającą wyniki wyszukiwania, jak również odpowiednie metainformacje związane z wyszukiwaniem. Przedstawiony tutaj fragment kodu reaguje na wspomniane dwa rodzaje zwróconych danych i wyświetla odpowiednie informacje użytkownikowi: 192

    Rozdział 4. • Usługi WWW

    if (PEAR::isError($result)) { print "Wystąpił błąd\n"; print $result->getMessage() . "\n"; exit(); } foreach ($result['Item'] as $book) { $title = $book['ItemAttributes']['Title']; $author = $book['ItemAttributes']['Author']; if (is_array($author)) { $author = implode(', ', $author); } printf("%s by %s\n", $title, $author); }

    Wynik działania całego skryptu będzie następujący: Behavior Modification: What It Is and How to Do It (7th Edition) by Garry L. Martin, Joseph Pear Web Database Applications with PHP & MySQL, 2nd Edition by Hugh E. Williams Learning PHP 5 by David Sklar PHP Hacks : Tips & Tools For Creating Dynamic Websites (Hacks) by Jack Herrington An Instance of the Fingerpost by Iain Pears The Portrait by Iain Pears The Prisoner Pear : Stories from the Lake by Elissa Minor Rust Dream of Scipio by Iain Pears Apples & Pears : The Body Shape Solution for Weight Loss and Wellness by Marie Savard, Carol Svec Each Peach Pear Plum board book (Viking Kestrel Picture Books) by Allan Ahlberg

    Jak widać, chcieliśmy odnaleźć książki na temat repozytorium PEAR, a zamiast tego otrzymaliśmy listę kilku książek napisanych przez autorów o nazwisku Pear. Ponieważ interesują nas tylko publikacje zawierające w tytule termin PEAR, zmodyfikujemy więc odrobinę nasze żądanie przeszukiwania:

    193

    PEAR. Programowanie w PHP

    $options = array(); $options['Title'] = 'PEAR'; $result = $amazon->ItemSearch('Books', $options);

    Zamiast wykonywać proste wyszukiwanie określonych słów kluczowych, przypisaliśmy w tablicy opcji odpowiednią wartość kluczowi Title (Tytuł) i powtórzyliśmy wyszukiwanie. Dokumentacja interfejsu API usług WWW księgarni Amazon Witryna Amazon oferuje wyczerpującą dokumentację wszystkich oferowanych usług WWW. Można ją znaleźć pod następującym adresem: http://aws.amazon.com/resources

    Metoda ItemSearch() pozwala programiście na przesyłanie długiej listy parametrów definiowanych w tablicy opcji. Dlatego też warto wiedzieć, że są dwa sposoby uzyskania kompletnej listy opcji wykorzystywanych przez tę metodę. Konwencjonalny sposób to zajrzenie do dokumentacji interfejsu API usługi. Drugi sposób polega natomiast na pobraniu przez aplikację listy wszystkich dostępnych parametrów wprost od naszego interfejsu API. Można to zrobić za pomocą metody Help() klasy Services_AmazonECS4: // tworzymy nowego klienta podając // identyfikatory subskrypcji i partnera

    $amazon = new Services_AmazonECS4($subscriptionId, $accociateId); $amazon->setLocale('US'); $result = $amazon->Help('Operation', 'ItemSearch'); print "Parametry dla ItemSearch()\n"; foreach ($result['OperationInformation']['AvailableParameters']['Parameter'] as $param) { echo "* $param\n"; }

    Jeśli uruchomimy teraz ten skrypt, to zwróci on pełną listę parametrów, które można definiować w tablicy opcji: Parametry dla ItemSearch() * Actor * Artist * AssociateTag * AudienceRating * Author * Availability ... * Title * Validate * VariationPage * Version * XMLEscaping

    194

    Rozdział 4. • Usługi WWW

    Metodę Help() można wykorzystać do pobierania informacji na temat dowolnej z metod oferowanych przez interfejs API. Można za jej pomocą pobrać nawet dokumentację samej metody Help(): $result = $amazon->Help('Operation', 'Help'); print_r($result);

    Metoda Help(), oprócz listy wszystkich dostępnych parametrów danej metody, podaje również informacje o tym, które parametry są wymagane (RequiredParameters), a także przykłady możliwych odpowiedzi: Array ( [Request] => Array ( [IsValid] => True [HelpRequest] => Array ( [About] => Help [HelpType] => Operation ) ) [OperationInformation] => Array ( [Name] => Help [RequiredParameters] => Array ( [Parameter] => Array ( [0] => About [1] => HelpType ) ) [AvailableParameters] => Array ( [Parameter] => Array ( [0] => AssociateTag [1] => ContentType [2] => Marketplace [3] => Style [4] => Validate [5] => Version [6] => XMLEscaping ) ) [DefaultResponseGroups] => Array ( [ResponseGroup] => Array

    195

    PEAR. Programowanie w PHP

    ( [0] => Request [1] => Help ) ) [AvailableResponseGroups] => Array ( [ResponseGroup] => Array ( [0] => Request [1] => Help ) ) )

    )

    Jeśli dany parametr znajduje się na liście wymaganych parametrów, to nie ma potrzeby dodawania go do listy opcji, ponieważ klient Services_AmazonECS4 żąda już przesłania metodzie wartości wymaganego parametru w osobnym argumencie. Przykładem mogą być parametry $about i $operation metody getHelp().

    Rozpatrywanie odpowiedzi od usługi W następnym przykładzie skorzystamy z dwóch kolejnych opcji metody ItemSearch(): opcji Sort oraz opcji ResponseGroup. Podczas gdy przeznaczenia pierwszej opcji — sortowanie danych — łatwo się domyślić, to druga z nich wymaga kilku słów objaśnienia. Należy wiedzieć, że księgarnia Amazon przechowuje całą masę informacji na temat każdego oferowanego towaru. Gdybyśmy zażyczyli sobie, aby metoda ItemSearch() zwracała wszystkie informacje dostępne na temat danego towaru, to rozmiar i zakres transmitowanych danych byłby zdecydowanie zbyt duży. W większości przypadków zależeć nam będzie na tym, by w odpowiedzi na żądanie przeszukiwania usługa WWW zwracała tylko jakąś cześć z dostępnych danych na temat towaru. Na szczęście opcja ResponseGroup pozwala określić, które konkretnie informacje są potrzebne naszej aplikacji. Opcji tej należy przesłać dzieloną przecinkami listę węzłów XML, które ma nam zwrócić usługa. Klient Services_AmazonECS4 automatycznie przekonwertuje te węzły XML na tablicę asocjacyjną, dzięki czemu będzie można do nich sięgać używając odpowiedniej nazwy znacznika. Przyjrzyjmy się teraz, w jaki sposób można ograniczać odpowiedzi usługi WWW za pomocą opcji ResponseGroup: // tworzymy nowego klienta podając // identyfikatory subskrypcji i partnera Amazon

    $amazon = new Services_AmazonECS4($subscriptionId, $accociateId); $amazon->setLocale('US'); $options = array(); $options['Publisher'] = 'Packt Publishing'; $options['Sort'] = 'salesrank'; $options['ResponseGroup'] = 'ItemIds,ItemAttributes,Images';

    196

    Rozdział 4. • Usługi WWW

    $result = $amazon->ItemSearch('Books', $options); if (PEAR::isError($result)) { print "Wystąpił błąd
    \n"; print $result->getMessage() . "
    \n"; exit(); } foreach ($result['Item'] as $book) { print ''; printf('', $book['DetailPageURL'], $book['SmallImage']['URL'], $book['ItemAttributes']['Title']); printf('%s', $book['ItemAttributes']['Title']); if (is_array($book['ItemAttributes']['Author'])) { $book['ItemAttributes']['Author'] = implode(', ', $book['ItemAttributes']['Author']); } printf('

    %s
    %s

    ', $book['ItemAttributes']['Author'], $book['ItemAttributes']['ListPrice']['FormattedPrice']); print ''; }

    Po uruchomieniu tego skryptu w oknie naszej przeglądarki pojawi się posortowana alfabetycznie lista dziesięciu książek wydawnictwa Packt wraz z miniaturką ilustracji widocznej na okładce książki, tak jak to można zobaczyć na rysunku 4.1. Po kliknięciu okładki książki zostaniemy przekierowani do witryny Amazon, gdzie będziemy mogli zamówić książkę, a nasze konto partnerskie zostanie zasilone odpowiednią kwotą pieniężną. Stronę tu zaprezentowaną utworzyliśmy przypisując opcji ResponseGroup listę ItemIds, ItemAttributes, Images. Lista ta poinformowała usługę Amazon, jakie informacje na temat każdego z wyszukanych produktów chcemy otrzymać. Przy takim ustawieniu opcji ResponseGroup zostaną zwrócone następujące informacje: „ Znacznik ItemAttributes zawierać będzie tablicę z atrybutami każdej książki, takimi jak autor, wydawca, cena itp. „ Znaczniki SmallImage, MediumImage i LargeImage zawierać będą adresy URL i informacje o wymiarach różnej wielkości obrazków produktu. „ Znacznik ASIN będzie zawierać unikatowy identyfikator towaru w katalogu produktów oferowanych przez witrynę Amazon.

    197

    PEAR. Programowanie w PHP

    Rysunek 4.1. Korzystanie z usługi Amazon

    Najprostszym sposobem przekonania się, w jaki sposób sięga się do zwróconych informacji za pomocą PHP, jest przesłanie wartości zwróconej przez metodę ItemSearch() funkcji print_r() języka PHP.

    Dodatkowe usługi Oczywiście witryna Amazon dostarcza jeszcze wielu innych usług i za pomocą pakietu Services_Amazon można sięgać do większości funkcji interfejsu API tego internetowego sklepu. Do oferowanych przezeń usług należą między innymi: „ Zdalnie kontrolowane wózki z zakupami „ Odszukiwanie i przeglądanie szczegółowych informacji na temat produktów „ Odnajdywanie podobnych produktów „ Dostęp do list życzeń (ang. wish lists) „ Dostęp do targowiska (ang. marketplace) księgarni Amazon „ Dostęp do artykułów oceniających produkty (ang. customer reviews) Do wszystkich tych funkcji można sięgać w podobny sposób, jak to zostało pokazane w przypadku funkcji ItemSearch(). W przypadku wątpliwości, jakie opcje można przesyłać określonej funkcji, programista zawsze może zajrzeć do znakomitej dokumentacji oferowanej przez witrynę Amazon. Wszystkie znaczniki XML da się bezpośrednio mapować na odpowiednie tablice asocjacyjne.

    198

    Rozdział 4. • Usługi WWW

    Na koniec wreszcie skorzystamy z funkcji ItemLookup() interfejsu API, by pokazać, jak bardzo przeglądanie towarów podobne jest do ich odszukiwania. Metoda ItemLookup() zwraca informacje na temat towaru, bazując na jego kodzie ASIN lub ISBN. Jeśli więc chcielibyśmy poznać autorów znakomitej książki Mastering Mambo1, której kod ISBN to 1-904811-51-5, to powinniśmy skorzystać z następującego kodu: // tworzymy nowego klienta podając // identyfikatory subskrypcji i partnera Amazon

    $amazon = new Services_AmazonECS4($subscriptionId, $accociateId); $amazon->setLocale('US'); $options = array(); $options['ResponseGroup'] = 'ItemAttributes'; $result = $amazon->ItemLookup('1904811515', $options); if (PEAR::isError($result)) { print "An error occured
    \n"; print $result->getMessage() . "
    \n"; exit(); } print "Autorzy to "; print implode(' i ', $result['Item'][0]['ItemAttributes']['Author'] );

    Jeśli uruchomimy ten skrypt, zwróci on następujące informacje: Autorzy to: Tobias Hauser i Christian Wenz

    Jak widać, jedyna różnica polega na innej nazwie metody i argumentów przesłanych obu metodom. Co więcej, w taki sam sposób działają wszystkie metody pakietu Services_Amazon!

    Tworzenie własnych usług WWW opartych na REST Korzystaliśmy już z usług WWW opartych na protokole REST używając pakietów Services_ Technorati lub Services_Amazon, niemniej w obu przypadkach implementacja klienta usługi ukrywała przed nami wszystkie szczegóły działania REST. Ponieważ REST to dość nowa technologia, może się okazać, że musimy sięgnąć do usługi, dla której repozytorium PEAR nie oferuje jeszcze implementacji klienta. To jednak nie problem, bowiem jak się w dalszej części przekonamy, można bez trudu zaprogramować klienta dla dowolnej usługi opartej na REST, korzystając z dwóch specjalnie do tego celu przeznaczonych pakietów PEAR: HTTP_Request i HTTP_ Serializer. 1

    W polskiej wersji: Mambo. Tworzenie wydajnych serwisów internetowych, Tobias Hauser, Christian Wenz, Helion, Gliwice 2006 (ISBN: 83-246-0648-3).

    199

    PEAR. Programowanie w PHP

    Jako przykładowej usługi WWW użyjemy usługi Yahoo!, a to z czterech powodów: „ Wiemy, że witryna Yahoo! używa już swojej usługi WWW w niektórych oferowanych przez siebie aplikacjach. „ W usłudze WWW Yahoo! bardzo łatwo się zarejestrować. „ Usługa WWW Yahoo! jest typową usługą REST. „ Usługa WWW Yahoo! jest dobrze udokumentowana i łatwa w użyciu. Warto również wiedzieć, że większość usług oferowanych w witrynach Yahoo! jest usługami opartymi na protokole REST. Aby móc skorzystać z którejkolwiek z tych usług, trzeba zdobyć identyfikator aplikacji, który można otrzymać w centrum dla programistów (ang. developer center) Yahoo!, pod adresem: http://api.search.yahoo.com/webservices/register_application. Aby uzyskać identyfikator aplikacji (ang. application ID), trzeba posiadać identyfikator Yahoo!, niemniej rejestracja w witrynie Yahoo! również jest darmowa. Identyfikator aplikacji będzie przesyłany do witryny usług WWW Yahoo! za każdym razem, gdy będziemy chcieli skorzystać z jednej z jej usług. W ten sposób witryna śledzi wykonywane przez nas odwołania do metod. Usługa Yahoo! najbardziej znana jest ze swojej wyszukiwarki, dlatego też właśnie usługi wyszukiwania Yahoo! użyjemy jako przykładowej usługi bazującej na protokole REST. Niemniej warto wiedzieć, że Yahoo! oferuje również inne usługi: „ Usługę del.icio.us umożliwiającą przechowywanie swoich zakładek w internecie. „ Flickr, internetową galerię fotografii. „ Usługę map Yahoo! Maps. „ Usługę muzyczną Yahoo! music Engine. „ Katalog zakupów Yahoo! „ I wiele innych… Do większości wspomnianych usług można sięgać dokładnie w taki sam sposób, jak do usługi przeszukiwania.

    W jaki sposób działają usługi REST Zanim przyjrzymy się bliżej usłudze WWW Yahoo!, warto poświęcić chwilę na omówienie, w jaki sposób działają usługi REST. Podobnie jak usługi oparte na protokołach XML-RPC i SOAP, również i usługi oparte na REST używają jako protokołu transportowego, protokołu HTTP. Niemniej usługi REST nie używają żądania POST protokołu HTTP do przesyłania danych XML zawierających odwołanie do metody i jej parametry. Zamiast tego po prostu wykorzystują możliwości oferowane przez protokół HTTP, by odpowiednio opakować w żądaniu nazwę metody i jej argumenty. W większości przypadków metoda interfejsu API osadzana jest w ścieżce żądanego adresu URL, a wszystkie argumenty metody są przesyłane w łańcuchu zapytania. Dlatego też typowy adres URL dla wywołania metody za pomocą REST będzie wyglądać mniej więcej tak: http://www.my-site.com/rest/search?query=PEAR&page=1

    200

    Rozdział 4. • Usługi WWW

    Adres URL zawiera wszystkie informacje potrzebne, by sięgnąć od usługi WWW, czyli: „ Adres URL, pod którym zlokalizowana jest usługa WW (tutaj: http://www.my-site.com/rest/). „ Przywoływaną metodę (tutaj: search). „ Argumenty przesyłane przywoływanej metodzie (tutaj: query=PEAR&page=1). Aby przywołać tę samą metodę używając protokołu XML-RPC lub nawet protokołu SOAP, trzeba by było przygotować skomplikowany dokument XML wysyłany następnie do usługi WWW. Mimo iż komunikowanie się z usługą na sposób REST jest prostsze, nadal ma jedną wadę: argumenty metod nie zawierają informacji o typie i są przesyłane w postaci łańcuchów. Niemniej nie jest to zbyt poważny problem, usługa bowiem wie, w jaki sposób należy obsłużyć argumenty i przekonwertować je na wartości odpowiedniego typu. Dokładnie w taki sam sposób dane przesyłane przez użytkownika są traktowane w standardowych aplikacjach PHP. Po przetworzeniu wywołania metody usługa odpowie klientowi w podobny sposób, w jaki odpowiedziałaby usługa XMLRPC lub SOAP, zwracając odpowiedni dokument XML. Różnica w porównaniu do tamtych typów usług polega na tym, że nie ma żadnego schematu XML opisującego format zwracanego dokumentu XML. Możemy zwracać taki dokument XML, jaki najlepiej odpowiadać będzie wynikom zapytania. Należy sprawdzić, czy dostawca usługi dostarcza odpowiedniego schematu lub dokumentacji opisującej format zwracanych danych XML. Dlatego też wyniki zapytania mogą być bardzo różne, na przykład następujące:

    PEAR 1

    http://pear.php.net PEAR - PHP Extension and Application Repository

    http://pear.php-tools.net Kanał PAT PEAR

    http://www.pearadise.net Katalog PEAR Channel

    ...

    Struktura zaprezentowanego tu dokumentu XML nie powinna być trudna do zrozumienia i zarówno ludzie, jak i aplikacje nie powinni mieć żadnych problemów z jej interpretacją. Dokument zawiera wszystkie informacje, jakich moglibyśmy oczekiwać w wyniku wywołania metody przeszukującej (search):

    201

    PEAR. Programowanie w PHP

    „ Całkowitą liczbę wyników wyszukiwania (25) „ Liczbę elementów wyświetlanych na stronie (10) „ Liczbę wyświetlanych stron (3) „ Szczegółowe informacje o każdym z wyników wyszukiwania Jeśli chcielibyśmy te same informacje upakować w odpowiedzi SOAP, to rozmiary danych niezbędnych do skonstruowania, przesłania i analizy odpowiedzi usługi WWW byłyby znacznie większe, co oczywiście sprawiałoby, że użytkownik musiałby dłużej oczekiwać na odpowiedź naszej aplikacji wyszukującej. Jak już wspomnieliśmy, jedyną wadą korzystania z protokołu REST jest utrata informacji o typie zwracanych wartości. Ponieważ jednak język PHP nie traktuje typów danych zbyt restrykcyjnie, nie powinno to być dla nas większym problemem. Aby przywołać usługę WWW opartą na protokole REST, konieczne będzie wykonanie następujących, prostych czynności: 1. Skonstruować adres URL zawierający wywoływaną metodę i wszystkie jej argumenty. 2. Wykonać żądanie HTTP i pobrać dokument XML z otrzymanej odpowiedzi. 3. Zanalizować dokument XML i pobrać potrzebne nam informacje. Teraz, gdy już wiemy, jak w ogólnym zarysie działa mechanizm komunikacji REST, możemy wrócić do usługi WWW Yahoo!, by przekonać się, w jaki sposób repozytorium PEAR może nam ułatwić komunikację z tą usługą.

    Sięganie do interfejsu API usług Yahoo! W naszym pierwszym przykładzie sięgniemy do usługi przeszukiwania internetu Yahoo!, prawdopodobnie najczęściej wykorzystywanego spośród oferowanych przez Yahoo! interfejsów API. Dokumentacja tej usługi jest dostępna pod adresem: http://developer.yahoo.net/search/ ¦web/V1/webSearch.html. Aby sięgnąć do tej usługi, trzeba będzie wykonać żądanie HTTP pod adres: http://api.search.yahoo.com/WebSearchService/V1/webSearch. Jeśli otworzymy ten adres URL w naszej przeglądarce internetowej, to otrzymamy w odpowiedzi następujący dokument XML:

    The following errors were detected: invalid value: appid (empty or missing) invalid value: query (empty or missing)

    Oczywiście sięgnęliśmy do usługi w prawidłowy sposób, ale nie poinformowaliśmy jej, co ma dla nas wyszukać, dlatego też usługa zwraca nam komunikat o błędzie. Aby otrzymać jakieś użyteczne wyniki, należy dodać jeszcze na końcu adresu URL &query=PEAR — w ten sposób wyszukamy w katalogu Yahoo! termin PEAR. Jeśli teraz wpiszemy tak zmodyfikowany adres URL w okienku adresowym naszej przeglądarki, to otrzymamy następujący dokument XML:

    PEAR :: The PHP Extension and Application Repository

    ... PEAR provides the above mentioned PHP components in the form of so called "Packages". If you would like to download PEAR packages, you can browse the complete ...

    http://pear.php.net/ http://pear.php.net/ 1136534400 text/html

    PEAR :: Package :: PEAR

    ... The PEAR package contains: * the PEAR installer, for creating, distributing ...

    203

    PEAR. Programowanie w PHP

    http://pear.php.net/package/PEAR http://pear.php.net/package/PEAR 1135065600 text/html

    http://216.109.125.130/...1&.intl=us 12366



    Ten dokument XML zawiera informacje o wszystkich witrynach WWW skatalogowanych przez Yahoo!, które zawierają termin PEAR. Dla każdego ze znalezionych elementów otrzymamy z powrotem: tytuł, krótkie podsumowanie, adres URL i odpowiednie informacje buforowania, jeśli Yahoo! oferuje buforowaną wersję tej strony WWW. Spróbujmy teraz zaimplementować skrypt PHP, który wysyłać będzie do usługi to samo wywołanie metody z jej interfejsu API, a następnie z otrzymanych wyników wydobywać będzie tytuł i podsumowanie. W tym celu wykonamy trzy czynności opisane wyżej i skonstruujemy adres żądania URL. Aby wykonać żądanie, użyjemy pakietu HTTP_Request: /** * Korzystamy z pakietu HTTP_Request, by utworzyć połączenie HTTP */

    require_once 'HTTP/Request.php'; // Tworzymy nowy obiekt żądania bazując na adresie URL interfejsu API

    $request = new HTTP_Request('http://api.search.yahoo.com/WebSearchService/V1/webSearch'); // dodajemy parametry żądania

    $request->addQueryString('appid', 'packt-pear-book'); $request->addQueryString('query', 'PEAR'); // Wysyłamy żądania HTTP i przechwytujemy odpowiedź

    $request->sendRequest(); // Sprawdzamy, czy żądanie się udało

    if ($request->getResponseCode() == 200) { // Wyświetlamy otrzymany dokument XML

    echo $request->getResponseBody(); }

    204

    Rozdział 4. • Usługi WWW

    Po utworzeniu instancji nowego obiektu żądania HTTP_Request możemy dodać do niego tyle parametrów GET, ile chcemy, a klasa HTTP_Request automatycznie zakoduje odpowiednio argumenty, tak aby otrzymany adres URL był prawidłowy. Następnie użyjemy metody sendRequest(), by przesłać żądanie do usługi, oraz sprawdzimy kod odpowiedzi w celu ustalenia, czy żądanie zakończyło się sukcesem. Metoda getResponseBody() zapewni nam dostęp do dokumentu XML zwróconego przez usługę. Jak widać, pakiet HTTP_Request ułatwia nam wykonanie dwóch pierwszych z trzech niezbędnych kroków, a wszystko to w zaledwie sześciu wierszach kodu. Kolejnym etapem jest przetworzenie dokumentu XML. W rozdziale 3. poznaliśmy pakiet XML_Unserializer, który potrafi przekształcić dowolny dokument XML na strukturę danych PHP. W tym miejscu wykorzystamy pakiet XML_Unserializer, by pobrać wszystkie dostępne dane z odpowiedzi zwróconej przez usługę i przetworzyć je w naszej aplikacji. /** * Pakiet XML_Unserializer przerobi dla nas kod XML */

    require_once 'XML/Unserializer.php'; $us = new XML_Unserializer(); // pobieramy dane z atrybutów

    $us->setOption(XML_UNSERIALIZER_OPTION_ATTRIBUTES_PARSE, true); // tworzymy tablice

    $us->setOption(XML_UNSERIALIZER_OPTION_COMPLEXTYPE, 'array'); // dekodujemy UTF-8 na ISO-8859-1

    $us->setOption(XML_UNSERIALIZER_OPTION_ENCODING_SOURCE, 'UTF-8'); $us->setOption(XML_UNSERIALIZER_OPTION_ENCODING_TARGET, 'ISO-8859-1'); // Tworzymy tablicę, nawet jeśli zwrócony zostanie tylko jeden wynik

    $us->setOption(XML_UNSERIALIZER_OPTION_FORCE_ENUM, array('Result')); // analizujemy dokument XML

    $result = $us->unserialize($request->getResponseBody()); // sprawdzamy, czy pojawił się jakiś błąd

    if (PEAR::isError($result)) { echo "Błąd: "; echo $result->getMessage(); } // pobieramy wyniki

    $result = $us->getUnserializedData();

    Po utworzeniu instancji obiektu XML_Unserializer będziemy musieli określić kilka opcji wpływających na proces przetwarzania dokumentu XML. Po pierwsze, polecimy obiektowi XML_ ¦Unserializer, aby uwzględniał atrybuty, ponieważ główny węzeł otrzymanego w odpowiedzi

    205

    PEAR. Programowanie w PHP

    dokumentu zawiera parę atrybutów, które mogą być dla nas istotne. Po drugie, zdecydujemy, że obiekt XML_Unserializer powinien tworzyć z zagnieżdżonych znaczników dokumentu tablice, a nie obiekty. Ponieważ Yahoo! zwraca dane w kodowaniu UTF-8, a my wolelibyśmy pracować raczej z kodowaniem ISO-8859-12, polecimy pakietowi XML_Unserializer, by kodował wszystkie dane w atrybutach i węzłach tekstowych używając kodowania ISO-8859-1. Na koniec wreszcie upewniamy się, że znacznik będzie przechowywany zawsze w tablicy indeksowanej, niezależnie od tego, ile razy się pojawi. W ten sposób upewnimy się, że będziemy mieli tablicę, którą będzie można przeglądać w pętli, niezależnie od tego, czy otrzymamy z powrotem kilka, czy tylko jedną stronę wyników. Po określeniu wszystkich opcji przesyłamy dokument XML obiektowi XML_Unserializer i pozwalamy mu wykonać za nas całą robotę. Wartość zwrócona przez metodę unserialize() może sygnalizować błąd, jeśli dokument XML zawierał jakieś błędy, dlatego też zanim przejdziemy dalej, najpierw sprawdzimy, czy pojawił się jakiś błąd PEAR_Error. Jeśli nic nie znajdziemy, to pobierzemy już deserializowane dane. Obecnie będą one miały postać tablicy o następującej strukturze: Array ( [xmlns:xsi] => http://www.w3.org/2001/XMLSchema-instance [xmlns] => urn:yahoo:srch [xsi:schemaLocation] => urn:yahoo:srch http://api.search.yahoo.com/WebSearchService/V1/ WebSearchResponse.xsd [totalResultsAvailable] => 1426310 [totalResultsReturned] => 10 [firstResultPosition] => 1 [Result] => Array ( [0] => Array ( [Title] => PEAR :: The PHP Extension and Application Repository [Summary] => ... PEAR provides the above mentioned PHP components in the form of so called "Packages". If you would like to download PEAR packages, you can browse the complete ... [Url] => http://pear.php.net/ [ClickUrl] => http://pear.php.net/ [ModificationDate] => 1136534400 [MimeType] => text/html ) [1] => Array ( [Title] => PEAR :: Package :: PEAR [Summary] => ... The PEAR package contains: 2

    W przypadku polskich znaków z kodowaniem ISO-8859-2 — przyp. tłum.

    206

    Rozdział 4. • Usługi WWW

    * the PEAR installer, for creating, distributing ... [Url] => http://pear.php.net/package/PEAR [ClickUrl] => http://pear.php.net/package/PEAR [ModificationDate] => 1135065600 [MimeType] => text/html [Cache] => Array ( [Url] => http://216.109.125.130/search/cache?appid=packtpearbook&query=PEAR&ei=UTF-8&u=pear.php.net/ package/PEAR&w=pear&d=aD-jIw0DMFBh&icp=1&.intl=us [Size] => 12366 ) ) ...pozostałe wyniki... )

    Tablica ta zawiera dokładnie te same informacje, co dokument XML, i można ją łatwo przejrzeć w pętli foreach: // przeglądamy wyniki w pętli

    foreach ($result['Result'] as $item) { printf("%s\n", $item['Title']); printf("%s\n\n", $item['Summary']); }

    Jeśli uruchomimy gotowy skrypt, to odszuka on w katalogu Yahoo! strony zawierające termin PEAR i wyświetli ładnie sformatowane wyniki poszukiwań: PEAR :: The PHP Extension and Application Repository ... PEAR provides the above mentioned PHP components in the form of so called "Packages". If you would like to download PEAR packages, you can browse the complete ... PEAR :: Package :: PEAR ... The PEAR package contains: * the PEAR installer, for creating, distributing ... Pear - Wikipedia, the free encyclopedia From Wikipedia, the free encyclopedia. Pear. Genus: Pyrus. L. Pears are trees of the genus Pyrus and the fruit of that tree, edible in some species. ... are important for edible fruit production, the European Pear Pyrus communis cultivated mainly in Europe and North ... also known as Asian Pear or Apple Pear), both grown mainly in ...

    W podobny sposób można za pomocą pakietów HTTP_Request i XML_Unserializer sięgać do praktycznie każdej usługi WWW opartej na REST. Musimy tylko przestudiować dokumentacje usługi, aby wiedzieć, jakie informacje należy jej przesłać w żądaniu.

    207

    PEAR. Programowanie w PHP

    W pierwszej części tego rozdziału dowiedzieliśmy się, jak sięgać do usług WWW opartych na protokołach XML-RPC i SOAP, a następnie omówiliśmy używanie implementacji klientów oferowanych przez repozytorium PEAR dla usług WWW korzystających z własnych interfejsów API. W drugiej części rozdziału zajmiemy się oferowaniem usług WWW za pomocą kilku pakietów dostępnych w repozytorium PEAR.

    Oferowanie usług WWW Teraz gdy wiemy już, jak korzystać z różnych usług WWW za pomocą języka PHP i oprogramowania z repozytorium PEAR, czytelników interesuje zapewne, w jaki sposób oferować użytkownikom własną usługę WWW. Dlatego też w drugiej części tego rozdziału skorzystamy z pakietu XML_RPC, by za jego pomocą zaoferować użytkownikom prostą usługę, z której bez problemu będą mogły korzystać różne klienty i różne języki programowania. Co więcej, zaoferujemy dokładnie te same funkcje, jak usługa SOAP, i przekonamy się również, jak łatwo współpracuje się z protokołem SOAP i językiem WSDL, gdy używamy PHP5 i pakietu Services_Webservice. Na koniec wreszcie zaoferujemy użytkownikom usługę WWW opartą na protokole REST, korzystając tylko i wyłącznie z serwera WWW, języka PHP oraz pakietu XML_ Serializer.

    Oferowanie usług WWW opartych na protokole XML-RPC W pierwszej części tego rozdziału rozpoczęliśmy naszą wędrówkę po świecie usług WWW od przygotowania klienta XML-RPC. Logiczne więc wydaje się, że drugą część niniejszego rozdziału najlepiej byłoby rozpocząć od zbudowania usługi XML-RPC, z której korzystać będzie mógł odpowiedni klient XML-RPC. Usługę XML-RPC można bez trudu przygotować wykorzystując ten sam pakiet XML-RPC, którego użyliśmy wcześniej do przygotowania klienta. Pakiet ten nie tylko zawiera funkcje umożliwiające budowanie komunikatów żądań i analizowanie otrzymywanych z powrotem komunikatów odpowiedzi, ale również oferuje analogiczne funkcje umożliwiające zbudowanie po przeciwnej stronie odpowiedniego serwera: „ Po pierwsze, serwer musi pobrać dokument XML wysłany przez klienta z metody POST żądania HTTP. „ Po drugie, dokument XML musi zostać przetworzony w taki sposób, aby wydobyć z niego nazwę przywoływanej funkcji lub metody oraz przesyłane jej parametry. „ Na koniec wreszcie trzeba w jakiś sposób przywołać odpowiednią metodę, a następnie wartości zwracane przez nią powinny zostać zakodowane w odpowiednim dokumencie XML zgodnym ze standardem protokołu XML-RPC, aby można było ten dokument odesłać klientowi.

    208

    Rozdział 4. • Usługi WWW

    Zanim jednak przejdziemy do szczegółowego omawiania, jak tworzyć usługę XML-RPC, musimy najpierw zaimplementować jakieś funkcje, które nasza usługa WWW będzie oferować. Jako że przykład ten dotyczy protokołu XML-RPC, postaramy się, aby nasza przykładowa funkcja była możliwie jak najprostsza. Dlatego też powrócimy do przykładowego dokumentu z etykietami albumów muzycznych, omawianego w rozdziale 3. Wszystkie informacje niezbędne do wyświetlenia danych o albumie przechowywane są w pliku record-label.php: /** * Lista dostępnych artystów i nagranych * przez nich albumów * * W prawdziwej aplikacji zastąpilibyśmy listę * bazą danych */

    $records = array('Elvis Presley' => array('That\'s All Right (Mama) & Blue Moon Of Kentucky', 'Good Rockin\' Tonight', ), 'Carl Perkins' => array( 'Gone, Gone, Gone' ) ); /** * pobieramy wszystkie albumy artysty * * @access public * @param string Nazwisko artysty * @return tablica (tablic|wartości logicznych) prezentująca wszystkie albumy, * lub wartość false, jeśli dany artysta nie istnieje */

    function getRecords($artist) { // W prawdziwej aplikacji użylibyśmy zapytania na bazie danych

    global $records; if (isset($records[$artist])) { return $records[$artist]; } return false; }

    Jest to bardzo prosta implementacja funkcji zwracającej wszystkie albumy nagrane przez danego artystę. Artyści i nagrane przez nich albumy przechowywane są dla uproszczenia w globalnej tablicy. Gdybyśmy tworzyli prawdziwą aplikację przygotowaną dla studia nagraniowego, lepiej byłoby przechowywać te informacje w bazie danych, aby właściciele studia mogli łatwo edytować informacje na temat artystów i ich nagrań. Na potrzeby naszego przykładu kod ten będzie jednak w zupełności wystarczający. Jeśli przywołamy funkcję getRecords() i prześlemy jej nazwisko artysty (na przykład Elvis Presley), to otrzymamy następujące wyniki:

    209

    PEAR. Programowanie w PHP

    Array ( [0] => That's All Right (Mama) & Blue Moon Of Kentucky [1] => Good Rockin' Tonight )

    Jeśli natomiast przesłalibyśmy nazwisko jakiegoś nieistniejącego artysty, to funkcja zwróciłaby wartość false. Teraz gdy zakończyliśmy już implementowanie logiki biznesowej naszej aplikacji, możemy przystąpić do implementowania serwera XML-RPC. Pakiet XML-RPC oferuje w tym celu specjalną klasę serwera XML-RPC-Server, która wykona za nas większość niezbędnej pracy. Klasa ta przetworzy odpowiednio komunikat wysłany przez klienta i konwertuje go na obiekt XML_RPC_ Message (dokładnie taki sam obiekt komunikatu tworzyliśmy za pomocą pakietu XML_RPC po stronie klienta). Następnie wydobędzie z komunikatu nazwę funkcji, którą należy wywołać, i sprawdzi, czy zarejestrowaliśmy funkcję PHP o nazwie odpowiadającej nazwie wydobytej funkcji. Jeśli tak, to przywołamy tę funkcję PHP, przesyłając jej jako argument obiekt XML_RPC_ Message. Oznacza to, że funkcja, którą implementujemy, musi być w stanie współpracować z argumentem XML_RPC_Message. Niestety nasza funkcja getRecords() oczekuje, że jako argument prześlemy jej łańcuch. Aby utworzyć nową usługę, będziemy więc musieli naszą obecną logikę biznesową obudować za pomocą funkcji getRecordsService(), która pobierać będzie z komunikatu XML_RPC_Message parametr artysty i przywoła funkcję getRecords(), przesyłając jej w argumencie łańcuch: function getRecordsService($args) { $artist = $args->getParam(0)->scalarval(); $records = getRecords($artist); }

    Po przywołaniu funkcji getRecords() zmienna $records powinna zawierać albo tablicę z danymi, albo zwrócić wartość false, jeśli nie udało się znaleźć takiego artysty. Moglibyśmy spróbować po prostu zwrócić tę wartość i mieć nadzieję, że reszta usługi będzie działać automatycznie. Niestety tak jednak nie jest. Będziemy musieli włożyć odrobinę pracy i zakodować wartość zwracaną przez funkcję jako wartość XML_RPC_Value, by na koniec wreszcie zamknąć tę wartość w odsyłanym w podpowiedzi komunikacie XML_RPC_Response: function getRecordsService($args) { $artist = $args->getParam(0)->scalarval(); $records = getRecords($artist); $val = XML_RPC_encode($records); $response = new XML_RPC_Response($val); return $response; }

    210

    Rozdział 4. • Usługi WWW

    Całość przebiega dokładnie tak samo jak kodowanie wartości po stronie klienta i tworzenie nowego komunikatu. Teraz pozostało nam tylko utworzyć nowy serwer i zarejestrować przygotowaną funkcję obudowującą jako funkcję XML-RPC. Oto kod niezbędny do przygotowania kompletnego serwera: /** * Dołączamy właściwą logikę biznesową */

    require_once 'record-label.php'; /** * Dołączamy klasę serwera XML-RPC */

    require_once 'XML/RPC/Server.php'; /** * Obudowanie XML-RPC dla tej logiki biznesowej * * @access public * @param XML_RPC_Message Komunikat wysyłany przez klienta * @return XML_RPC_Response Kodowana odpowiedź serwera */

    function getRecordsService($args) { $artist = $args->getParam(0)->scalarval(); $records = getRecords($artist); $val = XML_RPC_encode($records); $response = new XML_RPC_Response($val); return $response; } // mapujemy nazwy metod XML-RPC na funkcje PHP

    $map = array( 'label.getRecords' => array( 'function' => 'getRecordsService' ) ); // tworzymy i uruchamiamy usługę

    $server = new XML_RPC_Server($map);

    Tablica $map, która przesyłana jest konstruktorowi klasy serwera XML_RPC_Server, służy do mapowania udostępnianych publicznie przez usługę metod RPC na odpowiednie funkcje PHP. Serwer przesyłać będzie tej funkcji, jako jedyny argument, odebrany od klienta komunikat XML_RPC_Message. Po przygotowaniu naszego pierwszego serwera warto sprawdzić, czy działa on tak, jak zakładamy. Okazuje się, że nie, jeśli bowiem wpiszemy w przeglądarce adres serwera usługi, otrzymamy następujący komunikat o błędzie: faultCode 105 faultString XML error: Invalid document end at line 1

    211

    PEAR. Programowanie w PHP

    Jeśli przyjrzymy się kodowi źródłowemu strony, to przekonamy się, że ma ona postać dokumentu XML, który nasza przeglądarka traktuje jak kod HTML:



    faultCode 105

    faultString XML error: Invalid document end at line 1





    Ten dokument XML sygnalizuje, że serwer XML-RPC chce wysłać do klienta komunikat o błędzie, ponieważ nie został zaprogramowany do współpracy ze zwykłą przeglądarką internetową, a tylko ze specjalnym klientem XML-RPC. Dlatego też aby przetestować naszą nową usługę, musimy najpierw zaimplementować dla niej klienta. Jak się już przekonaliśmy, można to łatwo zrobić korzystając z pomocy pakietu XML_RPC: require_once 'XML/RPC.php'; $client = new XML_RPC_Client('/record-label.php', 'localhost'); $params = array(

    new XML_RPC_Value('Elvis Presley', 'string') ); $message = new XML_RPC_Message('label.getRecords', $params); $response = $client->send($message); if ($response->faultCode()) { echo "Nie mogę skorzystać z usługi XML-RPC.\n"; echo $response->faultString(); exit(); } $value = $response->value(); $records = XML_RPC_decode($value); print_r($records);

    212

    Rozdział 4. • Usługi WWW

    Należy upewnić się, że dopasowaliśmy ścieżkę i nazwę hosta w konstruktorze klasy XML_RPC_ Client, tak aby pasowała do naszej lokalnej konfiguracji. Teraz gdy uruchomimy przygotowany skrypt klienta, przywoła on metodę getRecords() i prześle jej jako parametr nazwisko Elvis Presley. Skrypt ten powinien zwrócić następujące informacje: Array ( [0] => That's All Right (Mama) & Blue Moon Of Kentucky [1] => Good Rockin' Tonight )

    Obsługa błędów Jeśli spróbujemy w żądaniu zastąpić przywoływaną obecnie metodę jakaś inną, której nie zaimplementowaliśmy w usłudze, to usługa automatycznie zwróci błąd, który zostanie przechwycony przez klienta. Błąd ten będzie można sprawdzić za pomocą metody faultCode(). Dlatego jeśli w kodzie skryptu klienta zmienimy jeden wiersz: $message = new XML_RPC_Message('label.getArtists', $params);

    to skrypt zwróci następujące informacje o błędzie: Could not use the XML-RPC service. Unknown method

    Ponieważ nie zaimplementowaliśmy takiej metody, usługa automatycznie zwraca odpowiednie informacje o błędzie bez żadnego wkładu pracy ze strony programisty. (Nie można skorzystać z usługi XML-RPC. Nieznana metoda). Należy jednak pamiętać, że użytkownik korzystający z klienta nadal może popełnić jakiś błąd, nawet jeśli klient przywołuje właściwą metodę. Oto przykład literówki w przesyłanym serwerowi nazwisku artysty: $params = array( new XML_RPC_Value('Elvis Prely', 'string') ); $message = new XML_RPC_Message('label.getRecords', $params);

    Jeśli uruchomimy ten skrypt, to wynik jego działania będzie następujący: 0

    Dzieje się tak dlatego, że oryginalna metoda getRecords(), gdy otrzyma nazwisko nieistniejącego artysty, zwraca wartość false, a ponieważ język PHP nie przestrzega restrykcyjnie typów danych, zwróci klientowi wartość 0. Oczywiście lepiej byłoby zasygnalizować błąd w serwerze XML-RPC, tak by mógł on być również przechwycony przez klienta. Sygnalizowanie błędu można wykonać używając innej sygnatury w konstruktorze klasy odpowiedzi XML_RPC_Response. Zamiast przesyłać informację w obiekcie XML_RPC_Value, prześlemy konstruktorowi jako pierwszy parametr 0, a następnie kod błędu i tekstowy komunikat opisujący błąd:

    213

    PEAR. Programowanie w PHP

    function getRecordsService($args) { $artist = $args->getParam(0)->scalarval(); $records = getRecords($artist); if ($records === false) { $response = new XML_RPC_Response(0, 50, 'Artysty "'.$artist.'" nie ma w naszej bazie danych.'); } else { $val = XML_RPC_encode($records); $response = new XML_RPC_Response($val); } return $response; }

    Jeśli uruchomimy teraz po wprowadzonych zmianach skrypt klienta, to zwróci on następujący komunikat: Could not use the XML-RPC service. Artysty "Elvis Prely" nie ma w naszej bazie danych.

    W ten sposób klient dowie się znacznie więcej o przyczynach błędu, niż gdyby skrypt zwracał po prostu 0. Pozostał nam jednak jeszcze jeden problem. Wyobraźmy sobie, że klient przywołuje metodę, nie przesyłając jej w ogóle żadnych parametrów: $message = new XML_RPC_Message('label.getRecords');

    W efekcie otrzymamy następujący komunikat informujący, że klientowi nie udało się skorzystać z usługi XML-RPC: Could not use the XML-RPC service. Invalid return payload: enable debugging to examine incoming payload

    Jeśli zgodnie z zaleceniami komunikatu uruchomimy w kliencie debugowanie (zwracanie bardziej szczegółowych komunikatów o błędach), korzystając w tym celu z wywołania $client>setDebug(1), to otrzymamy bardziej wyczerpujące informacje na temat źródła problemu: Fatal error: Call to undefined method XML_RPC_Response:: scalarval() in /var/www/record-label.php on line 21


    Jak widać, próbowaliśmy przywołać metodę slacarval() dla wartości XML_RPC_Value, która nie została przesłana w żądaniu. Problem ten można byłoby bez trudu rozwiązać sprawdzając, czy został przesłany odpowiedni parametr, i w przeciwnym wypadku sygnalizując problem; niemniej istnieje też znacznie prostszy sposób zautomatyzowania obsługi tego błędu. Kiedy tworzymy mapę rozsyłania (ang. dispatch map) dla usługi XML-RPC, to oprócz definiowania dla każdej metody tejże usługi odpowiedniej funkcji PHP, możemy również określić dla metody odpowiednią sygnaturę metody:

    214

    Rozdział 4. • Usługi WWW

    $map = array('label.getRecords' => array( 'function' => 'getRecordsService', 'signature' => array( array('array', 'string'), ), 'docstring' => 'Pobiera wszystkie albumy danego artysty.' ) );

    Sygnatura ma postać tablicy i możliwe jest przeciążenie metody i użycie jej w połączeniu z inną sygnaturą. Dla każdego wariantu musimy określić odpowiedni typ zwracanych informacji (w tym przypadku tablicę) oraz parametry, które metoda akceptuje. Ponieważ nasza metoda pozwala na przesyłanie jej tylko łańcuchów, zdefiniujemy tylko jedną sygnaturę metody. Teraz gdy ponownie uruchomimy klienta, pojawi się nowy, znacznie bardziej użyteczny komunikat o błędzie, informujący o tym, że metodzie przesłana została nieprawidłowa liczba parametrów (sygnatura wymagała przesłania jednego parametru, a nie został przesłany żaden): Could not use the XML-RPC service. Incorrect parameters passed to method: Signature permits 1 parameters but the request had 0

    Czytelnicy zauważyli już prawdopodobnie dodatkową pozycję docstring, którą dodaliśmy w ostatnim przykładzie do mapy rozsyłania. Została ona dodana, aby pokazać kolejną użyteczną funkcję klasy XML_RPC_Server — dodaje ona mianowicie do każdej usługi XML-RPC kilka funkcji, które umożliwiają introspekcję (czyli badanie działania) usługi. Dzięki temu za pomocą dowolnego klienta XML-RPC będziemy mogli pobrać na przykład listę metod oferowanych przez daną usługę: require_once 'XML/RPC.php'; $client = new XML_RPC_Client('/record-label.php', 'localhost'); $message = new XML_RPC_Message('system.listMethods'); $response = $client->send($message); $value = $response->value(); $methods = XML_RPC_decode($value); print_r($methods);

    Jeśli uruchomimy ten skrypt, to wyświetli on listę wszystkich metod oferowanych przez usługę: Array ( [0] => label.getRecords [1] => system.listMethods

    215

    PEAR. Programowanie w PHP

    [2] => system.methodHelp [3] => system.methodSignature )

    W podobny sposób można również uzyskać więcej informacji na temat każdej z obsługiwanych metod. I właśnie w tym celu dodaliśmy do naszej mapy rozsyłania właściwość docstring: $message = new XML_RPC_Message('system.methodHelp', array(new XML_RPC_Value( 'label.getRecords'))); $response = $client->send($message); $value = $response->value(); $help = XML_RPC_decode($value); echo $help;

    Po uruchomieniu tego skryptu wyświetli się tekst pomocy, który przygotowaliśmy dla metody label.getRecords(). Zawsze kiedy implementujemy usługę opartą na protokole XML-RPC, powinniśmy dodawać do mapy rozsyłania takie informacje, które ułatwią użytkownikom i programistom korzystanie z naszej usługi. Teraz gdy wiemy już wszystko, co trzeba, by przygotować za pomocą pakietu XML_RPC naszą własną usługę XML-RPC, będziemy mogli tworzyć usługi WWW dostępne dla wielu różnych użytkowników, aplikacji i języków programowania.

    Oferowanie usług WWW opartych na protokole SOAP Dowiedzieliśmy się już, jak tworzyć usługę WWW opartą na protokole XMLRPC, pora więc teraz na następny krok — nauczymy się, jak tworzyć usługę WWW opartą na protokole SOAP. Przed pojawieniem się PHP5 przygotowanie takiej usługi było bardzo trudne, na szczęście w wersji 5 język PHP oferuje rozszerzenie SOAP, które wykonuje większość niewdzięcznej pracy za programistę. Korzystaliśmy już z tego rozszerzenia we wcześniejszej części rozdziału, jako że pakiet Services_Google jest w zasadzie tylko obudowaniem rozszerzenia ext/soap, dostosowującym je nieco do potrzeb usługi WWW Google. Skoro język PHP dostarcza już rozszerzenia obsługującego protokół SOAP, czytelnicy mogliby się zastanawiać, do czego potrzebny jest w takim razie specjalny pakiet PHP. Cóż, jedną z największych słabości obecnego rozszerzenia SOAP jest to, że nie jest w stanie tworzyć dokumentów WSDL z istniejącego kodu PHP. WSDL to skrót od Web Service Describing Language (Język Opisu Usług WWW) i jest to specjalny format XML wykorzystywany do opisu usług WWW opartych na protokole SOAP. Dokument WSDL zawiera informacje na temat metod oferowanych przez usługę WWW i sygnatur tych metod oraz informacje o przestrzeni nazw, z której należy skorzystać, i wreszcie o tym, gdzie można znaleźć samą usługę. Pisanie

    216

    Rozdział 4. • Usługi WWW

    dokumentów WSDL ręcznie jest bardzo żmudne i wiąże się z ryzykiem popełnienia wielu błędów, zawierają one bowiem wiele informacji, których nie łatwo domyślić się intuicyjnie, a ponadto często są bardzo długie. Na przykład dokument WSDL opisujący usługę Google ma ponad 200 wierszy długości, mimo iż Google udostępnia w swojej usłudze WWW jedynie trzy metody. Wszystkie informacje zawarte w dokumencie WSDL można łatwo wydobyć z kodu PHP lub z dokumentacji kodu PHP, więc wpisywanie ich ręcznie byłoby w znacznej mierze niepotrzebnym powielaniem już wykonanej pracy. Większość współczesnych języków programowania obsługuje już automatyczne generowanie dokumentów WSDL i dzięki pakietowi Services_ Webservice repozytorium PEAR wprowadza tę funkcję również do języka PHP. Mimo iż jest to dość niedawno przygotowany pakiet oprogramowania, implementowanie usług WWW staje się jednak dzięki niemu o wiele prostsze. Celem pakietu Services_Webservice jest maksymalne zautomatyzowanie tworzenia usług WWW. Ponadto warto wiedzieć, że pakiet ten stosuje rozwiązanie bazujące na sterownikach, tak więc bardzo prawdopodobne, że w przyszłości obsługiwać będzie nie tylko protokół SOAP, ale również XML-RPC i być może nawet protokół REST. Obecnie jednak obsługuje niestety tylko protokół SOAP. Korzystając z pakietu Services_Webservice nie musimy zupełnie przejmować się wewnętrznymi szczegółami działania protokołu SOAP. Wystarczy tylko zaprogramować logikę biznesową aplikacji i przesłać ja pakietowi, który automatycznie przygotuje dla nas całą usługę WWW. Ponieważ protokół SOAP jest przeważnie wykorzystywany w połączeniu z językami obiektowymi, a repozytorium PEAR oferuje właśnie głównie kod obiektowy, więc pakiet Services_ Webservice oczekuje, że przesłana mu logika biznesowa usługi zostanie obudowana w klasach. Oznacza to, że przede wszystkim musimy przygotować nową implementację logiki biznesowej. Tak jak poprzednio, użyjemy przykładu z albumami muzycznymi. Sporą część kodu zaczerpnęliśmy wprost z przykładu usługi XML-RPC, ujęliśmy wszystko w jednej klasie RecordLabel, która następnie powinna zostać zachowana w pliku RecordLabel.php: /** * Oferuje różne metody umożliwiające sięganie * do danych albumów wydanych przez studio. */

    class RecordLabel { /** * Wszystkie nasze albumy. * * W prawdziwej aplikacji pobieralibyśmy * dane z bazy danych. */

    private $records = array( 'Elvis Presley' => array( 'That\'s All Right (Mama) & Blue Moon Of Kentucky', 'Good Rockin\' Tonight', ),

    217

    PEAR. Programowanie w PHP

    'Carl Perkins' => array( 'Gone, Gone, Gone' );

    ) /** * pobieramy wszystkie albumy danego artysty * * @param string * @return string[] */

    public function getRecords($artist) { $result = array(); if (isset($this->records[$artist])) { $result = $this->records[$artist]; } return $result; } /** * Pobieramy wszystkich artystów, z którymi mamy kontrakty * * @return string[] */

    }

    public function getArtists() { return array_keys($this->records); }

    Tak jak w poprzednim przykładzie usługi, również i tutaj przechowujemy dane w prostej tablicy w prywatnej właściwości klasy. Nasza nowa klasa RecordLabel oferuje dwie metody getArtists() i getRecords(). Metody te służą do pobierania artystów i albumów i są zrozumiałe same przez się. Dodaliśmy jednak do wszystkich metod i do samej klasy komentarze PHPDoc, ponieważ pakiet Services_Webservice z nich korzysta. Jeśli przyjrzeć się im bliżej, zauważymy komentarz, który na pierwszy rzut oka może się wydawać odrobinę dziwny. Obie metody zwracają proste tablice PHP, natomiast blok komentarza PHPDoc informuje, że zwracane dane są typu string[]. Dzieje się tak dlatego, że protokół SOAP został zaprojektowany po to, aby umożliwiać komunikację pomiędzy różnymi językami programowania, natomiast język PHP podchodzi do typów w sposób bardzo luźny. Tablica PHP może więc zawierać wartości łańcuchowe, liczby całkowite, obiekty, a nawet inne tablice. Swoboda taka jest natomiast niedopuszczalna w bardziej restrykcyjnych językach, takich jak Java, gdzie tablica może zawierać tylko wartości jednego i tego samego typu. Jeśli utworzymy tablicę w języku Java, to będziemy musieli poinformować kompilator, jakiego typu dane tablica ta będzie zawierać. Aby umożliwić komunikację między różnymi językami programowania, protokół SOAP ustanawia pewne reguły, które muszą być przestrzegane przez wszystkie implementacje protokołu SOAP, i dlatego właśnie nasza implementacja metod getRecords() i getArtists() napisana w języku PHP musi przyjąć ograniczenie, że metody będą zwracać tablice, które mogą zawierać wyłącznie łańcuchy. Składnia bloku komentarza została zapożyczona z języka Java, podobnie jak dodawanie na końcu typu nawiasów kwadratowych [], by utworzyć tablicę zawierającą dane tego typu. 218

    Rozdział 4. • Usługi WWW

    Poza tym kod ten wygląda dokładnie tak samo jak jakikolwiek inny standardowy obiektowy kod napisany w PHP5. Nie widać w nim nawet zupełnie śladu usług WWW. Niemniej można go wykorzystać do utworzenia nowej usługi WWW, i to wpisując nie więcej niż dziesięć wierszy kodu, co resztą udowadnia kolejny przykład: // Załączamy logikę biznesową

    require_once 'RecordLabel.php'; // Załączamy pakiet

    require_once 'Services/Webservice.php'; // Załączamy opcje SOAP

    $options = array( 'uri' => 'http://www.my-record-label.com', 'encoding' => SOAP_ENCODED ); // Tworzymy nową usługę WWW

    $service = Services_Webservice::factory('SOAP', 'RecordLabel', 'http://www.my-record-label.com', $options); $service->handle();

    Po dodaniu logiki biznesowej i klasy Services_Webservice pozostanie nam tylko określić dwie opcje protokołu SOAP: „ Przestrzeń nazw, która będzie jednoznacznie identyfikować naszą usługę WWW. „ Kodowanie, którego chcemy używać w naszej usłudze WWW. Po określeniu tych opcji użyjemy metody fabryki (factory) klasy Services_Webservice, by utworzyć nową usługę WWW. Metodzie fabryki prześlemy następujące argumenty: „ Typ usługi WWW, którą chcemy utworzyć (obecnie obsługiwane są tylko usługi SOAP). „ Nazwę klasy, która udostępnia metody usługi (może to być również instancja tej klasy). „ Wykorzystywaną przestrzeń nazw. „ Tablicę zawierającą specjalne opcje usługi WWW. Metoda fabryki zwróci nową instancję klasy Services_Webservice_SOAP, którą będzie można bez trudu uruchomić używając metody handle(). Jeśli otworzymy ten skrypt w przeglądarce, to automatycznie wygeneruje on stronę pomocy opisującą naszą usługę, tak jak to pokazuje rysunek 4.2. Pakiet Services_Webservice automatycznie pobrał informacje z bloków komentarzy (bloków doc), by wyświetlić informacje na temat usługi WWW (pobrane z bloku komentarza docblok na poziomie klasy) oraz informacje na temat każdej z metod oferowanych przez usługę. Strona pomocy zawiera również dwa łącza: jeden do odpowiedniego dokumentu WSDL, a drugi do odpowiedniego dokumentu DISCO. Dokument DISCO jest to dokument XML, który zawiera informacje, gdzie można znaleźć dokumenty WSDL opisujące daną usługę WWW.

    219

    PEAR. Programowanie w PHP

    Rysunek 4.2. Informacje o klasie RecordLabel

    Pakiet Services_Webservice generuje oba te dokumenty automatycznie i możemy do nich sięgać, dodając po prostu do adresu URL naszego skryptu końcówki ?wsdl lub ?DISCO. Od tej pory z naszej usługi będzie mógł korzystać każdy klient, który obsługuje usługi WWW oparte na protokole SOAP. Oczywiście warto przetestować usługę, zanim udostępnimy ją publicznie. Na szczęście jednak ponieważ pakiet Services_Webservice generuje odpowiedni dokument WSDL, możemy to zrobić bardzo łatwo. Oto testowy skrypt, który wykorzystuje rozszerzenie SOAP języka PHP5: $client = new SoapClient('http://localhost/record-label.php?wsdl'); $artists = $client->getArtists(); print_r($artists);

    Nowe rozszerzenie SOAP potrafi generować obiekty pośredników proxy PHP dla usługi WWW opartej na protokole SOAP wprost z dokumentu WSDL. Aby zwiększyć wydajność generowania pośredników proxy, dokument WSDL jest nawet dodatkowo buforowany po tym, jak został przeanalizowany po raz pierwszy. Korzystając z przeciążania „magicznej” metody _call() możemy przywoływać dla tego pośrednika proxy dowolną metodę, która została zaimplementowana w klasie wykorzystywanej przez serwer. Rozszerzenie SOAP przechwyci wywołanie metody, zakoduje je w dokumencie XML i wyśle do serwera, który dopiero wywoła odpowiednią metodę. Jeśli więc uruchomimy ten skrypt, zwróci on, tak jak oczekiwaliśmy, następujące informacje:

    220

    Rozdział 4. • Usługi WWW

    Array ( [0] => Elvis Presley [1] => Carl Perkins )

    Możemy też przywołać drugą metodę, która jest implementowana dokładnie w taki sam sposób: $client = new SoapClient('http://localhost/record-label.php?wsdl'); $artists = $client->getArtists(); foreach ($artists as $artist) { echo "$artist recorded:\n"; $records = $client->getRecords($artist); foreach ($records as $record) { echo "...$record\n"; } }

    W pisanych przez nas skryptach nie musimy się w ogóle przejmować mechanizmami działania protokołu SOAP. Wystarczy zaprogramować w serwerze odpowiednią logikę biznesową, zupełnie tak, jakbyśmy zamierzali z niej korzystać lokalnie, u klienta, i klient będzie mógł sięgać do danych tak, jakby pracował z obiektem RecordLabel.

    Zarządzanie błędami Do tej pory nie przejmowaliśmy się sygnalizowaniem błędów przesyłanych przez serwer, jednak jak łatwo się przekonać, można to zaprogramować bez trudu. Załóżmy, że chcemy zasygnalizować błąd, jeśli jakiś użytkownik będzie próbował pobrać informacje o albumach artysty, którego nie ma w naszej „bazie” danych. Wszystko, co trzeba będzie zrobić, to tylko zmienić odrobinę logikę biznesową serwera, aby w takiej sytuacji zwracał wyjątek: /** * Get all records of an artist * * @param string * @return string[] */

    public function getRecords($artist) { if (isset($this->records[$artist])) { $result = $this->records[$artist]; return $result; } else {

    221

    PEAR. Programowanie w PHP

    throw new SoapFault(50, 'The artist "'.$artist.'" is not in our database.'); } }

    Wyjątek SoapFault zostanie przesłany do klienta, gdzie można bez trudu przechwycić błąd: try { $records = $client->getRecords('Foo'); } catch (SoapFault $f) { echo "Wystąpił błąd.\n"; echo $f; }

    Oprócz wydobywania zawartości bloków dokumentacji i sygnatur metod, pakiet Services_ Webservice jest również w stanie korzystać z innych informacji zawartych w blokach doc. Jeśli nie zaleca się korzystania z danej metody, ponieważ dostępna jest inna, nowsza metoda, to wystarczy do bloku doc dodać komentarz @deprecated: /** * Pobieramy wszystkie albumy Elvisa. * * @return string[] * @deprecated Zamiast niej należy używać getRecords() */

    public function getRecordsByElvis() { return $this->getRecords('Elvis Presley'); }

    Metoda ta zostanie wtedy oznaczona jako „niezalecana” (ang. deprecated) także na wyświetlanej stronie pomocy. Na koniec wreszcie można ukryć pewne metody przed usługą WWW. Umożliwi to nam zaimplementowanie pomocniczych metod, które będzie można przywołać tylko z serwera usługi. /** * Pomocnicza metoda, która nie bedzie widziana przez usługę WWW. * * @webservice.hidden */

    public function doSomeHelperStuff() { // bez kodu, ta metoda to tylko atrapa

    }

    Dzięki tym wszystkim możliwościom oferowanym przez rozszerzenie SOAP dla PHP5 i pakiet Services_Webservice korzystanie z usług WWW opartych na protokole SOAP okazuje się nawet prostsze niż używanie usług opartych na protokole XML-RPC.

    222

    Rozdział 4. • Usługi WWW

    Oferowanie usług opartych na protokole REST za pomocą pakietu XML_Serializer Teraz gdy wiemy już, jak przygotowywać usługi WWW oparte na protokołach XML-RPC i SOAP, dzięki którym możliwości naszej usługi WWW będą dostępne dla każdego użytkownika podłączonego do internetu, czytelnicy mogą się zastanawiać, który z tych standardów jest wygodniejszy. Protokół SOAP jest bardzo złożony i tworzy rozbudowany kod XML, który będzie bardziej obciążać połączenie, a zatem wydłużać czas potrzebny naszej usłudze na udzielenie odpowiedzi. Dla wielu firm fakty te mają znaczenie i dlatego ostatnio coraz bardziej zyskuje na popularności prostszy protokół REST. Dlatego też, o ile nie są nam potrzebne pewne specjalne możliwości protokołu SOAP, takie jak współpraca z różnymi językami programowania czy zdolność do automatycznego generowania klientów za pomocą WSDL, to prostszy protokół REST może okazać się znacznie lepszym rozwiązaniem dla naszej firmy. REST korzysta z wypróbowanych technologii protokołu HTTP i języka XML nie dodając do nich własnej rozbudowanej składni. Zamiast kodowania parametrów naszego żądania w skomplikowanym dokumencie XML, korzysta on z opcji dostarczanej już przez standard HTTP — parametrów zakodowanych w adresie URL. Przywoływanie zdalnej metody w protokole XML-RPC (który i tak jest znacznie prostszy niż protokół SOAP) wymaga użycia następującego kodu:

    label.getRecords

    Elvis Presley



    Natomiast za pomocą REST to samo wywołanie metody można wykonać po prostu używając adresu URL, podobnego do przedstawionego niżej: http://www.example.com/rest/label/getRecords?artist=Elvis+Presley

    Użyliśmy słowa podobnego, ponieważ protokół REST określa tylko ogólne zasady, nie narzucając naszej usłudze WWW żadnych ścisłych reguł. Dlatego też to samo wywołanie metody można wykonać również używając jednego z poniższych adresów URL: http://www.example.com/index.php?method=label.getRecords&artist=El vis+Presley http://www.example.com/label/getRecords/Elvis+Presley

    Sposób kodowania wywołań metod w adresie URL pozostawiony został w gestii dostarczyciela usługi.

    223

    PEAR. Programowanie w PHP

    Oba zaprezentowane tutaj wywołania metody oferowanej przez usługę WWW zawierają praktycznie te same informacje: „ Nazwę wywoływanej metody (label.getRecords lub /label/getRecords). „ Parametr przesyłany wywoływanej metodzie (Elvis Presley). Jedyna różnica polega na tym, że wersja przygotowana dla protokołu XML-RPC zawiera również informacje o typie przesyłanych wartości parametrów, podczas gdy wersja REST przesyła tylko samą wartość. Niemniej ponieważ języki programowania dynamicznie zgadujące typy zyskują na popularności, nie jest to znowu aż tak wielkim problemem. Kodowanie wywołania metody na sposób właściwy dla protokołu REST jest natomiast znacznie prostsze niż kodowanie tego samego wywołania w protokole XML-RPC. Przyjrzyjmy się odpowiedziom. Odpowiedź serwera na przedstawione wcześniej wywołanie XML-RPC będzie wyglądać mniej więcej tak:





    That's All Right (Mama) & Blue Moon Of Kentucky

    Good Rockin' Tonight





    Tak jak poprzednio, jest to dość długi kod XML, który trzeba będzie przygotować, przesłać za pośrednictwem sieci i przeanalizować u klienta. A wszystko po to, by przesłać prostą indeksowaną tablicę zawierającą tylko dwa łańcuchy. Przyjrzyjmy się odpowiedzi na takie samo wywołanie metody usługi WWW, tym razem jednak przygotowane przy użyciu REST:

    That's All Right (Mama) & Blue Moon Of Kentucky Good Rockin' Tonight

    224

    Rozdział 4. • Usługi WWW

    Chociaż tym razem odpowiedź zawiera tylko surowe dane bez żadnych danych o typie, to nadal zawiera te same informacje, oszczędza zasoby komputera i łącza sieciowe, a ponadto jest bardziej zrozumiała dla człowieka. Ponieważ w tym przypadku dokument ZML z odpowiedzią nie musi przestrzegać żadnych specjalnie określonych reguł, możemy po prostu przygotować i dostarczyć następujący dokument:



    Jedyne zasady, które obowiązują w przypadku odpowiedzi REST, są takie, że muszą one mieć postać poprawnego dokumentu XML i użytkownicy usługi powinni wiedzieć, co usługa odsyła im w odpowiedzi. Oczywiście trzeba będzie udokumentować schemat XML, z którego korzystamy, tak aby nasi klienci wiedzieli, jak interpretować zwracane im dane XML.

    Nasza własna usługa REST Spróbujmy teraz zaimplementować naszą własną usługę REST. Trudno o lepszą ilustrację niż zestawienie albumów muzycznych, którego używaliśmy we wszystkich przykładach w ostatnich dwóch rozdziałach. W przypadku naszej pierwszej usługi REST skorzystamy z dokładnie tej samej logiki biznesowej, której użyliśmy w przykładzie z pakietem Services_Webserwice. Dla czytelników, którzy zdążyli już zapomnieć, jakie metody oferowała w tym przykładzie klasa RecordLabel oraz jakie miały one sygnatury, krótkie przypomnienie: class RecordLabel { public function getRecords($artist); public function getArtists() }

    Samą implementację klasy pominęliśmy, ponieważ prezentowaliśmy ją już w poprzednim przykładzie, a ponadto nie ma zastosowania w przypadku naszej usługi WWW opartej na protokole REST. Ponieważ logika biznesowa usługi oferuje dwie metody, również i nasza nowa usługa powinna udostępniać te dwie metody: „ Metoda getArtists() zwracać będzie wszystkich artystów, którzy nagrywali w danym studiu nagraniowym. „ Metoda getRecords(string $artist) zwracać będzie z kolei wszystkie albumy nagrane przez danego artystę. Zanim przystąpimy do implementowania usługi, musimy określić schemat adresu URL, którego klienci usługi WWW będą musieli używać, aby wywoływać oferowane przez nią metody. Aby uniknąć zachodu związanego z korzystaniem z modułu mod_rewrite serwera Apache, najprościej będzie zakodować nazwę metody jako parametr adresu URL, tak aby wszystkie

    225

    PEAR. Programowanie w PHP

    żądania mogły korzystać z tego samego bazowego adresu URL. Inne usługi oparte na protokole REST (takie jak na przykład Yahoo!) załączają parametr nazwy metody do adresu URL w ten sposób, że stanowi on część ścieżki, i korzystają z modułu mod_rewrite serwera Apache, by przekierowywać żądania do skryptu, który przetwarza wszystkie żądania. W ten sposób nie zdradzamy się, że nasza usługa została zaprogramowana z użyciem języka PHP. Wszystkie parametry, które mogą być potrzebne wywoływanym metodom, również zostaną zakodowane w standardowych parametrach adresu URL. W ten sposób typowy adres URL pozwalający na sięganie do naszej usługi może mieć następującą postać: http://www.your-domain.com/REST/index.php?m=getArtists http://www.your-domain.com/REST/index.php?m=getRecords&artist= Elvis+Presley

    Zaimplementujemy teraz nową klasę, do której będzie można sięgnąć wpisując adres http:// www.your-domain.com/REST/index.php i która będzie rozpatrywać wszystkie przesyłane jej parametry. Aby obsłużyć te żądania, usługa będzie potrzebować przynajmniej następujących danych: „ Obiektu, który będzie zawierał całą logikę biznesową usługi. „ Nazwę parametru żądania, który zawierać będzie wywoływaną przez klienta metodę. Aby upewnić się, że informacje te będą zawsze dostępne, dołączymy je do konstruktora naszej usługi: /** * Ogólnego zastosowania serwer REST */

    class REST_Server { /** * obiekt, który zawiera logikę biznesową */

    private $handler; /** * nazwa parametru żądania, który zawiera nazwę metody */

    private $methodVar; /** * Tworzymy nowy serwer REST * * @param object Obiekt zawierający logikę biznesową * @param string Nazwa zmiennej żądania * zawierającej wywołanie metody */

    public function __construct($handler, $methodVar = 'm') { $this->handler = $handler; $this->methodVar = $methodVar; } }

    226

    Rozdział 4. • Usługi WWW

    Jeśli utworzymy nową usługę, nie zostanie ona uruchamiana automatycznie, dlatego też będziemy musieli dodać do usługi nową metodę, która zajmie się następującymi sprawami: 1. Sprawdzi, czy klient przesłał nazwę wywoływanej metody. Jeśli nie, zasygnalizuje błąd. 2. Sprawdzi, czy logika biznesowa usługi oferuje metodę wywoływaną przez klienta. Jeśli nie, zasygnalizuje błąd. 3. Sprawdzi, czy klient przesłał wszystkie argumenty niezbędne wywoływanej metodzie. Jeśli nie, zasygnalizuje błąd. 4. Przywoła metodę żądania. Jeśli wywołanie metody się nie powiedzie, zwróci błąd. W przypadku powodzenia natomiast przygotuje reprezentację wyników w formacie XML i odeśle ją do klienta. Pierwsze zadanie wydaje się dość łatwe, czytelnicy mogą natomiast zastanawiać się, jak zaimplementować zadanie drugie i trzecie. Język PHP5 oferuje nową opcję, zwaną odbiciem (ang. reflection), która umożliwia wykonywanie introspekcji (tj. zaglądanie do środka) klas, metod i funkcji. W przypadku naszej prostej usługi REST wykorzystamy odbicie, by sprawdzić, czy obiekt przechowujący jej logikę biznesową oferuje metody, które klient chce przywołać. Co więcej, odbicie umożliwia nam również ustalenie, ilu argumentów i o jakich nazwach wymaga dana metoda. W ten sposób będziemy mogli mapować parametry przesłane w żądaniu HTTP na parametry przesyłane metodzie. Korzystanie z interfejsu API odbić jest bardzo proste. Przedstawiony tutaj kod sprawdza, czy obiekt zawiera metodę o nazwie getRecords(), i wyświetli nazwy parametrów, które należy przesłać tej metodzie: $label = new RecordLabel(); // Tworzymy obiekt odbicia, który może // dostarczyć nam informacji o przesłanym obiekcie

    $reflect = new ReflectionObject($label); try { // pobieramy obiekt dostarczający informacji // o metodzie getRecords()

    $method = $reflect->getMethod('getRecords'); } catch (ReflectionException $e) { echo "Metoda 'getRecords' nie istnieje."; exit(); } echo "Metoda 'getRecords' istnieje.\n"; echo "Liczba parametrów metody: ".$method->getNumberOfParameters()."\n"; // pobieramy informacje o wszystkich parametrach, // które należy przesłać tej metodzie

    $parameters = $method->getParameters(); foreach ($parameters as $parameter)

    227

    PEAR. Programowanie w PHP

    { $name = $parameter->getName(); echo " - $name\n"; }

    Po uruchomieniu skrypt ten zwróci następujący komunikat: Metoda 'getRecords' istnieje. Liczba parametrów metody: 1 - artist

    Jak widać, interfejs API odbić umożliwia łatwe wydobywanie w trakcie działania programu informacji na temat metod dowolnego używanego obiektu. Wykorzystamy tę opcję, by mapować parametry żądania HTTP na parametry metod w naszej prostej usłudze WWW opartej na protokole REST: /** * Uruchamiamy usługę REST */

    public function service() { // Później wyślemy XML

    header('Content-Type: text/xml'); // pobieramy nazwę metody, która powinna zostać przywołana

    if (!isset($_GET[$this->methodVar])) { $this->sendFault(1, 'No method requested.'); } $method = $_GET[$this->methodVar]; // Sprawdzamy, czy metoda istnieje

    $reflect = new ReflectionObject($this->handler); try { $method = $reflect->getMethod($method); } catch (ReflectionException $e) { $this->sendFault(2, $e->getMessage()); } // sprawdzamy, czy zostały przesłane // wszystkie parametry metody

    $parameters = $method->getParameters(); $values = array(); foreach ($parameters as $parameter) { $name = $parameter->getName(); if (isset($_GET[$name])) {

    228

    Rozdział 4. • Usługi WWW

    $values[] = $_GET[$name]; continue;

    }

    } if ($parameter->isDefaultValueAvailable()) { $values[] = $parameter->getDefaultValue(); continue; } $this->sendFault(3, 'Missing parameter ' . $name . '.'); // Przywołujemy samą metodę i wysyłamy wyniki klientowi

    try { // Rozwiązanie to będzie działać począwszy od PHP 5.1: // $method->invokeArgs($this->handler, $values);

    $result = call_user_func_array( array($this->handler, $method->getName()), $values); $this->sendResult($method->getName(), $result);

    }

    } catch (Exception $e) { $this->sendFault($e->getCode(), $e->getMessage()); }

    Ta niezbyt długa metoda obsługuje wszystkie z wcześniej wymienionych zadań, które spełniać musi nasza usługa. Po pierwsze wysyła nagłówek Content-Type informujący, że zawartość będzie typu XML, ponieważ usługi REST zawsze zwracają dane w formacie XML. Następnie sprawdza, czy w żądaniu została przesłana nazwa wywoływanej metody i czy logika biznesowa usługi oferuje metodę o tej nazwie. Jeśli któryś z tych testów da wynik negatywny, przywoła metodę sendFault(), by zasygnalizować klientowi błąd. Dalsza część kodu zajmuje się sprawdzaniem implementacji wywoływanej metody. Jeśli żądana przez klienta metoda istnieje, to pobieramy listę potrzebnych jej parametrów i przeglądamy je w pętli. Dla każdego z parametrów wydobywać będziemy odpowiednią wartość parametru z żądania lub używać wartości domyślnej. Jeśli wartość parametru nie zostanie przesłana w żądaniu, a usługa nie określa domyślnej wartości parametru, to zasygnalizujemy kolejny błąd. Wartości parametrów wydobyte z żądania przechowywane będą w tablicy $values. Gdy już wszystkie parametry metody zostaną odpowiednio inicjowane, przywołamy samą metodę używając wywołania call_user_func_array(). Poczynając od PHP 5.1 interfejs API odbić nie oferuje już metody invokeArgs(), która umożliwiała przywoływanie dowolnej metody, przesyłając jej tablicę zawierającą argumenty tej metody. Wynik działania przywołanej metody zostanie przechwycony i odesłany klientowi w formacie XML przy użyciu w tym celu metody sendResult(). Jak łatwo zauważyć, w tym fragmencie kodu zastosowaliśmy dwie metody, których jak dotąd nie zaimplementowaliśmy. Dlatego też przed testowym uruchomieniem usługi trzeba je będzie wcześniej napisać. Oto kod implementujący metodę sendFault():

    229

    PEAR. Programowanie w PHP

    /** * Sygnalizujemy błąd * * @param integer, liczba całkowita, kod błędu * @param string, łańcuch, komunikat o błędzie */

    protected function sendFault($faultCode, $faultString) { $serializer = new XML_Serializer(); $serializer->setOption(XML_SERIALIZER_OPTION_ROOT_NAME, 'fault'); $serializer->serialize(array('faultCode' => $faultCode, 'faultString' => $faultString)); echo $serializer->getSerializedData(); exit(); }

    Czytelnicy, którzy uważnie czytali rozdział poświęcany przetwarzaniu dokumentów XML za pomocą modułów oferowanych przez repozytorium PEAR, wiedzą już, że pakiet XML_Serializer, z którego korzystamy, potrafi przygotować dokument XML z praktycznie dowolnych danych. W tym przypadku przesyłamy po prostu tablicę zawierającą kod błędu oraz jego opis. Te cztery wiersze kodu przygotują następujący dokument XML:

    1 Nie ma żądanej metody.

    Jeśli klient otrzyma taki dokument XML, bez trudu zidentyfikuje go jako informacje o błędzie i odpowiednio przetworzy. Naszą metodę sendFault() można obecnie przywoływać poprzez przesłanie jej dowolnego kodu błędu i łańcucha z jego opisem, co sygnalizuje klientowi dowolny błąd. Kod metody sendResult() jest bardzo podobny do kodu metody sendFault(): /** * Wysyłamy wynik do klienta w postaci kodu XML * * @param string, łańcuch nazwa przywoływanej metody * @param różnie, wyniki korzystania z logiki biznesowej */

    protected function sendResult($methodName, $result) { $matches = array(); if (preg_match('/^get([a-z]+)s$/i', $methodName, $matches)) { $defaultTag = strtolower($matches[1]); } else { $defaultTag = 'item'; }

    230

    Rozdział 4. • Usługi WWW

    $serializer = new XML_Serializer(); $serializer->setOption(XML_SERIALIZER_OPTION_ROOT_NAME, $methodName.'Result'); $serializer->setOption(XML_SERIALIZER_OPTION_DEFAULT_TAG, $defaultTag); $serializer->serialize($result); echo $serializer->getSerializedData(); exit(); }

    Niemniej metoda ta jest odrobinę bardziej złożona, ponieważ wykorzystuje jako główny znacznik nazwę wywoływanej przez klienta metody, a jeśli nazwa metody zaczyna się od get*, to użyje reszty nazwy jako domyślnego znacznika dla indeksowanych tablic. Dlatego też, jeśli przywołamy usługę za pomocą przykładowego adresu URL przedstawionego wcześniej, to otrzymamy od serwera usługi następujący dokument XML:

    Elvis Presley Carl Perkins

    That's All Right (Mama) & Blue Moon Of Kentucky

    Good Rockin' Tonight

    W klasach tworzących logikę biznesową możemy sygnalizować błędy, uruchamiając odpowiednie wyjątki, co jest standardowym sposobem zgłaszania błędów w programowaniu obiektowym. Aby dodać do metody getRecords() kod obsługujący błędy, konieczna będzie jej nieznaczna modyfikacja: /** * Pobieramy wszystkie albumy artysty * * @param string * @return string[] */

    public function getRecords($artist) { if (isset($this->records[$artist])) { $result = $this->records[$artist]; return $result; } throw new Exception('Artysty "'.$artist. '" nie ma w naszej bazie danych.', 50); }

    231

    PEAR. Programowanie w PHP

    Wyjątek ten będzie automatycznie przechwytywany przez klasę REST_Server i przekształcany na odpowiedni dokument XML, który następnie zostanie wysłany do klienta:

    50

    Artysty "P.Diddy" nie ma w naszej bazie danych.

    Jeśli dodamy do logiki biznesowej usługi nowe metody, staną się one natychmiast dostępne za pośrednictwem naszej nowej usługi REST. Możemy również przesłać zupełnie inny obiekt obudowujący logikę biznesową i będziemy mogli bez trudu sięgać do niego za pomocą serwera REST. Ponieważ zapoznaliśmy się już z pakietem XML_Serializer, nie będziemy mieli problemu z modyfikowaniem dostarczanych klientowi dokumentów XML. Implementację klienta naszej nowej usługi REST pozostawiamy czytelnikom jako ćwiczenie domowe.

    Podsumowanie W tym rozdziale omawialiśmy różne usługi WWW. Dowiedzieliśmy się, jak działają usługi oparte na protokołach XML-RPC i SOAP oraz w jaki sposób przygotowywać usługi oparte na znacznie prostszym protokole REST. Rozdział ten omawiał zarówno korzystanie z tych usług za pomocą odpowiednich klientów, jak też tworzenie i udostępnianie publiczne własnej usługi WWW. Dowiedzieliśmy się również, jak korzystać z pakietu XML_RPC, by za jego pomocą sięgać do usługi WWW oferowanej przez witrynę PEAR i w ten sposób pobierać informacje na temat różnych pakietów oprogramowania. Korzystaliśmy również z pakietu Services_Google, który obudowując rozszerzenie SOAP dla języka PHP umożliwia sięganie do interfejsu API wyszukiwarki Google. Za pomocą pakietów Services_Amazon i Services_Technorati sięgaliśmy do dwóch popularnych usług WWW opartych na protokole REST, nie przejmując się przesyłanymi dokumentami XML. Na przykładzie interfejsu API usług WWW oferowanych przez Yahoo! pokazaliśmy, w jaki sposób za pomocą pakietów HTTP_Request i XML_Unserializer można korzystać z dowolnej usługi WWW opartej na protokole REST, niezależnie od tego, w jakim formacie XML zwraca ona dane. W drugiej części rozdziału zajmowaliśmy się natomiast oferowaniem własnych usług WWW. Dowiedzieliśmy się, jak za pomocą pakietu XML_RPC udostępniać usługę opartą na protokole XML_RPC, umożliwiającą również introspekcję kodu usługi. Za pomocą pakietu Services_ Webservice zautomatyzowaliśmy tworzenie usługi opartej na protokole SOAP, włącznie z generowaniem dokumentu WSDL z każdej klasy, która powinna być udostępniona jako usługa.

    232

    Rozdział 4. • Usługi WWW

    Na koniec wreszcie przygotowaliśmy ogólnego zastosowania serwer REST, który nożna wykorzystać do utworzenia nowej usługi WWW na bazie dowolnej klasy definiującej jej logikę biznesową. Kategoria poświęcona usługom WWW w repozytorium PEAR cały czas się rozrasta, oferując coraz więcej klientów dla różnych usług WWW. Wszystkie one jednak oparte są na rozwiązaniach, które prezentowaliśmy w tym rozdziale.

    233

    PEAR. Programowanie w PHP

    234

    5 Praca z datami Ten rozdział jest wprowadzeniem do sekcji Date and Time (data i czas) repozytorium PEAR. Omawiać będzie pakiety Date, Date_Holidays i Calendar. Przekonamy się, w jaki sposób ułatwiają one programowanie i jak z nich korzystać, by rozwiązywać problemy z reprezentacją dat i czasu. Sekcję Date and Time można znaleźć w repozytorium PEAR pod adresem: http:// pear.php.net/packages.php?catpid=8&catname=Date+and+time. Po lekturze tego rozdziału czytelnicy wiedzieć będą, jak za pomocą opisanych tu pakietów zastępować standardowe funkcje daty i czasu języka PHP. Znajomość tych trzech bibliotek ułatwia pisanie wydajnych aplikacji wymagających obsługi dat.

    Praca z pakietem Date Czytelnicy zadają sobie pewnie pytanie, dlaczego mieliby korzystać z pakietu PEAR::Date zamiast ze standardowych funkcji PHP obsługujących datę i czas, bazujących na znacznikach czasu systemu Unix. Prawdę powiedziawszy, korzystanie z pakietu PEAR::Date oznacza, że programy będą działać wolniej, ponieważ został on napisany w języku PHP, a nie w C. Dodatkowo dochodzi konieczność zapoznania się z nowym pakietem i nauczenia, jak z niego korzystać. Niemniej pakiet ten ma też wiele zalet, z których bodaj najważniejszą jest ta, że PEAR:: Date nie został oparty na znacznikach czasu systemu Unix, więc działać będzie również tam, gdzie nie są one dostępne. Znacznik czasu służy do definiowania wartości w określonym formacie, służącej do określania czasu. Znaczniki czasu systemu Unix zliczają sekundy począwszy od 1 stycznia 1970 roku, od godziny 00:00 czasu GMT (uniwersalnego). Obecnie komputery przechowują te znaczniki czasu jako 32-bitowe liczby całkowite ze znakiem, mogą zatem przechowywać wartości od –2 147 483 648 do 2 147 483 648. W tym schemacie czas startowy 01/01/1970 00:00h GMT

    PEAR. Programowanie w PHP

    jest reprezentowany przez wartość 0 (zero), a czas 01/01/1970 00:01h GMT przez liczbę całkowitą 60. Problem ze znacznikami czasu systemu Unix polega na tym, że dokładnie 19 stycznia 2038 roku siedem sekund po godzinie 3:14 rano czasu GMT osiągną one maksymalną dopuszczalną wartość liczby całkowitej. Jest to problem odrobinę podobny do problemu roku 2000. W sekundę później wszystkie liczniki czasu wyzerują się i zaczną zliczać czas od początku, czyli od wartości –2 147 483 648. W tym momencie wiele 32-bitowych programów na całym świecie solidarnie przestanie działać lub ulegnie awarii. Oczywiście część specjalistów twierdzi, że do roku 2038 komputery już na pewno będą korzystać z 64-bitowych liczb całkowitych. Liczby takie wystarczają do przechowywania informacji o czasie daleko wykraczających poza oczekiwany czas istnienia wszechświata. Oczywiście będzie to prawdą dla większości aplikacji. Co jednak ze starymi systemami (tzw. legacy systems), których z ważnych przyczyn nie unowocześniano, lub z programami, które muszą zachować zdolność do współdziałania z oprogramowaniem 32-bitowym? Korzystanie z pakietu PEAR::Date pozwoli nam zabezpieczyć się przed wspomnianym problemem ze znacznikami czasu. Co więcej, pakiet PEAR::Date jest pakietem zorientowanym obiektowo, dzięki czemu oferuje wiele użytecznych metod, a oprócz tego potrafi rozpoznawać strefy czasowe. Ponadto zakres czasu obejmujący na przykład jedną godzinę nie musi być definiowany jako 3600 sekund, ale może być reprezentowany za pomocą znacznie wygodniejszego w użyciu obiektu Date_Span: $timespan = new Date_Span('1 hour')

    Pakiet PEAR::Date dostarcza wielu naprawdę użytecznych funkcji. Dlatego przyda się nawet tym programistom nie planującym tworzenia oprogramowania, które będzie w użyciu po roku 2038.

    Pakiet Date Na kolejnych stronach pokażemy, jak tworzyć przepytywać i modyfikować obiekty typu Date (daty). Opowiemy również o tym, jak porównywać ze sobą różne obiekty daty i jak wyświetlać zawarte w nich informacje o dacie i czasie w dowolnym formacie, którego potrzebować będzie programista. Pora więc zacząć naszą wycieczkę po świecie dat.

    Tworzenie obiektu daty Date Pierwszą rzeczą, której wypada się nauczyć podczas pracy z pakietem PEAR::Date, jest tworzenie obiektów klasy Date, przechowujących informacje o dacie. Konstruktorowi klasy można przesyłać jeden, opcjonalny parametr. Jeśli nie prześlemy żadnego parametru, to obiektowi Date zostanie przypisana bieżąca data i godzina. Jeśli natomiast zdecydujemy się przesłać konstruktorowi jakiś parametr, to będziemy mogli inicjować obiekt Date dowolną wartością daty i czasu. Dopuszczalne formaty parametru, w którym przesyłamy konstruktorowi datę i czas, to format ISO 8601, format znaczników czasu systemu Unix lub też po prostu inny obiekt Date:

    236

    Rozdział 5. • Praca z datami

    require_once 'Date.php'; $now = new Date(); $iso8601 = new Date('2005-12-24 12:00:00'); $mysqlTS = new Date('20051224120000'); $unixTS = new Date(mktime(12, 0, 0, 12, 24, 2005)); $dateObj = new Date($unixTS);

    Gdy już obiekt zostanie utworzony, będziemy mogli modyfikować jego właściwości za pomocą metody setDate(). Niezależnie od tego, czy ustawimy informacje o dacie i czasie za pomocą konstruktora, czy też metody setDate(), obiekt zostanie inicjowany odpowiednią wartością daty i czasu zgodną ze strefą czasową naszego komputera.

    Wydobywanie informacji o czasie Obiekty Date udostępniają kilka metod, które umożliwiają uzyskiwanie szczegółowych informacji o ich właściwościach. Na przykład oferują zestaw metod służących do pobierania wartości zapisanych w różnych właściwościach obiektu Date, służących do przechowywania informacji o dacie i czasie: getYear() (roku), getMonth() (miesiącu), getDay() (dniu), getHour() (godzinie), getMinute() (minucie) i getSecond() (sekundzie). Jeśli natomiast chcemy pobrać wszystkie informacje za pomocą jednej metody, to możemy skorzystać z metody getTime(), by pobrać znacznik czasu systemu Unix lub metody getDate(), by pobrać datę i godzinę w postaci odpowiednio sformatowanego łańcucha. Tej drugiej metodzie można przesłać opcjonalny parametr, który definiować będzie format informacji zwracanych w łańcuchu. Kolejna tabela prezentuje stałe pozwalające określić różne formaty danych zwracanych w łańcuchu oraz podaje przykłady dla następującego obiektu daty: $date = newDate('2005-12-24 09:30:00'). Stała

    Format zwracanych danych

    DATE_FORMAT_ISO

    2005-12-24 09:30:00 (RRRR-MM-DD gg:mm:ss)

    DATE_FORMAT_ISO_BASIC

    20051224T093000Z (RRRRMMDDTggmmssZ)

    DATE_FORMAT_ISO_EXTENDED

    2005-12-24T09:30:00Z (RRRR-MM-DDTgg:mm:ssZ)

    DATE_FORMAT_ISO_EXTENDED_MICROTIME

    2005-12-24T09:30:00.000000Z (RRRR-MM-DDTgg:mm:ss.s*Z)

    DATE_FORMAT_TIMESTAMP

    200512240930000 (RRRRMMDDggmmss)

    DATE_FORMAT_UNIXTIME

    1135413000 (liczba całkowita: liczba sekund, które upłynęły od 01/01/1970 od godz. 00:00 GMT)

    Kolejny zestaw metod pozwala na pobieranie wartości właściwości zawierających dodatkowe informacje na temat daty i czasu przechowywanego w obiekcie Date. Opis niektórych z tych metod i przykłady danych zwróconych przez obiekt Date prezentujemy w kolejnej tabeli: 237

    PEAR. Programowanie w PHP

    Metoda

    Opis

    Wynik dla 2005-12-24 09:30:00

    getDayName($abbr)

    Zwraca nazwę dnia. Opcjonalny parametr pozwala określić, czy w formie skróconej (false), czy pełnej (true) — domyślnie ma on wartość false.

    Saturday (sobota, łańcuch)

    getDayOfWeek()

    Zwraca dzień w tygodniu dla danej daty w postaci liczby całkowitej: niedziela = 0, poniedziałek = 1, …, sobota = 6.

    6 (liczba całkowita)

    getDaysInMonth()

    Zwraca liczbę dni w miesiącu właściwym dla daty przechowywanej w obiekcie.

    31 (liczba całkowita)

    getQuarterOfYear() Zwraca numer kwartału właściwy dla daty przechowywanej w obiekcie.

    4 (liczba całkowita)

    getWeekOfYear()

    Zwraca numer tygodnia właściwego dla daty przechowywanej w obiekcie.

    51 (liczba całkowita)

    getWeekInMonth()

    Zwraca liczbę tygodni w miesiącu właściwym dla daty przechowywanej w obiekcie.

    5 (liczba całkowita)

    isLeapYear()

    Informuje, czy dany rok jest przestępny (true), czy nie (false).

    false (wartość logiczna)

    isPast()

    Sprawdza, czy data przechowywana w obiekcie nie jest wcześniejsza od bieżącej daty.

    false (wartość logiczna)

    isFuture()

    Sprawdza, czy data przechowywana w obiekcie nie jest późniejsza od bieżącej daty.

    false (wartość logiczna)

    Dysponując obiektem Date, możemy również bez trudu ustalić, jaka data poprzedza przechowywaną w nim datę lub po niej następuje. Można to zrobić za pomocą metod getNextDay() (następny dzień) lub getPrevDay() (poprzedni dzień). W ten sam sposób można również ustalić nazwę dnia tygodnia poprzedzającego tydzień przechowywany w obiekcie lub następującego po nim. Oto przykłady pobierania tych informacji: $date = new Date('2005-12-24 09:30:00'); $prev = $date->getPrevDay(); // 2005-12-23 09:30:00 $prevWd = $date->getPrevWeekday(); // 2005-12-23 09:30:00 $next = $date->getNextDay(); // 2005-12-25 09:30:00 $nextWd = $date->getNextWeekday(); // 2005-12-26 09:30:00

    Każda z tych metod zwraca nowy obiekt Date zawierający odpowiednie informacje o dacie. Godzina pozostaje taka sama jak w oryginalnym obiekcie Date.

    238

    Rozdział 5. • Praca z datami

    A jeśli potrzebujemy dodatkowych informacji o dacie i czasie? Czytelnicy, którzy potrzebować będą dodatkowych informacji o dacie i czasie, powinni przyjrzeć się dostarczanej przez pakiet PEAR:Date klasie Date_Calc. Klasa ta jest bardzo często wykorzystywana wewnętrznie do różnych wyliczeń przez klasę Date i dostarcza mnóstwa metod, którym warto się przyjrzeć, jeśli klasa Date nie oferuje nam potrzebnych informacji.

    Modyfikowanie obiektów daty Date Właściwości obiektu Date można określać w momencie konstruowania obiektu, jak również używając odpowiednich metod pozwalających na definiowanie właściwości już w trakcie istnienia obiektu. Można określić wszystkie właściwości daty i czasu za jednym razem przywołując metodę setDate(). Domyślnie oczekuje ona jako parametru daty w formacie łańcucha ISO 8601. Można jednak określić również inny format łańcucha daty, podając w drugim parametrze, jakiego formatu danych używamy. Służą do tego stałe opisane w prezentowanej wcześniej tabeli, zestawiającej różne formaty daty zwracane przez metodę getTime(). Jeśli chcemy po prostu precyzyjnie zdefiniować wybraną wartość obiektu, możemy skorzystać z którejś z następujących funkcji służących do ustawiania różnych wartości: setYear() (roku), setMonth() (miesiąca), setDay() (dnia), setHour() (godziny), setMinute() (minuty) i setSecond() (sekundy). Każda z nich wymaga przesłania jej jednego parametru reprezentującego wartość właściwości obiektu, którą chcemy zdefiniować. Kolejnym sposobem zmieniania obiektu Date polega na wykorzystaniu metod addSeconds() i subtractSeconds(). Za ich pomocą możemy określić, ile sekund należy odpowiednio dodać lub odjąć od bieżących informacji o dacie i godzinie zapisanych w obiekcie. Na przykład przy użyciu wywołania $date->addSeconds(3600) zwiększymy właściwość obiektu Date przechowującą informacje na temat godziny o 1 (ponieważ 1 godzina równa się 3600 sekund). W dalszej części, przy okazji omawiania obiektu Date_Span, przekonamy się, że istnieją również inne sposoby dodawania lub odejmowania przedziałów czasu od istniejących obiektów Date. Jeśli mamy obiekt Date nazwany $a i chcemy przypisać wartości jego właściwości innemu obiektowi Date o nazwie $b, można to łatwo zrobić przywołując dla obiektu $b metodę copy() i przesyłając jej jako argument obiekt $a. Po tej operacji w obiekcie $b będą zapisane takie same informacje na temat daty i czasu, jak w obiekcie $a. Oto praktyczny przykład korzystania z metod umożliwiających modyfikowanie obiektów Date: $date = new Date('2005-12-24 09:30:00'); $copy = new Date();

    // $copy initialized with current date/time

    $copy->copy($date);

    // $copy => 2005-12-24 09:30:00

    239

    PEAR. Programowanie w PHP

    $copy->setHour(12); $copy->setMinute(0);

    // $copy => 2005-12-24 12:30:00 // $copy => 2005-12-24 12:00:00

    $copy->addSeconds(30); // $copy => 2005-12-24 12:00:30 $date->setDate($copy->getDate()); // $date => 2005-12-24 12:00:30

    Porównywanie dat ze sobą Jedną z typowych czynności wykonywanych podczas pracy z datami jest porównywanie ich ze sobą. Obiekty Date posiadają metody umożliwiające: „ Sprawdzanie, czy czas zapisany w obiekcie jest wcześniejszy, czy późniejszy od określonej daty. „ Sprawdzanie, czy dwa obiekty zawierają takie same informacje o dacie i czasie. „ Sprawdzanie, czy dwa obiekty Date są równe lub który z nich zawiera wcześniejszy, a który późniejszy czas. Jeśli mamy obiekt Date i chcemy ustalić, jak ma się on do innego obiektu Date, to możemy skorzystać z jednej z następujących metod: „ before() „ after() „ equals() Metody te służą odpowiednio do ustalania: czy data w jednym obiekcie jest wcześniejsza niż w drugim, czy jest późniejsza, czy też są takie same. Oto przykład korzystania z nich: $d1 = new Date('2005-12-24'); $d2 = new Date('2005-12-30'); // false $equal = $d1->equals($d2); $d1_Before_d2 = $d1->before($d2); // true $d1_After_d2 = $d1->after($d2); // false

    Klasa Date oferuje również specjalną metodę, gdy musimy porównać daty, by je posortować. Metoda ta to Date::compare() i można z niej korzystać statycznie. Wymaga ona przesłania jej dwóch parametrów reprezentujących obiekty Date, które mają zostać porównane. Jeśli obiekty są równe, zwróci 0, jeśli pierwszy jest wcześniejszy od drugiego, zwróci -1, a jeśli pierwszy jest późniejszy od drugiego, 1. Jest to bardzo wygodne, gdy zachodzi konieczność posortowania obiektów Date, ponieważ metody tej można używać jako metody definiowanej przez użytkownika w funkcjach sortujących tablice PHP. Oto przykład pokazujący, jak za pomocą funkcji usort() i metody Date:compare() posortować tablicę obiektów Date. $dates = array(); $dates[] = new Date('2005-12-24'); $dates[] = new Date('2005-11-14');

    240

    Rozdział 5. • Praca z datami

    $dates[] = new Date('2006-01-04'); $dates[] = new Date('2003-02-12'); usort($dates, array('Date', 'compare'));

    Ponieważ metoda Date::compare() jest statyczną metodą klasy, możemy jej przesłać tablicę zawierającą dwa łańcuchy definiujące klasę i nazwę metody.

    Formatowanie dat Wartości właściwości obiektów Date można wyświetlać używając metody format(). Zwracany łańcuch z informacjami o dacie i czasie będzie wtedy odpowiednio lokalizowany w zależności od lokalnych ustawień czasu. Lokalne ustawienia można modyfikować za pomocą metody setlocale() języka PHP. Oto przykład korzystania z tej funkcji: $date = new Date('2005-12-24 09:30:00'); echo $date->format('%A, %D %T'); // wyświetla: Saturday, 12/24/2005 // 09:30:00

    Jak widać, możemy zmieniać format zwracanego łańcucha korzystając z odpowiednich zmiennych zastępczych (ang. placeholders). Oto tabela prezentująca pełną listę zmiennych zastępczych, których można użyć podczas formatowania. Zmienna

    Opis

    %a

    Skrótowa angielska nazwa dnia tygodnia (Mon, Tue, Wed, …)

    %A

    Pełna angielska nazwa dnia tygodnia (Monday, Tuesday, Wednesday, …)

    %b

    Skrótowa angielska nazwa miesiąca (Jan, Feb, Mar, …)

    %B

    Pełna angielska nazwa miesiąca (January, February, March, …)

    %C

    Numer stulecia (z przedziału od 00 do 99)

    %d

    Numer dnia w miesiącu (z przedziału od 01 do 31)

    %D

    Data w formacie, jaki uzyskalibyśmy wpisując %m/%d/%y

    %e

    Numer dnia w miesiącu z użyciem liczb jednocyfrowych (z przedziału od 1 do 31)

    %E

    Liczba dni od początki epoki Uniksa (od 01/01/1970 00:00h GMT)

    %h

    Godzina w postaci liczby dziesiętnej, również jednocyfrowej (od 0 do 23)

    %H

    Godzina w postaci liczby dziesiętnej (od 00 do 23)

    %i

    Godzina jako liczba dziesiętna na 12-godzinnej tarczy zegara, z użyciem liczb jednocyfrowych (od 1 do 12)

    %I

    Godzina jako liczba dziesiętna na 12-godzinnej tarczy zegara (od 01 do 12)

    %j

    Numer dnia w roku (z przedziału od 001 do 366)

    %m

    Numer miesiąca w postaci liczby dziesiętnej (z przedziału od 01 do 12)

    241

    PEAR. Programowanie w PHP

    Zmienna

    Opis

    %M

    Minuty w postaci liczby dziesiętnej (z przedziału od 00 do 59)

    %n

    Znak nowego wiersza (\n)

    %O

    Przesunięcie związane ze zmianą czasu (DST, ang. daylight saving time, czas letni), podane w formacie „+/-HH:MM”

    %o

    Standardowe przesunięcie strefy czasowej, podane w formacie „+/-HH:MM”

    %p

    Skrót „am” (przed południem) lub „pm” (po południu), w zależności od aktualnej godziny

    %P

    Skrót „AM” (przed południem) lub „PM” (po południu), w zależności od aktualnej godziny

    %r

    Godzina podana z użyciem skrótów am/pm. Identycznie z „%I:%M:%S %p”

    %R

    Godzina podana w notacji 24-godzinnej. Identycznie z „%H:%M”

    %s

    Sekundy z podaniem dziesiętnej wartości czasów krótszych niż jedna sekunda

    %S

    Sekundy w postaci liczby dziesiętnej (z przedziału od 00 do 59)

    %t

    Znak tabulacji (\t)

    %T

    Bieżąca godzina. Identycznie z „%H:%M:%S”

    %w

    Numer dnia tygodnia (niedziela = 0, poniedziałek = 1, …, sobota = 6)

    %U

    Numer tygodnia w bieżącym roku

    %y

    Rok w postaci ostatnich dwóch cyfr dziesiętnych (z przedziału od 00 do 99)

    %y

    Rok w postaci pełnej liczby dziesiętnej (z przedziału od 0000 do 9999)

    %%

    Sam znak %

    Tworzenie obiektu Date_Span Oprócz klasy Date moduł PEAR:Date oferuje również klasę Date_Span, która umożliwia określanie przedziałów czasu z dokładnością do sekundy. Konstruktorowi klasy można przesłać wiele różnych parametrów. Można zdefiniować przedział czasu używając tablicy, specjalnie sformatowanego łańcucha lub dwóch obiektów Date. Istnieją też inne sposoby definiowania przedziałów, ale te spotyka się najczęściej. Dalej zaprezentujemy przykłady ilustrujące tworzenie obiektu Date_Span, definiujące przedział czasu o długości 1 dnia, 6 godzin, 30 minut i 15 sekund. Aby utworzyć przedział czasu używając tablicy, musi ona zawierać wartości określające dzień, godzinę, minutę i sekundę: $span = new Date_Span(array(1, 6, 30, 15));

    Jeśli skorzystamy z dwóch obiektów Date, to przedział czasu zostanie wyznaczony jako różnica pomiędzy tymi dwiema datami: $span = new Date_Span( new Date('2005-01-01 00:00:00'), new Date('2005-01-02 06:30:15'));

    242

    Rozdział 5. • Praca z datami

    Jeśli prześlemy konstruktorowi liczbę całkowitą, to określi ona długość przedziału czasu w sekundach: $span = new Date_Span(109815);

    Najwygodniejszym sposobem jest jednak przesyłanie konstruktorowi parametru w postaci łańcucha. Domyślnie konstruktor oczekiwać będzie, że łańcuch poda czas w formacie NNSV (Non Numeric Separated Values), w którym każda wartość nieliczbowa traktowana jest automatycznie jak separator. Długość przedziału czasu zależeć będzie od tego, ile wartości liczbowych będzie zawierać łańcuch. Oto co na ten temat mówi dokumentacja interfejsu API klasy: „Jeśli nie zostaną przesłane żadne wartości, długość przedziału czasu zostanie określona jako zero. Jeśli przesłana zostanie tylko jedna wartość, to użyta zostanie ona do określenia liczby godzin. Jeśli zostaną przesłane dwie wartości, określą one odpowiednio liczbę godzin i minut. Jeśli trzy wartości, to określą liczbę godzin, minut i sekund”. Jeśli natomiast prześlemy cztery wartości, to określą one odpowiednio liczbę dni, godzin, minut i sekund. Oto przykład pokazujący, jak zdefiniować przedział czasu, o którym wspomnieliśmy wyżej: $span = new Date_Span('1,6,30,15'); // dzięki formatowi NNSV możemy korzystać również z:

    $span2 = new Date_Span('1,06:30:15');

    Konstruktor potrafi jednak analizować również inne, nawet bardzo złożone formaty czasu zapisane w łańcuchu, pod warunkiem że określimy, jak ma wyglądać ten format. Robi się to za pomocą odpowiednich zmiennych zastępczych. Więcej informacji można znaleźć w dokumentacji interfejsu API dla metody Date_Span::setFromString().

    Modyfikowanie obiektów Date_Span Właściwości obiektu Date_Span można zmieniać za pomocą zestawu specjalnie przeznaczonych do tego metod. Jednym z ciekawszych sposobów modyfikowania przedziału czasu jest skorzystanie z metody set(). Zachowuje się ona dokładnie tak samo, jak opisywany wcześniej konstruktor klasy. Prawdę powiedziawszy, konstruktor definiując wartości nowo tworzonego obiektu sięga po prostu do tej metody. Innym sposobem jest skorzystanie ze specjalnych metod pozwalających określać przedział czasu przesyłając odpowiednią wartość w postaci liczby godzin, minut, tablicy lub w jeszcze jakiejś innej postaci. Metody te to setFromArray() (za pomocą tablicy), setFromDateDiff() (za pomocą dwóch obiektów Date), setFromDays() (podając liczbę dni), setFromHours() (podając liczbę godzin), setFromMinutes() (podając liczbę minut), setFromSeconds() (podając liczbę sekund) i setFromString() (za pomocą łańcucha). Można również modyfikować przedział czasu odejmując lub dodając do niego inny przedział czasu. Służą do tego metody add() (dodawanie) i subtract() (odejmowanie):

    243

    PEAR. Programowanie w PHP

    $span1 = new Date_Span('1 hour'); $span2 = new Date_Span('2 hours'); $span1->add($span2); // $span1 to obecnie 3 godziny

    Klasa Date_Span oferuje również metodę copy() umożliwiającą kopiowanie przedziałów czasu. Działa ona dokładnie tak samo jak metoda Date::copy() i można wykorzystać ją do przypisywania jednemu przedziałowi czasu wartości zapisanej w innym obiekcie Date_Timespan.

    Konwertowanie przedziałów czasu Klasa Date_Span oferuje cztery metody umożliwiające pobieranie przedziałów czasu w formie wartości liczbowych. Metody te to toDays() (zwracająca przedział czasu liczony w dniach), toHours() (zwracająca przedział czasu liczony w godzinach), toMinutes() (zwracająca przedział czasu liczony w minutach) i toSeconds() (zwracająca przedział czasu liczony w sekundach): $span = new Date_Span('1,06:30:15'); // 1 day, 6 hours, 30 min, 15 sec $days = $span->toDays(); $hours = $span->toHours(); $minutes = $span->toMinutes(); $seconds = $span->toSeconds();

    // 1.27100694444 // 30.5041666667 // 1830.25 // 109815

    Porównywanie przedziałów czasu Jeśli chcemy porównać dwa obiekty przedziałów czasu Date_Span, to mamy do dyspozycji pięć przygotowanych w tym celu metod: equal() (równy), greater() (większy), greaterEqual() (większy lub równy), lower() (mniejszy) i lowerEqual() (mniejszy lub równy). Przywołanie każdej z tych metod dla danego obiektu pozwala porównać go z innym obiektem. Każda z metod zwraca odpowiednią wartość logiczną: $span1 = new Date_Span('1,6:30:15'); $span2 = new Date_Span('2,12:30:15'); $span1->lower($span2); $span1->lowerEqual($span2); $span1->equal($span2); $span1->greater($span2); $span1->greaterEqual($span2);

    // true // true // false // false // false

    Obiekt Date_Span oferuje również metodę porównującą compare(), którą można wykorzystać do sortowania obiektów przedziałów czasu Dates_Span według ich długości. Należy przesłać jej jako argumenty dwa obiekty przedziałów czasu, a metoda zwróci 0, jeśli będą równe; -1, jeśli pierwszy jest krótszy, lub 1, jeśli krótszy jest drugi. Oto kod pokazujący, jak za jej pomocą wykonać sortowanie:

    244

    Rozdział 5. • Praca z datami

    $tspans = $tspans[] $tspans[] $tspans[] $tspans[]

    array(); = new Date_Span('1, 12:33:02'); = new Date_Span('1, 00:33:02'); = new Date_Span('3, 00:00:00'); = new Date_Span('1');

    usort($tspans, array('Date_Span', 'compare'));

    Kolejna użyteczną metodą, choć nie można jej wykorzystać do porównywania przedziałów czasu, jest metoda isEmpty(). Zwróci ona wartość true (prawda), jeśli przedział czasu będzie miał zerową długość (jest pusty), w pozostałych przypadkach natomiast zwróci wartość false: $span = new Date_Span(''); $empty = $span->isEmpty(); // true

    Formatowanie przedziałów czasu Korzystając z metody format(), możemy sformatować obiekt Date_Span do odpowiedniego łańcucha. Podobnie jak metoda Date::format() oferuje ona kilka użytecznych zmiennych zastępczych, za pomocą których można określić format zwracanych danych. Niektóre zmienne zastępcze zostały zestawione w kolejnej tabeli. Informacje o pozostałych można znaleźć w opisie metody Date_Span::format() w dokumentacji interfejsu API klasy Date_Span. Zmienna

    Opis

    %C

    Przedział liczony w dniach, godzinach, minutach i sekundach, identycznie z %D, %H:%M:%S

    %d

    Dni w postaci liczby zmiennoprzecinkowej

    %D

    Dni w postaci liczby dziesiętnej

    %h

    Godziny w postaci liczby dziesiętnej (z zakresu od 0 do 23)

    %H

    Godziny w postaci liczby dziesiętnej (z zakresu od 00 do 23)

    %m

    Minuty w postaci liczby dziesiętnej (z zakresu od 0 do 59)

    %M

    Minuty w postaci liczby dziesiętnej (z zakresu od 00 do 59)

    %R

    Czas w notacji 24-godzinnej, identycznie z %H:%M

    %s

    Sekundy w postaci liczby dziesiętnej (z zakresu od 0 do 59)

    %S

    Sekundy w postaci liczby dziesiętnej (z zakresu od 00 do 59)

    %T

    Odpowiednik bieżącego czasu, identycznie z %H:%M:%S

    Obiekty Date a przedziały czasu Klasa Date oferuje dwie metody pozwalające na pracę z obiektami przedziałów czasu Date_ Span. Za pomocą tych metod można wykonywać proste operacje na obiektach daty, dodając lub odejmując od nich przedziały czasu. Metody te to dodająca przedział czasu metoda addSpan()

    245

    PEAR. Programowanie w PHP

    i odejmująca przedział czasu metoda subtractSpan(). Obie wymagają przesyłania im jako parametru obiektu Date_Span. Oto kod pokazujący, jak zwiększyć datę o dwa dni: $date = new Date('2005-12-24 12:00:00'); $span = new Date_Span('2, 00:00:00'); $date->subtractSpan($span); echo $date->getDate(); // 2005-12-22 12:00:00

    Możliwość dodawania lub odejmowania przedziałów czasu przydaje się w bardzo wielu sytuacjach. Rozważmy na przykład następującą sytuację — chcielibyśmy ustalić, na który dzień miesiąca przypadała druga niedziela w grudniu 2005 roku. W tym celu wystarczy odnaleźć pierwszą niedzielę grudnia 2005 roku i dodać do otrzymanej daty przedział czasu długości jednego tygodnia: $date = new Date('2005-12-01'); // find first Sunday

    while ($date->getDayOfWeek() != 0) { $date = $date->getNextDay(); } // advance to second Sunday

    $date->addSpan(new Date_Span('7,00:00:00')); echo $date->getDate(); // 2005-12-11 00:00:00

    Obsługa stref czasowych za pomocą klasy Date_Timezone Strefą czasową nazywamy obszar Ziemi, na którym obowiązuje ten sam czas lokalny. „Wszystkie strefy czasowe definiowane są względem tzw. Czasu Uniwersalnego (UTC, Coordinated Universal Time). Punktem odniesienia jest tu Południk Zerowy (długość geograficzna 0°), który przebiega przez Królewskie Obserwatorium Astronomiczne w Greenwich na przedmieściach Londynu w Wielkiej Brytanii. Z tego powodu często mówi się potocznie, że czasem bazowym dla wszystkich stref czasowych jest czas Greenwich (GMT, Greenwich Mean Time). Niemniej obecnie obowiązującym czasem odniesienia jest Czas Uniwersalny UTC, mierzony niezależnie od czasu ustalanego na podstawie obserwacji astronomicznych, które dawniej prowadzone były w obserwatorium w Greenwich”. (patrz http://en.wikipedia.org/wiki/Timezone oraz http://pl.wikipedia.org/wiki/Strefa_czasowa) Dodatkowo część krajów zmienia w okresie letnim dla oszczędności strefę czasową, przechodząc na tzw. Czas Letni (ang. daylight saving time — DST). W większości krajów środkowej i zachodniej Europy obowiązuje w zimie tzw. Czas Środkowoeuropejski, CET (od ang. Central European Time, równy UTC+1), natomiast w lecie — Środkowoeuropejski Czas Letni, CEST (od ang. Central European Summer Time, równy UTC+2). 246

    Rozdział 5. • Praca z datami

    Na szczęście pakiet PEAR:Date zawiera również klasę Date_Timezone, która znacznie ułatwia pracę ze strefami czasowymi.

    Tworzenie obiektu Date_Timezone Konstruktor klasy Date_Timezone wymaga przesłania mu pojedynczego argumentu będącego identyfikatorem strefy czasowej, którą chcemy utworzyć. Jeśli identyfikator strefy czasowej będzie prawidłowy, otrzymamy odpowiadający jej obiekt Date_Timezone. Jeśli nie, zostanie utworzony obiekt strefy czasowej dla Czasu Uniwersalnego, UTC. Możemy też uzyskać obiekt Date_Timezone odpowiadający domyślnej strefie czasowej lokalnego systemu, korzystając ze statycznej metody getDefault(). Jeśli natomiast chcielibyśmy zmienić domyślną strefę czasową na inną, można to zrobić za pomocą metody setDefault(), również przywoływanej statycznie. W razie wątpliwości, jaki powinien być właściwy identyfikator strefy czasowej przesyłany konstruktorowi klasy lub metodzie setDefault(), możemy sprawdzić listę wszystkich dostępnych identyfikatorów stref czasowych używając metody getAvailableIDs() lub zweryfikować poprawność identyfikatora za pomocą metody Date_Timezone::isValidID(). Oto przykład korzystania z obu wymienionych metod: require_once 'Date/TimeZone.php'; // TimeZone.php oraz // duże 'Z'

    $validIDs = Date_Timezone::getAvailableIDs(); // tablica zawierająca // około 550 identyfikatorów

    $tz1 = new Date_Timezone('Europe/London'); echo $tz1->getID(); // Europa/Londyn // nieprawidłowa strefa czasowa

    $tz2 = new Date_Timezone('Something/Invalid'); echo $tz2->getID(); // czas uniwersalny (UTC) // domyślna systemowa strefa czasowa

    $default = Date_Timezone::getDefault(); // czas środkowoeuropejski letni (CEST) echo $default->getID();

    Pobieranie informacji na temat strefy czasowej Klasa Date_Timezone udostępnia kilka metod, które pozwalają na sprawdzenie identyfikatora strefy czasowej, jej skrótowej i pełnej nazwy, tego, czy wykonywana jest w niej zmiana na czas letni (DST), oraz paru innych użytecznych informacji. Kolejna tabela zestawia te metody, podając krótkie objaśnienie każdej z nich.

    247

    PEAR. Programowanie w PHP

    Metoda

    Opis

    getID()

    Zwraca identyfikator strefy czasowej.

    getLongName()

    Zwraca pełną nazwę strefy czasowej.

    getShortName()

    Zwraca skrótową nazwę strefy czasowej.

    getDSTLongName()

    Zwraca pełną nazwę czasu DST dla danej strefy czasowej.

    getDSTShortName()

    Zwraca skrótową nazwę czasu DST dla danej strefy czasowej.

    hasDaylightTime()

    Zwraca wartość true (prawda), jeśli w danej strefie czasowej wykorzystywany jest czas DST. W przeciwnym razie zwraca wartość false .

    getDSTSavings()

    Pobiera przesunięcie czasu letniego, DST, dla danej strefy czasowej (Uwaga: obecnie jest ono ręcznie ustawione w kodzie klasy i zawsze wynosi 1 godzinę! Warto jednak wiedzieć, że rzeczywiste przesunięcie czasu DST może być inne, co oznacza, że wartość zwracana przez metodę będzie nieprawidłowa). Zwraca zero, jeśli w danej strefie nie dokonuje się zmiany czasu na letni.

    getRawOffset()

    Zwraca samo przesunięcie danej strefy czasowej względem czasu standardowego (bez uwzględniania zmian związanych z czasem DST).

    Porównywanie obiektów stref czasowych Jak uważni czytelnicy zapewne już się domyślają, istnieją również metody, które umożliwiają porównywanie różnych obiektów Date_Timezone. Prawdę powiedziawszy, przygotowane zostały w tym celu dwie metody — isEqual() i isEquivalent(). Metoda isEqual() sprawdza, czy dwa obiekty stref czasowych określają tę samą strefę czasową, to znaczy czy strefa „Europe/ London” (Europa/Londyn) jest identyczna ze strefą „Europe/London” i żadną inną. Natomiast metody isEquivalent() można użyć, by sprawdzić, czy dwie strefy czasowe mają to samo przesunięcie względem czasu uniwersalnego (UTC) i czy obie strefy uwzględniają zmianę czasu na letni, czy nie. Oto obie metody w działaniu. $london $london2 $berlin $amsterdam

    = = = =

    new new new new

    Date_Timezone('Europe/London'); Date_Timezone('Europe/London'); Date_Timezone('Europe/Berlin'); Date_Timezone('Europe/Amsterdam');

    // UTC // UTC // UTC+1 // UTC+1

    $london->isEqual($london2); // true $london->isEqual($berlin); // false $london->isEquivalent($berlin); // false $berlin->isEquivalent($amsterdam); // true

    Obiekty Date i strefy czasowe Obiekty obu klas Date i Date_Timezone oferują metody umożliwiające obu klasom współpracę ze sobą. Na przykład możemy zmienić strefę czasową obiektu Date przy okazji konwertując — jeśli chcemy (lub też nie) — jego właściwości daty i czasu. Co więcej, możemy sprawdzić, 248

    Rozdział 5. • Praca z datami

    czy bieżąca data i czas zapisane w obiekcie dotyczą czasu letniego lub też sprawdzić przesunięcie względem czasu uniwersalnego (UTC) dla określonej daty i czasu. Kolejna tabela zestawia metody klasy Date ułatwiające pracę ze strefami czasowymi. Metoda

    Opis

    setTZ($tzObj)

    Definiuje obiekt strefy czasowej dla określonego obiektu Date.

    setTZByID($id)

    Określa strefę czasową dla obiektu Date na podstawie podanego identyfikatora strefy czasowej.

    convertTZ($tzObj)

    Konwertuje właściwości daty i czasu obiektu Date na nową strefę czasową, właściwą dla przesłanego jej obiektu strefy czasowej.

    convertTZbyID($id)

    Konwertuje właściwości daty i czasu obiektu Date na nową strefę czasową, właściwą dla przesłanego jej identyfikatora strefy czasowej.

    toUTC()

    Konwertuje właściwości daty i czasu obiektu Date na czas uniwersalny (UTC) i podobnie konwertuje na czas UTC strefę czasową.

    inDaylightTime()

    Sprawdza, czy właściwości daty i czasu obiektu Date są ustawione na czas letni.

    Nikt nie jest doskonały, nawet pakiet PEAR::Date Jeśli chodzi o obsługę stref czasowych, moduł PEAR::Date polega na funkcjach zależnych od systemu operacyjnego, które jednak nie muszą niestety działać poprawnie na każdym komputerze. Zastrzeżenie to dotyczy między innymi metod Date_Timezone::inDaylightTime() oraz Date_Timezone: :getOffset(). Oto wyciąg z dokumentacji tłumaczący przyczyny tego problemu: „OSTRZEŻENIE: Usiłujemy tutaj „przekonać” system operacyjny, aby przekazał nam informację, czy dany czas będzie dla określonej strefy czasowej czasem letnim (DST). Korzystamy w tym celu z funkcji putenv(), która może nie działać w tzw. trybie bezpiecznym (ang. safe mode) i jest zależna od uniksowego systemu mierzenia czasu, co oznacza, że będzie podawać prawidłowe wyniki tylko dla dat z przedziału od 1970 do 2038 roku. Oznacza to również, że nasz sposób zależy od funkcji systemu operacyjnego i zatem może nie działać w systemie Windows lub też w systemie, w którym baza informacji o strefach czasowych zoneinfo nie jest zainstalowana lub nie została poprawnie skonfigurowana”.

    Kolejny przykład ilustruje, w jaki sposób należy korzystać z tych metod. Pokazuje, w jaki sposób przekonwertować obiekt Date ze strefy czasowej „Europe/Berlin” na strefę czasową „Europe/ London”. $date = new Date('2005-12-24 12:00:00'); $date->setTZbyID('Europe/Berlin'); echo $date->getDate(); // 2005-12-24 12:00:00 $date->convertTZbyID('Europe/London'); echo $date->getDate(); // 2005-12-24 11:00:00

    249

    PEAR. Programowanie w PHP

    Przekonaliśmy się już, że klasa Date_Timezone() oferuje metodę getRawOffset() pozwalającą określić przesunięcie danej strefy czasowej względem czasu standardowego (UTC). Metoda ta jednak nie jest w stanie sprawdzić konkretnego obiektu daty ani ustalić, czy nie dotyczy ona przypadkiem czasu letniego (DST). Aby sprawdzić przesunięcie względem czasu uniwersalnego określonego obiektu daty i strefy czasowej, należy przywołać w obiekcie Date_Timezone metodę getOffset(), przesyłając jej jako argument badany obiekt Date. Metoda ta sprawdzi, czy czas zapisany w obiekcie data będzie nie będzie dla określonej strefy czasowej przypadkiem czasem letnim, i zwróci odpowiednie przesunięcie względem czasu uniwersalnego, skorygowane ewentualnie o przesunięcie związane z czasem letnim (DST).

    Pakiet PEAR::Date — podsumowanie Jak mieliśmy okazję się przekonać, pakiet PEAR::Date jest naprawdę wszechstronnym pakietem, który pozwala programiście uniknąć całego bałaganu związanego zazwyczaj z pracą z datami, operacjami matematycznymi na nich oraz uwzględnianiem stref czasowych. Programiści, którzy szukają obiektowego interfejsu API dla dat lub rozwiązania problemu roku 2038, nie muszą już dłużej zaprzątać tym uwagi. Wadą tego pakietu jest jednak to, że w przypadku stref czasowych musi niestety korzystać z metod zależnych od systemu operacyjnego, które mogą nie zadziałać w każdym systemie. Niemniej mimo tego mankamentu jest to nadal znakomity pakiet! Do momentu pojawienia się PHP 5.1 z oferowanym przezeń rozszerzeniem dla dat nie ma w języku PHP lepszego rozwiązania. Dlatego też, jeśli jesteśmy zmuszeni pracować z wersją języka PHP wcześniejszą niż 5.1, najlepiej pozostać przy pakiecie PEAR::Date.

    Pakiet Date_Holidays Jeśli piszemy aplikację, która musi wyliczać daty świąt, pomocnym narzędziem będzie oczywiście pakiet PEAR::Date_Holidays. Jego główne zadanie polega na wyliczaniu dat świąt (i innych specjalnych dni) oraz sprawdzaniu, czy dana data jest datą dnia świątecznego. Ukrywa ona przed programistą całą skomplikowaną mechanikę wyliczania ruchomych świąt, takich jak Wielkanoc (ang. Easter) czy Zielone Świątki (ang. Whitsun). Dodatkowo pozwala on na łatwe odfiltrowywanie różnych świąt, rozpoznaje święta lokalne dla różnych krajów i dostarcza informacji na temat świąt w wielu różnych językach. Nic prostszego niż sprawdzić, czy nasze urodziny w roku 2005 wypadną w dzień świąteczny: require_once 'Date/Holidays.php'; $driver = Date_Holidays::factory('Christian', 2005);

    250

    Rozdział 5. • Praca z datami

    // sprawdzam swoją datę urodzenia ;-)

    if($driver->isHoliday(new Date('2005-09-09'))) { echo 'Co za cudowny dzień! Święto i urodziny na raz.'; } else { echo 'Dziś są moje urodziny.'; }

    Dlatego jeśli nie chcemy na nowo wymyślać koła i pisać od podstaw biblioteki wyliczającej dni świąteczne dla różnych dat i różnych krajów oraz regionów, to najlepiej skorzystać z pakietu Date_Holidays. Zanim zaczniemy pisanie kodu, najpierw kilka podstawowych wiadomości na temat pakietu. Bez obaw jednak — postaramy się, by ten wykład był możliwie krótki.

    Instancjacja sterownika Pakiet Date_Holidays korzysta ze specjalnych klas, które wyliczają święta dla różnych religii, krajów lub regionów świata. Klasy te nazywane są sterownikami. Oznacza to, że możemy podać pakietowi Date_Holidays rok i kraj, dla którego chcemy wyliczyć święta, i pakiet zaoferuje nam odpowiedni sterownik, który będziemy mogli wykorzystać, by zdobyć szczegółowe informacje na temat świąt. Sterownik ten poinformuje, jakie święta rozpoznaje, poda ich daty, pozwoli sprawdzić, czy dana data jest świętem — a to jeszcze nie wszystkie jego możliwości. Instancję sterownika tworzy się za pomocą klasy Date_Holidays. Dostarcza on statycznej metody fabryki, której przesyłamy identyfikator sterownika, rok i odpowiednie ustawienia lokalne, po czym zwraca obiekt sterownika: $driver = Date_Holidays::factory($driverId, $year, $locale); if (Date_Holidays::isError($driver)) { die('Nie udało się utworzyć sterownika: ' . $driver->getMessage()); } else { // ... dalej

    echo 'Sterownik utworzony z powodzeniem!'; }

    W tym przykładzie metodzie Date_Holidays::factory() przesłaliśmy trzy argumenty, z których tylko identyfikator sterownika jest obowiązkowy. Identyfikator sterownika jest nazwą sterownika wyliczającego święta, który utworzymy. Obecnie (tj. w wersji 0.17.0) pakiet oferuje następujące jedenaście sterowników (ich identyfikatory zostały wytłuszczone): „ Christian (Święta chrześcijańskie). „ Composite (Specjalny sterownik łączący jeden lub więcej sterowników. Zachowuje się tak samo jak „zwykły” sterownik. Więcej na ten temat opowiemy dalej, w podrozdziale poświęconym łączeniu różnych sterowników świąt).

    251

    PEAR. Programowanie w PHP

    „ „ „ „ „ „ „ „ „

    Denmark (Święta duńskie). Discordian (Święta dyskordiańskie). Germany (Święta niemieckie). Jewish (Święta żydowskie). PHPdotNet (Daty urodzin kilku znanych ludzi należących do społeczności języka PHP). Slovenia (Święta słoweńskie). Sweden (Święta szwedzkie). UNO (Święta ustanowione przez Organizację Narodów Zjednoczonych). USA (Święta amerykańskie).

    Aby ustalić, jakie sterowniki oferuje nasza wersja pakietu Date_Holidays, można po prostu skorzystać z metody Date_Holudays::getInstalledDrivers(), która zwróci tablicę zawierającą informacje na temat zainstalowanych sterowników.

    Drugi argument to rok, dla którego chcemy wyliczyć daty świąt. Trzeci argument to opcjonalnie przesyłany łańcuch z ustawieniami lokalnymi, z których powinien skorzystać sterownik. Przesyłane w nim ustawienia lokalne wpływać będą na działanie każdej metody sterownika, która zwraca obiekt świąt, nazwę świąt lub inne informacje podlegające lokalnym modyfikacjom. Łańcuch ustawień lokalnych może być dwuliterowym kodem języka zgodnym ze standardem ISO 639 lub też jakąś kompozycją dwuliterowych kodów języków i dwuliterowego kodu kraju zgodnego ze standardem ISO 3166. Na przykład możemy użyć łańcucha "en_GB", by zdefiniować ustawienia lokalne English/United Kingdom (język angielski/Wielka Brytania). Jeśli nie zostaną określone żadne ustawienia lokalne, sterownik skorzysta z domyślnego języka angielskiego. Należy zawsze pamiętać, aby sprawdzać, czy metoda fabryki zwróciła obiekt sterownika, czy może błąd. Pakiet Date_Holidays korzysta z domyślnego mechanizmu obsługi błędów oprogramowani PEAR, dlatego też błąd można bez trudu sprawdzić za pomocą przywoływanej statycznie metody Date_Holidays::isError(). Tworzenie nowych sterowników za pomocą kodów krajów zamiast identyfikatorów sterowników Standard ISO 3166 (patrz http://www.iso.org/iso/en/prods-services/iso3166ma/index.html) definiuje kody identyfikujące różne kraje i terytoria zależne. Kody te mogą składać się z dwóch lub trzech liter lub trzech cyfr. Pakiet Date_Holidays oferuje dodatkową metodę fabryki umożliwiającą tworzenie sterownika za pomocą wspomnianych dwu- lub trzyliterowych kodów zamiast podawania identyfikatora sterownika. Na przykład, aby utworzyć sterownika dla Szwecji (Sweden), należy — zamiast wpisywać jak zwykle Date_Holidays::factory('Sweden') — użyć polecenia Date_Holidays::factoryISO3166('se') lub Date_Holidays::factoryISO3166('swe'). Listę kodów ISO 3166 można znaleźć pod następującym adresem: http://www.unc.edu/~rowlett/units/codes/country.htm.

    252

    Rozdział 5. • Praca z datami

    Jeśli chcemy zmienić rok, dla którego mają zostać wyliczone daty świąt, to należy skorzystać z metody setYear(). Wymaga ona przesłania jej jednego argumentu określającego nowy rok, dla którego mają zostać wyliczone święta. Warto jednak pamiętać, że wywołanie tej metody sprawi, iż sterownik będzie musiał przeliczyć na nowo daty wszystkich świąt w roku, tak więc może być ona pewnym obciążeniem dla komputera.

    Identyfikowanie świąt Aby zidentyfikować dane święto, potrzebować będziemy specjalnego identyfikatora, za pomocą którego będziemy się mogli do niego odwoływać. Identyfikator ten pełni podobną funkcję, jak klucz główny w systemach baz danych. W kontekście pakietu Date_Holidays identyfikator ten nazywany jest nazwą wewnętrzną (ang. internal name). Aby uzyskać informacje na temat określonego święta, należy przesłać sterownikowi wewnętrzną nazwę tego święta. Ustalenie, jakie są wewnętrzne nazwy świąt obsługiwanych przez dany sterownik, ułatwia metoda getInternalHolidayNames(). Oto przykład wykorzystania tej metody: $driver = Date_Holidays::factory($driverId, $year, $locale); $internalNames = $driver->getInternalHolidayNames();

    Przywołana w taki sposób metoda getInternalHolidayNames() zwróci tablicę zawierającą wewnętrzne nazwy świąt dla tego sterownika: Array ( [0] => jesusCircumcision [1] => epiphany [2] => mariaCleaning ... [43] => newYearsEve )

    Znając wszystkie wewnętrzne nazwy świąt obsługiwanych przez dany sterownik, uzyskujemy lepszą wiedzę, jakie święta dany sterownik jest w stanie wyliczać. Wiedza ta przyda nam się na przykład podczas korzystania z metody getHoliday(), o której opowiemy więcej w dalszej części rozdziału.

    Klasa Date_Holidays_Holiday Niektóre z metod sterownika zwracają obiekty klasy Date_Holidays_Holiday. Obiekt tego typu będzie nam dostarczać informacji na temat pojedynczego, wybranego święta. Kolejna tabela zestawia przydatne metody oferowane przez ten obiekt.

    253

    PEAR. Programowanie w PHP

    Metoda

    Opis

    getDate()

    Zwraca obiekt Date zawierający datę świąt.

    getInternalName() Zwraca wewnętrzną nazwę święta. getTitle()

    Zwraca lokalny tytuł święta.

    toArray()

    Zwraca dane święta w postaci tablicy asocjacyjnej. Zawiera ona następujące klucze: "date" (data), "internalName" (wewnętrzna nazwa) i "title" (tytuł).

    Wyliczanie świąt Po lekturze tego krótkiego wprowadzenia czytelnicy powinni już nie mieć wątpliwości co do tego, jak się tworzy obiekty sterowników. Teraz pora dowiedzieć się, jak korzystać ze sterowników, aby za ich pomocą zdobywać informacje na temat różnych świąt.

    Pobieranie informacji na temat świąt Jeśli interesują nas informacje na temat wszystkich świąt, które sterownik może dla nas wyliczyć, to należy skorzystać z metody getHolidays(). Zwróci ona tablicę asocjacyjną, której klucze będą wewnętrznymi nazwami świąt, a wartościami — odpowiadające im obiekty Date_ Holidays_Holiday. Obiekt Date_Holidays_Holiday zawiera kompletne informacje na temat danego święta, takie jak jego wewnętrzna nazwa (dla sterownika), jego tytuł i data. Jeśli interesuje nas tylko tytuł święta, to możemy skorzystać z metody getHolidayTitles(), a jeśli tylko data — z metody getHolidayDates(). Obie zwracają podobną tablicę asocjacyjną, w której kluczami są wewnętrzne nazwy świąt przypisane im przez sterownik, wartościami zaś odpowiednie tytuły świąt lub obiekty daty świat w postaci obiektów PEAR::Date. Oto przykład wykorzystania tych metod w praktyce: $driver = Date_Holidays::factory('Christian', 2005); $holidays = $driver->getHolidays(); // zwraca tablicę asocjacyjną $titles = $driver->getHolidayTitles(); // zwraca tablicę asocjacyjną $dates = $driver->getHolidayDates(); // zwraca tablicę asocjacyjną Pakiet Date_Holidays i daty Pakiet PEAR::Date_Holidays korzysta wewnętrznie z obiektów dat PEAR::Date, by za ich pomocą zapisywać daty świąt. Właściwościom godziny, minut i sekund w tych obiektach zawsze przypisana będzie wartość zero. Ponadto strefą czasową wykorzystywaną w obiektach daty będzie strefa czasu uniwersalnego (UTC).

    254

    Rozdział 5. • Praca z datami

    Jeśli nie potrzebujemy informacji o wszystkich świętach, których dostarcza sterownik, a tylko o pewnym określonym świecie, należy skorzystać z metody getHoliday(). Jako pierwszy argument należy jej przesłać wewnętrzną nazwę święta stosowaną przez sterownik. Zakładając, że chcemy zdobyć informacji na temat Świąt Wielkanocnych, trzeba metodzie przesłać łańcuch "easter", ponieważ właśnie taka jest wewnętrzna nazwa Wielkanocy używana przez sterownik. Niektóre z metod nie zwracają kompletnego obiektu świat, a tylko informacje o tytule i dacie. Są to metody getHolidayTitle() i getHolidayDate(). Podobnie jak metoda getHoliday(), obie metody wymagają przesłania im jako pierwszego argumentu wewnętrznej nazwy święta. Metody te zwracają odpowiednio: tytuł święta lub jego datę w postaci obiektu PEAR::Date. Kolejny przykład pokazuje, jak należy korzystać z wspomnianych metod: $driver = Date_Holidays::factory('Christian', 2005); $holiday = $driver->getHoliday('easter'); // obiekt Date_Holidays_Holiday

    $title

    = $driver->getHolidayTitle('easter');

    $date

    = $driver->getHolidayDate('easter');

    // string(13) "Easter Sunday" // obiekt Date: 2005-03-27

    Filtrowanie wyników Niektóre z opisywanych wcześniej metod zwracają informacje na temat wielu różnych świąt, przesyłając je w odpowiednich tablicach. Domyślnie sterownik zwraca informacje na temat wszystkich znanych mu świąt, czasami jednak potrzebujemy danych tylko na temat wybranych. Prezentowane metody pozwalają zawęzić zakres zwracanych wyników, oferując odpowiednie filtry. Filtr jest to obiekt zawierający wewnętrzne nazwy świąt używane przez sterownik. Jeśli prześlemy go jako argument funkcji, która zwraca listę lub tablicę świąt, filtr zdecyduje, które ze świąt zostaną włączone do zwracanych wyników. Pakiet Date_Holidays obsługuje różne typy filtrów: „ Filtry negatywne (ang. blacklist filters) — elementy zdefiniowane w filtrze zostaną usunięte z listy zwracanych świąt. Klasa tego filtra to Date_Holidays_Filter_Blacklist. „ Filtry pozytywne (ang. whitelist filters) — tylko elementy zdefiniowane w filtrze będą zwracane w liście świąt. Klasa tego filtra to Date_Holidays_Filter_whitelist. „ Filtry złożone (ang. composite filters) — pozwalają na połączenie w jednym filtrze kilku różnych filtrów. Filtr taki zachowuje się zupełnie tak samo jak zwykły filtr. Poszczególne filtry łączy się za pomocą relacji OR. „ Filtry predefiniowane (ang. predefined filters) — są to specjalne filtry przygotowywane w pewnych określonych celach. Na przykład dostępne są filtry, które uwzględniają tylko oficjalne święta państwowe. Obecnie pakiet Date_Holidays oferuje tylko kilka predefiniowanych filtrów.

    255

    PEAR. Programowanie w PHP

    Aby ustalić, jakie filtry oferuje nasza wersja pakietu Date_Holidays, wystarczy przywołać metodę Date_Holidays::getInstalledFilters(), która zwraca tablicę zawierającą informacje na temat wszystkich zainstalowanych filtrów.

    Filtry negatywne i filtry pozytywne tworzone są za pomocą właściwych dla nich konstruktorów, którym przesyła się jeden argument — tablicę zawierającą listę wewnętrznych nazw świąt, które powinny znaleźć się w filtrze. Oto przykład pokazujący, jak tworzymy tego typu filtry i korzystamy z nich: $driver

    = Date_Holidays::factory('Christian', 2005);

    echo count($driver->getHolidays());

    // wyświetla: 44

    $whitelist = new Date_Holidays_Filter_Whitelist( array('goodFriday', 'easter', 'easterMonday')); $wlHolidays = $driver->getHolidays($whitelist); echo count($wlHolidays); // wyświetla: 3 $blacklist = new Date_Holidays_Filter_Blacklist( array('goodFriday', 'easter', 'easterMonday')); $blHolidays = $driver->getHolidays($blacklist); // wyświetla: 41 echo count($blHolidays);

    Jak pokazuje ten przykład, sterownik rozpoznaje 44 różne święta. Jeśli skorzystamy z filtra pozytywnego, metoda getHolidays() zwróci tablicę zawierającą dokładnie trzy elementy (goodFriday — Wielki Piątek, easter — Wielkanoc i easterMonday — Poniedziałek Wielkanocny). Jeśli natomiast skorzystamy z filtra negatywnego zawierającego te trzy elementy, to nie pojawią się one na zwróconej liście świąt. Dlatego też zwrócona tablica będzie zawierać tylko 41 elementów. Korzystając z tych dwóch typów filtrów możemy wybrać dowolny, interesujący nas zestaw świąt. Predefiniowane filtry również są filtrami negatywnymi lub pozytywnymi określającymi, które ze świąt powinny być odrzucane lub powinny pozostać na liście. Oznacza to, że możemy tworzyć instancje predefiniowanych filtrów, korzystając z konstruktorów bezargumentowych. Wszystkie niezbędne wewnętrzne nazwy świąt są już definiowane w odpowiednich klasach. Instancje predefiniowanych filtrów możemy tworzyć używając operatora new i przesyłając go dowolnej funkcji, która pozwala na stosowanie filtrów. Jeśli chcemy skorzystać z kombinacji jednego lub więcej filtrów, można to zrobić za pomocą klasy Date_Holidays_Filter_Composite. Klasa ta służy głównie do łączenia predefiniowanych filtrów, ale oczywiście możemy użyć dowolnej klasy, która jest rozszerzeniem klasy Date_Holidays_Filter. Musimy jedynie przygotować obiekt złożonego filtra i dodać do niego (za pomocą metody addFilter()) lub odjąć od niego (za pomocą metody removeFilter()) odpowiednie obiekty filtrów. Po przygotowaniu takiego złożonego filtra będziemy mogli użyć go do filtrowania świąt. Oto przykład:

    256

    Rozdział 5. • Praca z datami

    require_once 'Date/Holidays.php'; require_once 'Date/Holidays/Filter/Composite.php'; $driver

    = Date_Holidays::factory('Christian', 2005);

    $filter1 = new Date_Holidays_Filter_Whitelist( array('goodFriday', 'easter')); $filter2 = new Date_Holidays_Filter_Whitelist( array('easterMonday')); $composite = new Date_Holidays_Filter_Composite(); $composite->addFilter($filter1); $composite->addFilter($filter2); $holidays = $driver->getHolidays($composite); echo count($holidays); // wyświetla: 3

    Warto zauważyć, że musimy ręcznie dodać plik zawierający klasę Date_Holidays_Filter_ Composite. Plik ten jest bowiem dość rzadko wykorzystywany przez klasę Date_Holidays i dlatego nie jest załączany domyślnie.

    Łączenie sterowników świąt Jak już wspomnieliśmy, programista może połączyć kilka filtrów i traktować je jak jeden normalny filtr. To samo można uczynić z obiektami sterowników. W tym celu należy najpierw utworzyć instancję obiektu klasy komponowanego sterownika Date_Holidays_Driver_Composite za pomocą metody Date_Holidays::factory(). Następnie można będzie dodać do takiego komponowanego sterownika kolejne sterowniki za pomocą metody addDriver() lub odejmować je przy użyciu metody removeDriver(). Obie metody wymagają przesłania im jako argumentu obiektu odpowiedniego sterownika. Ilustruje to kolejny przykład. Jeśli dodamy wewnętrzne nazwy dwóch osobnych sterowników, to przekonamy się, że komponowany sterownik, który powstanie, będzie połączeniem obu tych sterowników. $driver1 = Date_Holidays::factory('Christian', 2005); echo count($driver1->getInternalHolidayNames()); // wyświetla: 44 $driver2 = Date_Holidays::factory('UNO', 2005); echo count($driver2->getInternalHolidayNames()); // wyświetla: 67 $composite = Date_Holidays::factory('Composite'); $composite->addDriver($driver1); $composite->addDriver($driver2); $holidays = $composite->getInternalHolidayNames(); // wyświetla: 111 echo count($holidays);

    257

    PEAR. Programowanie w PHP

    Czy dziś mamy święto? Pakiet Date_Holidays oferuje dwa sposoby sprawdzania, czy dzień o podanej dacie jest świętem. Metoda isHoliday() umożliwia ustalenie, czy określona data jest datą dnia świątecznego. Metodzie tej przesyła się jako pierwszy argument znacznik czasu systemu Unix, łańcuch daty w formacie zgodnym ze standardem ISO 8601 lub obiekt Date. Opcjonalnie można jej przesłać jako drugi argument obiekt filtra. Metoda ta zawsze zwraca wartość logiczną (true lub false). Sprawdźmy dla przykładu, czy 5 maja 2005 roku jest dniem świątecznym: $driver = Date_Holidays::factory('Christian', 2005); if ($driver->isHoliday('2005-05-05')) { echo 'Ten dzień jest świętem!'; } else { echo 'Tego dnia nie ma święta!'; }

    Jeśli chcemy uzyskać informacje, czy istnieje święto powiązane z określoną datą, należy skorzystać z metody getHolidayForDate(). Podobnie jak metoda isHoliday(), wymaga ona przesłania jej w pierwszym argumencie zmiennej zawierającej datę, którą chcemy sprawdzić. Domyślnie metoda zwraca obiekt Date_Holidays_Holiday, jeśli uda się jej znaleźć święto przypisane tej dacie, lub wartość null, jeśli takiego święta nie znajdzie. W prawdziwym świecie może się jednak zdarzyć, że jednej dacie przypisane jest wiele różnych świąt. Prawdopodobieństwo takiego zdarzenia rośnie jeszcze, gdy korzystamy ze sterowników złożonych z paru sterowników. Aby rozwiązać ten problem, możemy skorzystać z trzeciego argumentu i za jego pomocą ustalić, że metoda powinna akceptować również sytuacje, gdy uda jej się dopasować kilka świąt. Jeśli prześlemy w trzecim argumencie wartość logiczną true, to metoda zwróci tablicę zawierającą obiekty dopasowanych świąt, nawet jeśli uda jej się dopasować do daty tylko jedno święto. Dla porządku warto wspomnieć tutaj również o drugim argumencie. Można za jego pomocą zdefiniować ustawienia lokalne. Jeśli prześlemy w nim ustawienia lokalne, to metoda skorzysta z tych właśnie ustawień lokalnych zamiast z ustawień globalnych zdefiniowanych dla danego sterownika. Opowiemy o tym jeszcze dokładniej przy okazji omawiania obsługi tłumaczenia nazw świąt na różne języki. Kolejny przykład pokazuje, jak korzystać z metody getHolidayForDate(): $driver $date

    = Date_Holidays::factory('Christian', 2005); = '2005-05-05';

    // tylko jedna zwracana wartość

    $holiday = $driver->getHolidayForDate($date); if (! is_null($holiday))

    258

    Rozdział 5. • Praca z datami

    { echo $holiday->getTitle(); } // kilka zwracanych wartości

    $holidays = $driver->getHolidayForDate($date, null, true); if (! is_null($holidays)) { foreach ($holidays as $holiday) { echo $holiday->getTitle(); } }

    Przykład ten pokazuje, w jaki sposób zmienia się wartość zwracana przez metodę w zależności od ustawień trzeciego argumentu. Za pierwszym razem zwracany jest pojedynczy obiekt świąt, natomiast za drugim razem tablica. Za pomocą tych metod można również pobierać wszystkie święta, które występują w określonym przedziale czasu. Wystarczy napisać odpowiednią pętlę. Na szczęście pakiet Date_Holidays oferuje metodę getHolidaysForTimespan(), która znacznie ułatwia wyszukiwanie świąt dla określonego przedziału czasu. Ponadto jest ona szybsza, ponieważ wewnętrznie korzysta ze struktury opartej na tablicach asocjacyjnych, w której święta są już indeksowane według swoich dat. Metoda ta zwraca tablicę zawierającą obiekty Date_Holidays_Holiday dla wszystkich świąt w badanym przedziale czasu. Podobnie jak kilka innych metod, które mieliśmy okazję poznać, umożliwia ona dodatkowo określanie w drugim i czwartym argumencie odpowiedniego filtra świąt oraz ustawień lokalnych.

    Tłumaczenie nazw świąt na inne języki Wspomnieliśmy już, że pakiet Date_Holidays oferuje funkcje internacjonalizacji (potocznie w branży informatycznej nazywane I18N). Oznacza to, że potrafi prezentować informacje na temat świąt w różnych językach. Korzysta w tym celu ze specjalnych plików językowych w formacie XML, z których każdy zawiera zestaw tłumaczeń dla świąt obsługiwanych przez określony sterownik. Wspomniane pliki XML zawierają informacje, które umożliwiają nam przypisywanie świętom odpowiednich lokalizowanych tytułów. Kolejny przykład pokazuje wyciąg z pliku zawierającego nazwy świąt chrześcijańskich przetłumaczonych na język francuski:



    jesusCircumcision Circoncision de Jésus

    259

    PEAR. Programowanie w PHP

    epiphany 'épiphanie

    [...]

    Aby korzystać z możliwości internacjonalizacji (I18N), należy poinformować pakiet Date_Holidays, gdzie może znaleźć odpowiednie pliki językowe, i określić ustawienia lokalne sterownika właściwe dla języka, na który chcemy przetłumaczyć nazwy świąt. Odpowiednie pliki językowe zostały zainstalowane w podkatalogu data w katalogu, w którym instalowane jest oprogramowanie PEAR. Pliki językowe umożliwiają nam nie tylko tłumaczenia tytułów (pełnych nazw) świąt, ale również pozwalają na określanie dodatkowych informacji na temat świąt. Za pomocą znacznika (właściwość) możemy określać tak wiele różnych dodatkowych informacji, ile tylko chcemy. Każda właściwość posiadać będzie unikatowy identyfikator (w zakresie pojedynczego święta) i przechowywane w niej dane znakowe. Odpowiedni kod XML dla święta Obrzezania Jezusa Chrystusa (obchodzonego od IV wieku) może wyglądać na przykład tak:

    jesusCircumcision Circoncision de Jésus

    static 4th century

    Nie ma tutaj żadnych predefiniowanych właściwości, których należałoby używać w charakterze identyfikatorów. Wszystko zależy całkowicie od nas. Możemy przechowywać w pliku XML dowolne informacje, jakie tylko chcemy. Możemy wykorzystać te właściwości, by zdefiniować, czy dane święto występuje zawsze tego samego dnia, w celu dostarczenia szczegółowego opisu święta. Wartości właściwości możemy pobierać za pomocą metody Date_Holidays_ Holiday::getProperties(), jeśli dysponujemy obiektem święta, lub też przy użyciu metody getHolidayProperties() obiektu Date_Holidays_Driver. Metody te wymagają przesłania im nazwy święta i identyfikatora odpowiednich ustawień lokalnych.

    Dodawanie nowego pliku językowego Aby dodać plik językowy, należy skorzystać z metod addTranslationFile() i addCompiledTranslationFile() klasy sterownika. Pierwsza metoda umożliwia dodawanie pliku tłumaczenia zawierającego dane XML. Druga natomiast służy do dodawania plików zawierających dane w postaci serializowanej (tzw. plików skompilowanych). Druga metoda działa o wiele szybciej, 260

    Rozdział 5. • Praca z datami

    ponieważ nie musi przetwarzać kodu XML. Obu metodom przesyła się dwa argumenty — absolutną ścieżkę do pliku i ustawienia lokalne definiujące tłumaczenie nazw świat przechowywane w pliku. $driver = Date_Holidays::factory('Christian', 2005); $file = '/var/lib/pear/data/Date_Holidays/lang/Christian/fr_FR.xml'; $driver->addTranslationFile($file, 'fr_FR');

    Po dodaniu w ten sposób pliku tłumaczeń sterownik będzie mógł prezentować informacje na temat świąt w odpowiednim lokalnym języku. Skompilowane pliki językowe mają rozszerzenie .ser i przechowywane są w tym samym katalogu co zwykłe pliki językowe w formacie XML. Możemy nawet przygotować nasze własne pliki językowe i umieścić je w dowolnym wybranym katalogu. Jeśli będą prawidłowe, a pakiet Date_Holidays będzie miał odpowiednie uprawnienia umożliwiające sięganie do nich, to będziemy mogli z nich korzystać. Aby skompilować nasze własne pliki językowe XML, można skorzystać ze skryptu CLI pear-dh-compiletranslationfile dostarczanego przez pakiet Date_Holidays. Skrypt ten oczekuje, że nazwa pliku zostanie konwertowana (może ponadto obsługiwać kilka plików), po czym zapisuje skompilowane dane do pliku używając tej samej bazowej nazwy i rozszerzenia .ser. Więcej informacji na temat skryptu i jego opcji można uzyskać wpisując w wierszu poleceń PHP-CLI pear-dh-compile-translationfile --help: $ pear-dh-compile-translationfile --help Date_Holidays language-file compiler Usage: pear-dh-compile-translationfile [options] filename(s) -d --outputdir= directory. -v --verbose --parameters values(1-...)

    Directory where compiled files are saved. Defaults to the current working Enable verbose mode. Input file(s)

    Pobieranie lokalizowanych danych wyjściowych Definiując odpowiednie ustawienia lokalne możemy decydować o języku, w jakim zwracać będą dane metody sterownika. Ustawienie to może wpływać na działanie całego obiektu sterownika lub tylko pojedynczej metody. Określanie ustawień lokalnych dla całego obiektu sterownika można przeprowadzić na dwa sposoby: 1. Podczas konstruowania obiektu sterownika za pomocą metody Date_Holidays::factory(). Trzeci argument można wykorzystać, by przesłać łańcuch identyfikujący wykorzystywane ustawienia lokalne. 261

    PEAR. Programowanie w PHP

    2. Po skonstruowaniu obiektu sterownika można użyć metody setLocale(), której przesyła się jeden argument w formie łańcucha. Ponadto kilka metod sterownika również pozwala na określanie ustawień lokalnych, które będą wykorzystywane podczas przywoływania danej metody. Mamy tu na myśli metody: getHoliday(), getHolidayForDate(), getHolidays(), getHolidayTitle() i getHolidayTitles(). Każda z tych metod posiada argument umożliwiający określanie ustawień lokalnych. Kolejny przykład pokazuje, w jaki sposób na zwracane dane będą wpływać ustawienia lokalne określane dla sterownika lub metody: // sterownik domyślnie używa tłumaczeń włoskich

    $driver = Date_Holidays::factory('Christian', 2005, 'it_IT'); $driver->addCompiledTranslationFile( '/var/lib/pear/data/Date_Holidays/lang/Christian/it_IT.ser', 'it_IT'); $driver->addCompiledTranslationFile( '/var/lib/pear/data/Date_Holidays/lang/Christian/fr_FR.ser', 'fr_FR'); // korzysta z tłumaczeń domyślnych

    echo $driver->getHolidayTitle('easter') . "\n"; // francuskie tłumaczenie dla wybranej metody

    echo $driver->getHolidayTitle('easter', 'fr_FR') . "\n"; // ustawiamy fr_FR (język francuski) jako domyślne ustawienia lokalne

    $driver->setLocale('fr_FR'); // korzystamy z domyślnego języka, aktualnie francuskiego

    echo $driver->getHolidayTitle('easter') . "\n";

    Jeśli wykonamy ten skrypt, zwróci on nazwę święta Niedzieli Zmartwychwstania Pańskiego po włosku i po francusku: Domenica di Pasqua della Risurrezione dimanche de Pâques dimanche de Pâques

    Warto zauważyć, że nie wszystkie pliki tłumaczeń są kompletne. Oznacza to, że możemy dodać plik językowy (np. z tłumaczeniami francuskimi), określić odpowiednie ustawienia lokalne (w tym przypadku fr_FR), a mimo to nie otrzymamy tłumaczenia święta. Dzieje się tak, gdy plik językowy nie zawiera odpowiedniego tłumaczenia danego święta. Domyślnie przywoływana metoda, gdy natrafi na tego rodzaju problem, będzie zwracać błąd. Możemy jednak to zmienić, korzystając z metody Date_Holidays::staticSetProperty(). Metodzie tej należy przesłać w pierwszym argumencie nazwę właściwości, która powinna zostać zmodyfikowana, a w drugim — nową wartość tej właściwości. Właściwość, którą trzeba zmienić, nosi nazwę

    262

    Rozdział 5. • Praca z datami

    "DIE_ON_MISSING_LOCALE". Jeśli przypiszemy jej wartość false , to w przypadku braku odpowiedniego tłumaczenia nazwy święta, sterownik będzie zwracał domyślną, angielską nazwę tego święta. Jak widać, możemy zdecydować, które z rozwiązań bardziej nam odpowiada. Kolejny przykład pokazuje, jak radzić sobie ze statycznymi właściwościami: $driver = Date_Holidays::factory('Christian', 2005, 'fr_FR'); $driver->addCompiledTranslationFile( '/var/lib/pear5/data/Date_Holidays/lang/Christian/fr_FR.ser', 'fr_FR'); // ustawienia domyślne, nie ma potrzeby definiowania ich wprost

    Date_Holidays::staticSetProperty('DIE_ON_MISSING_LOCALE', true); $title = $driver->getHolidayTitle('whitMonday'); if (Date_Holidays::isError($title)) { echo $title->getMessage(); } else { echo $title; } echo "\n---\n"; // ustawienia domyślne, nie ma potrzeby definiowania ich wprost

    Date_Holidays::staticSetProperty('DIE_ON_MISSING_LOCALE', false); // nie ma potrzeby sprawdzania, czy pojawił się błąd, niemniej // tytuł święta może nie mieć odpowiedniego tłumaczenia lokalnego

    echo $driver->getHolidayTitle('whitMonday') . "\n";

    Skrypt ten zwróci następujący komunikat, informujący, że udało się znaleźć międzynarodową nazwę święta (Whit Monday, Poniedziałek Wielkanocny — lany poniedziałek), natomiast nie znaleziono nazwy lokalnej: The internal name (whitMonday) for the holiday was correct but no localized title could be found --Whit Monday Prosimy o pomoc Zachęcamy autorów, którzy napisali własny sterownik z lokalnymi świętami dla pakietu Date_Holidays, do skontaktowania się z programistami odpowiedzialnymi za obsługę pakietu lub otworzenia żądania nowej funkcji (ang. feature request) na stronie głównej pakietu Date_Holidays, by w ten sposób dodać nową łatę w narzędziu wykrywania błędów (ang. bug tracking tool).

    263

    PEAR. Programowanie w PHP

    Pakiet Date_Holidays — podsumowanie Pakiet Date_Holidays upraszcza wykonywanie obliczeń związanych z odnajdywaniem świąt oraz oferuje narodowe tłumaczenia ich nazw. Obecnie obsługuje sześć sterowników. Większość plików językowych dostępna jest w wersji angielskiej i niemieckiej, a dla niektórych istnieją również wersje francuskie i włoskie. Mamy nadzieję, że zestaw dostępnych sterowników będzie bogatszy w kolejnych wersjach pakietu. Niemniej już w chwili obecnej pakiet ten oferuje dobrze przemyślaną architekturę, którą łatwo można rozbudowywać pisząc własne sterowniki, filtry i pliki językowe.

    Praca z pakietem Calendar Podczas poszukiwań różnych opartych na języku PHP narzędzi oferujących kalendarz znajdziemy w internecie wiele rozwiązań. Jedne z nich są lepsze, inne gorsze. Niemniej w większości przypadków zauważymy, że będą one miały podobne ograniczenia. Niektóre biblioteki mają ręcznie zakodowane nazwy miesięcy i dni lub muszą zwracać dane w ściśle określonym formacie. Pakiet PEAR::Calendar ułatwia generowanie struktur kalendarzy, nie zmuszając programisty do generowania danych w odpowiednim formacie wyjściowym, ani nie wymagając przechowywania za pomocą jednego, dopuszczalnego narzędzia. Pakiet ten upraszcza znacznie generowanie kalendarzy mających postać tabel i pozwala na wyświetlanie danych w dowolnym wybranym formacie (np. HTML, WML, ASCII). Pakiet ten dostarcza klas reprezentujących wszystkie ważne jednostki związane z kalendarzem, takie jak rok, miesiąc, tydzień, dzień, godzina, minuta czy sekunda. Każda z tych związanych z datą klas może być podstawą tworzenia podrzędnych względem niej jednostek. Na przykład obiekt reprezentujący miesiąc może posłużyć do zbudowania zawartych w nim dni tygodni. Przyjrzyjmy się następującemu skryptowi, służącemu do budowania i pobierania obiektów dla każdego dnia w grudniu 2005: // Przełączamy się na mechanizm PEAR::Date

    define('CALENDAR_ENGINE', 'PearDate'); require_once 'Calendar/Month.php'; require_once 'Calendar/Day.php'; $month = new Calendar_Month(2005, 12); // grudzień 2005 $month->build(); // budujemy obiekty dni zawartych w miesiącu // przeglądamy pobrane obiekty dni w pętli

    while ($day = $month->fetch())

    264

    Rozdział 5. • Praca z datami

    { echo $day->getTimestamp() . "\n"; }

    Po uruchomieniu wyświetli on serię znaczników czasu, każdy w osobnym wierszu: 2005-12-01 00:00:00 2005-12-02 00:00:00 ... 2005-12-31 00:00:00

    Większość metod zwraca dane w postaci liczbowej, co jest wielką zaletą podczas budowania aplikacji niezależnych od języka programowania. Mamy możliwość własnoręcznego lokalizowania formatów danych i ich nazw korzystając z odpowiednich funkcji języka PHP lub funkcji pakietu dat PEAR::Date. Pakiet PEAR::Calendar oferuje różne mechanizmy wykonywania obliczeń, między innymi mechanizm znaczników czasu systemu Unix i mechanizm oparty na pakiecie PEAR::Date. Możemy nawet przygotować mechanizm kalendarza dla bardziej złożonych kalendarzy, takich jak na przykład kalendarz chiński. W razie braku jakiejś opcji lub funkcji, można ją będzie bez trudu dodać za pomocą tzw. dekoratorów (ang. decorators). Pakiet PEAR::Calendar oferuje bazowy dekorator, z którego możemy korzystać przygotowując własne dekoratory. W ten sposób mamy gwarancję, że nasze modyfikacje nie zostaną usunięte przez kolejne wersje pakietu Calendar. Na kolejnych stronach wprowadzimy czytelnika w sekrety pakietu PEAR::Calendar i pokażemy, jak korzystać z rozlicznych dobrodziejstw, które on oferuje. Mechanizmy kalendarza Pakiet PEAR::Calendar korzysta z wielu różnych mechanizmów (tzw. silników, ang. engines) umożliwiających wykonywanie obliczeń związanych z datą i godziną. Klasy te, implementujące interfejs Caledndar_Engine, można stosować wymiennie. Obecnie dostępny jest mechanizm oparty na znacznikach czasu systemu Unix (wykorzystywany domyślnie) oraz mechanizm bazujący na pakiecie PEAR::Date. Możemy wybrać, z którego z nich chcemy korzystać, redefiniując w tym celu stałą 'CALENDAR_ENGINE'. Mamy do wyboru dwie możliwości: define('CALENDAR_ENGINE', 'UnixTs') dla znaczników czasu Unixa lub define('CALENDAR_ENGINE', 'UnixTs') dla dat PEAR::Date.

    Podstawowe klasy i pojęcia związane z pakietem Calendar Pakiet PEAR::Calendar oferuje wiele publicznych klas, za pomocą których można rozwiązywać różne problemy. Każda z tych klas przynależy do jakiejś kategorii. Są klasy dat, tabelowe klasy dat, dekoratory i klasy sprawdzające dane (ang. validation clases). Najpierw trzeba zapoznać się podstawowymi klasami dat i tabelowymi klasami dat pakietu Calendar.

    265

    PEAR. Programowanie w PHP

    Każda klasa daty reprezentuje jedną z podstawowych jednostek daty (czasu): rok, miesiąc, dzień, minutę, godzinę lub sekundę. Z kolei tabelowe klasy dat służą głównie do tworzenia kalendarzy w postaci tabel. Klasy należące do obu kategorii wywodzą się z bazowej klasy Calendar i dziedziczą wszystkie jej metody. Diagram UML klasy Calendar został przedstawiony na rysunku poniżej.

    Rysunek 5.1. Diagram UML klasy Calendar

    Kolejna tabela zestawia klasy dat, podaje ścieżki do załączanych plików (reuire/include), krótki opis każdej klasy i nazwy obiektów podrzędnych, które dana klasa tworzy. Klasa

    require/include

    Opis

    Tworzy obiekty

    Calendar_Year

    Calendar/Year.php

    Reprezentuje rok.

    Calendar_Month, Calendar_Month_Weekdays, Calendar_Month_Weeks

    Calendar_Month

    Calendar/Month.php

    Reprezentuje miesiąc.

    Calendar_Day

    Calendar_Day

    Calendar/Day.php

    Reprezentuje dzień.

    Calendar_Hour

    Calendar_Hour

    Calendar/Hour.php

    Reprezentuje godzinę.

    Calendar_Minute

    Calendar_Minute

    Calendar/Minute.php

    Reprezentuje minutę.

    Calendar_Second

    Calendar_Second

    Calendar/Second.php

    Reprezentuje sekundę.

    266

    Rozdział 5. • Praca z datami

    Tabelowe klasy dat natomiast ułatwiają renderowanie (wyświetlanie) kalendarzy w postaci tabel. Dlatego też klasy te pozwalają na definiowanie między innymi takich informacji, jak to, czy dany dzień ma być pusty lub też czy jest pierwszy lub ostatni w tabeli. Kolejny rysunek przedstawiający tabelowy kalendarz na wrzesień 2005 roku pokazuje efekty, które można uzyskać za pomocą takich klas. Pola pustych dni zostały zabarwione kolorem szarym, pierwszy dzień każdego tygodnia — kolorem zielonym, a ostatni — pomarańczowym. Odpowiednie obiekty Calendar_Day zwracają wartość true (prawda), gdy przywoływana jest metoda isEmpty() (sprawdzająca, czy pole jest puste), isFirst() (sprawdzająca, czy to pierwszy dzień tygodnia) lub isLast() (sprawdzająca, czy to ostatni dzień tygodnia).

    Rysunek 5.2. Kalendarz na wrzesień 2005 przygotowany za pomocą tabelowej klasy dat

    Kolejna tabela zestawia tabelowe klasy dat. Klasa

    require/include

    Opis

    Tworzy obiekty

    Calendar_Month Calendar/Month/ ¦_Weekdays Weekdays.php

    Reprezentuje miesiąc i jest w stanie tworzyć zawarte w nim obiekty dni. Oprócz klasy Calendar_Month określa informacje dla stanów isFirst() (jest pierwszy), isLast() (jest ostatni) i isEmpty() (jest pusty) dla każdego z budowanych dni. Można je wykorzystać podczas tworzenia tabeli prezentującej miesiąc.

    Calendar_Day

    Calendar Calendar/Month/ ¦_Month_Weeks Weeks.php

    Reprezentuje miesiąc i jest w stanie tworzyć zawarte w nim obiekty tygodni.

    Calendar_Week

    Calendar_Week

    Reprezentuje w postaci tabeli wybrany tydzień w miesiącu. Jest w stanie tworzyć obiekty dni i w razie potrzeby określa status isEmpty().

    Calendar_Day

    Calendar/Week.php

    267

    PEAR. Programowanie w PHP

    Tworzenie obiektów Konstruktor każdej z bazowych klas daty pozwala na przesyłanie mu liczb całkowitych jako argumentów. Liczba przesyłanych argumentów zależeć będzie od tego, jaki obiekt daty chcemy utworzyć. Zasadniczo trzeba będzie zdefiniować tyle argumentów, ile jest potrzebnych, by dokładnie zlokalizować w czasie daną jednostkę. Dla roku wystarczy określić jeden argument, natomiast podczas tworzenia obiektu Calendar_Day trzeba będzie podać już trzy argumenty. Kolejny przykład pokazuje, jak tworzy się każdą z bazowych klas pakietu Calendar. // klasy dat

    $year = new Calendar_Year(2005); $month = new Calendar_Month(2005, 12); $day = new Calendar_Day(2005, 12, 24); $hour = new Calendar_Hour(2005, 12, 24, 20); $minute = new Calendar_Minute(2005, 12, 24, 20, 30); $second = new Calendar_Second(2005, 12, 24, 20, 30, 40); // tabelowe klasy dat

    $firstDay = 0; // W tabelowej reprezentacji kalendarza pierwszym // dniem tygodnia jest Niedziela

    $monthWkD = new Calendar_Month_Weekdays(2005, 12, $firstDay); $monthWk = new Calendar_Month_Weeks(2005, 12, $firstDay); $week = new Calendar_Week(2005, 12, 24, $firstDay);

    Tabelowe klasy danych pozwalają na przesyłanie im trzeciego argumentu, określającego, który dzień ma być pierwszy. Może to być dowolna liczba od 0 do 6 (niedziela = 0, poniedziałek = 1, …, sobota = 6). Przykład ten ilustruje miłą właściwość pakietu Calendar: załóżmy, że zmienna $week zawiera tydzień z dniem 24 grudnia 2005 roku. W takiej sytuacji wystarczy skorzystać z wywołania $week->thisWeek('n_in_month'), by otrzymać numer tego tygodnia w miesiącu, i z wywołania $week->thisWeek('n_in_year'), by otrzymać numer tygodnia w bieżącym roku.

    Pobieranie informacji Podstawowe klasy kalendarza oferują kilka metod służących do pobierania informacji z określonych obiektów. Są tam między innymi metody umożliwiające ustalenie, jaką datę i godzinę zawiera dany obiekt, oraz metody pozwalające na sprawdzenie, które daty są wcześniejsze, a które późniejsze. Metody te to odpowiednio this*(), prev*() i next*(). Zamiast gwiazdki należy zawsze podać odpowiednią jednostkę daty. Może to być Year (rok), Month (miesiąc), Day (dzień), Hour (godzina), Minute (minuta) lub Second (sekunda). Ponadto klasa Calendar_Week oferuje dodatkowo metody thisWeek(), prevWeek() i nextWeek(), umożliwiające sprawdzenie tych samych informacji dla tygodnia. Kolejny przykład pokazuje, w jaki sposób metody te przywoływane będą w obiekcie Calendar_Day. $day = new Calendar_Day(2005, 12, 24); echo $day->thisYear(); echo $day->thisMonth();

    268

    // wyświetla: 2005 // wyświetla: 12

    Rozdział 5. • Praca z datami

    echo echo echo echo

    $day->thisDay(); $day->thisHour(); $day->thisMinute(); $day->thisSecond();

    // wyświetla: 24 // wyświetla: 0 // wyświetla: 0 // wyświetla: 0

    Metodom this*(), prev*() i next*() można też przesyłać opcjonalny argument, który pozwala programiście wpływać na zwracaną przez nie wartość. W argumencie tym przesyłamy metodzie łańcuch, który określać będzie format zwracanej wartości. Oto możliwe wartości tego łańcucha: „ "int" — jednostka czasu określana jest za pomocą liczby całkowitej; jest to domyślne ustawienie, jeśli nie określimy tego argumentu. „ "timestamp" — metoda zwróci znacznik czasu odpowiadający określonej jednostce kalendarzowej. „ "object" — metoda zwróci obiekt daty; przydaje się podczas korzystania z metod next*() i prev*() sprawdzających wzajemne relacje miedzy datami. „ "array" — zwraca wartość jednostki kalendarzowej w postaci tablicy. Tak więc możliwe argumenty dla metod thisWeek(), prevWeek() i nextWeek() klasy Calendar_ Week to "timestamp", "array", "n_in_month" oraz "n_in_year". Kolejny przykład pokazuje, w jaki sposób należy korzystać z wymienionych wyżej argumentów, by za ich pomocą wpływać na rodzaj wartości zwracanych przez metodę. $second = new Calendar_Second(2005, 12, 24, 20, 30, 40); echo $second->nextDay('int') . "\n"; echo $second->nextDay('timestamp') . "\n"; print_r( $second->nextDay('object') ); print_r( $second->nextDay('array') );

    Kod ten zwróci następujące dane: 25 2005-12-25 00:00:00 Calendar_Day Object ( ! część danych pominęliśmy dla oszczędności papieru ! ) Array ( [year] => 2005 [month] => 12 [day] => 25 [hour] => 0 [minute] => 0 [second] => 0 )

    269

    PEAR. Programowanie w PHP

    Klasa Calendar oferuje jeszcze dwie kolejne metody: getTimestamp() i setTimestamp(). Jak można się domyślić, metoda getTimestamp() zwraca wartość znacznika czasu zapisaną w określonym obiekcie kalendarza, a metoda setTimestamp() umożliwia nam modyfikowanie informacji o dacie i czasie zapisanych w obiekcie. Wartość zwrócona przez metodę getTimestamp() zależeć będzie od wykorzystywanego mechanizmu obliczania daty. Jeśli użyjemy mechanizmu opartego na znacznikach czasu systemu Unix, metoda zwróci właśnie znacznik czasu Uniksa. Jeśli użyjemy mechanizmu opartego na pakiecie PEAR::Date, to metoda zwróci łańcuch w formacie RRRR-MM-DD gg:mm:ss. Warto zauważyć, że wywołanie $day->getTimestamp() będzie miało dokładnie taki sam efekt jak wywołanie $day->thisDay('timestamp').

    Tworzenie i pobieranie Jak wspomnieliśmy we wprowadzeniu do pakietu Calendar, klasy dat i tabelowe klasy dat umożliwiają tworzenie (budowanie) zawartych w nich jednostek kalendarzowych. Każda klasa udostępnia metodę build(), którą można wykorzystać do generowania „obiektów potomnych” bieżącego obiektu. Po skonstruowaniu obiektów potomnych za pomocą metody build() będziemy mogli sięgać do gotowych obiektów potomnych po kolei lub też do wszystkich jednocześnie. Aby przeglądać obiekty potomne jeden po drugim, należy użyć metody fetch(), która korzysta z mechanizmu iteratora. Każde kolejne jej wywołanie zwracać będzie następny z obiektów potomnych, aż do momentu, gdy dotrzemy do końca zbioru — wtedy metoda fetch() zwróci wartość false . W ten sposób będziemy mogli wygodnie przeglądać wszystkie obiekty potomne w pętli while. Kolejny przykład pokazuje, jak korzystać z tego iteratora. Kod ten może się wydawać znajomy, jako że mieliśmy już okazję oglądać go we wprowadzeniu do pakietu Calendar. $month = new Calendar_Month(2005, 12); // Grudzień 2005 $month->build(); while ($day = $month->fetch()) { echo $day->getTimestamp() . "\n"; }

    Przedstawiony tu skrypt zbuduje obiekty dni w miesiącu grudniu roku 2005 i wyświetli odpowiednio sformatowaną datę każdego dnia: 2005-12-01 00:00:00 2005-12-02 00:00:00 ... 2005-12-31 00:00:00

    Aby pobrać od razu wszystkie elementy potomne, można skorzystać z metody fetchAll(), która zwróci tablicę zawierającą wszystkie obiekty daty dla wszystkich elementów potomnych. W zależności od oryginalnej klasy daty zwrócona tablica będzie indeksowana począwszy od 0 lub 1. Dla klas Calendar_Year, Calendar_Month, Calendar_Month_Weekdays, Calendar_Month_Weeks i Calendar_Week tablica będzie indeksowana począwszy od 1. Dla klas Calendar_Day, Calendar_Hour, Calendar_Minute i Calendar_Second pierwszym indeksem tablicy będzie 0. Czytelnicy,

    270

    Rozdział 5. • Praca z datami

    którzy zastanawiają się, dlaczego przyjęto taką konwencję, powinni przyjrzeć się tabelom tworzonym dla klas dat i tabelowych klas dat i zastanowić się, jakiego rodzaju obiekty potomne tworzy dana klasa. Klasy budujące obiekty potomne dla godzin minut i sekund będą zwracać tablice indeksowane od 0. Opisane tutaj zasady budowania i pobierania obiektów potomnych sprawiają, że tworzenie obiektów kalendarza staje się bardzo proste i nie wymaga skomplikowanych obliczeń. Obiekty potomne nie są budowane automatycznie w momencie tworzenia obiektu rodzica, a dopiero gdy będziemy ich potrzebować i skonstruujemy je za pomocą metody build().

    Dokonywanie wyboru Metoda build() pozwala na specjalne oznaczanie elementów podczas ich konstruowania. Znakowanie to jest wykonywane, kiedy określamy indeksowaną tablicę obiektów daty, które zostaną wybrane. Kiedy metoda build() generuje element potomny, porównuje go z elementami tablicy i jeśli znajdzie element idealnie pasujący do tworzonego, to generowany element potomny zostaje wybrany. Po wybraniu elementu przywołanie na nim metody isSelected() sprawdzającej zaznaczenie zwróci wartość true (prawda). Możemy wykorzystać tę opcję do oznaczania dni, które powinny zostać wyróżnione w zwracanym kalendarzu. Kolejny przykład pokazuje, jak korzystać z tej możliwości wybierania elementów. $month = new Calendar_Month(2005, 12); $stNicholas = new Calendar_Day(2005, 12, 6); $xmasEve = new Calendar_Day(2005, 12, 24); $selection = array($stNicholas, $xmasEve); $month->build($selection); while ($day = $month->fetch()) { if ($day->isSelected()) { echo $day->getTimestamp() . "\n"; } }

    Skrypt ten wyświetli następujące informacje: 2005-12-06 00:00:00 2005-12-24 00:00:00

    Obiekty w tablicy wybranych elementów $selection, pasujące do zbudowanych już obiektów potomnych, zastąpią je. Oznacza to, że metody fetch() lub fetchAll() zwracać będą obiekty, które umieścimy w tablicy wybranych elementów. Dzięki temu będziemy mogli wstawiać do obiektów kalendarza swoje własne obiekty. Zazwyczaj robi się to rozszerzając klasę Calendar_ Decorator, która jest bazową klasą dla dekoratorów. Więcej na temat korzystania z dekoratorów (ang. decorators) opowiemy w dalszej części, przy okazji rozważań nad modyfikowaniem zachowań standardowych klas.

    271

    PEAR. Programowanie w PHP

    Sprawdzanie poprawności obiektów kalendarza Pakiet PEAR::Calendar oferuje specjalne klasy sprawdzające (ang. validation clases), które służą do testowania poprawności dat w kalendarzu. Aby wykonać proste sprawdzenie poprawności dat, wystarczy przywołać metodę isValid() dla każdej podklasy klasy Calendar. Metoda ta zwraca wartość true (prawda), jeśli data jest prawidłowa, lub false , jeśli data nie jest poprawna. Aby umożliwić bardziej dokładne sprawdzanie poprawności dat, każda z podstawowych klas zwraca za pośrednictwem metody getValidator() specjalny obiekt Calendar_Validator. Obiekt ten oferuje zestaw użytecznych metod, które pozwalają na bardziej precyzyjne rozpoznanie przyczyny błędu. Metody klasy Calendar_Validator zostały zestawione w kolejnej tabeli. Metoda

    Opis

    fetch()

    Przegląda w pętli wszystkie błędy.

    isValid()

    Sprawdza, czy określony obiekt kalendarza jest poprawny. Przywołuje wszystkie metody z grupy isValid*().

    isValidDay()

    Sprawdza, czy dany dzień w obiekcie kalendarza jest poprawny.

    isValidHour()

    Sprawdza, czy dana godzina w obiekcie kalendarza jest poprawna.

    isValidMinute()

    Sprawdza, czy dana minuta w obiekcie kalendarza jest poprawna.

    isValidMonth()

    Sprawdza, czy dany miesiąc w obiekcie kalendarza jest poprawny.

    isValidSecond()

    Sprawdza, czy dana sekunda w obiekcie kalendarza jest poprawna.

    isValidYear()

    Sprawdza, czy dany rok w obiekcie kalendarza jest poprawny.

    Oto kod pokazujący, w jaki sposób sprawdza się obiekty daty w kalendarzu: $day = new Calendar_Day(2005, 13, 32); if (! $day->isValid()) { echo "Data dnia jest nieprawidłowa! \n"; // bardziej precyzyjne sprawdzanie dat

    $validator = $day->getValidator(); if (! $validator->isValidDay()) { echo "Nieprawidłowy dzień: " . $day->thisDay() . "\n"; } if (! $validator->isValidMonth()) { echo "Nieprawidłowy miesiąc: " . $day->thisMonth() . "\n"; } if (! $validator->isValidYear()) { echo "Nieprawidłowy rok: " . $day->thisYear() . "\n"; } }

    272

    Rozdział 5. • Praca z datami

    Kod ten wyświetli następujące informacje: Data dnia jest nieprawidłowa! Nieprawidłowy dzień: 32 Nieprawidłowy miesiąc: 13

    Sprawdzanie poprawności a wprowadzanie poprawek Zamiast tylko sprawdzać poprawność obiektów daty, można je od razu poprawiać tak, by zawierały prawidłowe daty. W tym celu wystarczy tylko skorzystać z metody adjust(). Metoda ta zmieni nieprawidłowe daty na poprawne. Na przykład 32 dzień 13 miesiąca 2005 roku zostanie poprawiony na 1 lutego 2006 (2006-02-01): $day = new Calendar_Day(2005, 13, 32); $day->adjust(); // wyświetla: 2006-02-01 00:00:00 echo $day->getTimestamp();

    Obsługa błędów wykrytych podczas sprawdzania dat Klasa Calendar_Validator pozwala na przeglądanie po kolei wykrytych błędów za pomocą metody fetch(). Metoda ta zwraca obiekty błędów Calendar_Validation_Error lub wartość false , jeśli nie wykryto żadnego błędu. Wspomniany obiekt błędu udostępnia cztery metody: getMessage(), getUnit(), getValue() i toString(). Metody te opisaliśmy w kolejnej tabeli. Metoda

    Opis

    getMessage()

    Zwraca komunikat o błędzie związany z nieprawidłową wartością. Komunikaty te są w języku angielskim, niemniej można je modyfikować redefiniując stałe CALENDAR_VALUE_TOOSMALL i CALENDAR_VALUE_TOOLARGE.

    getUnit()

    Zwraca nieprawidłową jednostkę daty. Może to być jedna z następujących jednostek: "Year" (rok), "Month" (miesiąc), "Day" (dzień), "Hour" (godzina), "Minute" (minuta), "Second" (sekunda).

    getValue()

    Zwraca wartość nieprawidłowej jednostki danych. Będzie to ta sama liczba całkowita, którą zwróciłyby metody thisYear(), thisMonth() itd.

    toString()

    Zwraca łańcuch zawierający komunikat o błędzie, jednostkę i wartość jednostki. Prawdę powiedziawszy, jest kombinacją trzech poprzednich metod.

    Dzięki tym metodom możemy dokładnie ustalić przyczyny niepoprawności daty. Przyjrzyjmy się kolejnemu przykładowi, który pokazuje, w jaki sposób można przeglądać i wyświetlać informacje o błędach. $day = new Calendar_Day(2005, 13, 32); if (! $day->isValid()) {

    273

    PEAR. Programowanie w PHP

    $validator = $day->getValidator(); while ($error = $validator->fetch()) { echo sprintf("Nieprawidłowa data: jednostka %s, wartość %s. Uzasadnienie: %s \n", $error->getUnit(), $error->getValue(), $error->getMessage()); } }

    Skrypt ten wyświetli następujące informacje: Nieprawidłowa data: jednostka Month, wartość 13. Uzasadnienie: Too large: max = 12 Nieprawidłowa data: jednostka Day, wartość 32. Uzasadnienie: Too large: max = 31

    Modyfikowanie działania standardowych klas Dekoratory (ang. decorators) pozwalają programiście na dodawanie do obiektów kalendarza własnych funkcji i opcji. Zaletą korzystania z dekoratorów jest to, że nie musimy już ręcznie rozszerzać żadnej z głównych klas kalendarza.

    Czym właściwie są dekoratory? Dekoratory umożliwiają nam dynamiczne rozszerzanie funkcji wybranego obiektu. Dekorator robi to obudowując obiekt, który ma zostać zmodyfikowany, zamiast go rozszerzać. Zaleta tego rozwiązania polega na tym, że możemy wybrać, który obiekt powinien zostać w ten sposób „dekorowany”, zamiast zmieniać od razu działanie wszystkich obiektów wybranej klasy. W normalnych warunkach dekorator oczekuje, że obiekt, który ma być dekorowany, zostanie mu przesłany jako argument w momencie konstruowania i będzie oferował taki sam interfejs API, jak obudowany przez niego dekorator lub też interfejs wzbogacony o parę metod. Zdefiniowany w ten sposób dekorator potrafi sam decydować, czy określona wywoływana metoda będzie przetwarzana przez dekorowany obiekt, czy też wręcz przeciwnie — zwracana przez nią wartość nie będzie poddawana żadnej modyfikacji.

    Wspólna bazowa klasa dekoratorów Pakiet PEAR::Calendar oferuje bazową klasę dekoratorów: Calendar_Decorator. Klasa ta udostępnia programiście interfejs API będący połączeniem wszystkich podklas klasy Calendar. W konstruktorze klasy Calendar_Decorator należy przesłać jako argument obiekt typu Calendar. Tak utworzona klasa dekoratora jeszcze niczego nie dekoruje, a tylko przesyła wszystkie odwołania do metod dekorowanemu obiektowi Calendar, który przekazywaliśmy konstruktorowi dekoratora.

    274

    Rozdział 5. • Praca z datami

    W ten sposób zaoszczędzamy sobie wiele pracy podczas tworzenia własnych dekoratorów, ponieważ musimy teraz już tylko rozszerzyć klasę Calendar_Decorator bez konieczności implementowania jakiejkolwiek metody zajmującej się delegowaniem wywołań. Jak już wspomnieliśmy przy okazji opisywania wyboru elementów, istnieje możliwość dodawania do obiektów własnych funkcji. Wystarczy w tym celu przesłać tablicę tychże obiektów metodzie build() obiektu daty kalendarzowej. Każdy obiekt w takiej tablicy będzie musiał zaimplementować publiczny interfejs API klasy Calendar. Najlepszym sposobem tworzenia własnych klas spełniających te wymagania jest tworzenie ich jako rozszerzeń klasy Calendar_ Decorator.

    Standardowe dekoratory Pakiet PEAR::Calendar dostarcza kilku standardowych dekoratorów, które mogą okazać się przydatne w pewnych sytuacjach. Aby skorzystać z tych klas, należy dołączyć je do naszego skryptu. Listę standardowych dekoratorów prezentuje kolejna tabela. Dekorator

    Opis

    Calendar_Decorator_Textual

    Ułatwia pobieranie tekstowych reprezentacji miesięcy i dni tygodnia. Jeśli istotna jest dla nas wydajność, lepiej jednak zamiast z dekoratora korzystać z klasy Calendar_Util_Textual.

    Calendar_Decorator_Uri

    Pomaga przy tworzeniu łącz HTML służących do nawigowania w obrębie kalendarza. Jeśli istotna jest dla nas wydajność, lepiej jednak zamiast z dekoratora korzystać z klasy Calendar_Util_Uri.

    Calendar_Decorator_Weekday

    Ułatwia pobieranie dni tygodnia.

    Calendar_Decorator_Wrapper

    Ułatwia obudowywanie obiektów potomnych zbudowanych w innym dekoratorze. Dekoruje metody fetch() i fetchAll() oraz pozwala na określanie nazwy dekoratora, który będzie obudowywać pobierany obiekt.

    Generowanie danych w formie graficznej Kalendarze w witrynach WWW większość ludzi wyobraża sobie jako kalendarze w formie tabelek, umożliwiające użytkownikom nawigowanie pomiędzy poszczególnymi miesiącami, tygodniami i dniami w roku. Pakiet PEAR::Calendar oferuje kilka użytecznych funkcji, które ułatwiają ich tworzenie. Teoretycznie możemy przygotować kalendarz tak szczegółowy, że będzie nam umożliwiał nawigowanie nie tylko pomiędzy poszczególnymi miesiącami, ale również minutami. Metody isFirst(), isLast() i isEmpty() klasy CalendarDay ułatwiają budowanie struktury kalendarza w formie tabeli. Aby na przykład przygotować kalendarz w formie tabeli dla września 2005 roku, należy skorzystać z następującego kodu:

    275

    PEAR. Programowanie w PHP

    // Wrzesień 2005, pierwszym dniem miesiąca jest poniedziałek (Monday)

    $month = new Calendar_Month_Weekdays(2005, 9, $firstDay = 1); $month->build(); // lokalizowany tekst dla nagłówka kalendarza

    $header = strftime('%B %Y', $month->thisMonth('timestamp')); echo isEmpty()) { echo '
     '.$Day->thisDay().'
    ';

    276

    Rozdział 5. • Praca z datami

    Kiedy obiekt dnia Calendar_Day informuje, że jest pierwszym dniem tygodnia (metoda isFirst() zwraca wartość true), tworzony jest nowy wiersz. Puste dni (dla których metoda is Empty() zwraca true) renderowane są za pomocą jednostki „twardej” spacji ( ), a po dniach, które informują, że są ostatnimi dniami w tygodniu (metoda isLast() zwraca wartość true), skrypt kończy kolejny wiersz tabeli. Otrzymana tabela miesiąca wyświetlana w przeglądarce została ukazana na rysunku 5.3:

    Rysunek 5.3. Kalendarz przygotowany w postaci tabeli

    Kalendarze w formie tabel z opcjami nawigacyjnymi Na typowej stronie nie będziemy jednak zazwyczaj ograniczać się do renderowania statycznego kalendarza, ale raczej przygotujemy taki, który za pomocą odpowiednich opcji nawigacyjnych (łącz) umożliwi użytkownikowi przeglądanie różnych miesięcy, tygodni, dni i innych jednostek kalendarzowych. Pakiet PEAR::Calendar dostarcza dwóch klas ułatwiających renderowanie (wyświetlanie) łącz służących do poruszania się w obrębie kalendarza. Są to klasy Calendar_Util_Uri i Calendar_Decorator_Uri. Obie rozwiązują w zasadzie ten sam problem. Jeśli zależy nam na wydajności, należy korzystać raczej z klasy Calendar_Util_Uri. Konstruktorowi klasy można przesłać od jednego do sześciu argumentów. Można wykorzystać je, by określić nazwy parametrów żądań używanych dla roku, miesiąca, dnia, godziny, minuty i sekundy. Tak więc obiekt utworzony za pomocą wywołania $foo = new Calendar_Util_Uri('y', 'm', 'd') będzie generował łańcuchy identyfikatorów zasobów URI o następującej postaci: "y=2005&m= 9&d=9". Im bardziej rozdrobnione nazwę określimy, tym więcej parametrów zawierać będzie łańcuch URI. Klasa dostarcza trzech metod: prev(), next() i this(), które zwracać będą łańcuchy URI odpowiednio dla: poprzedniej, następnej i bieżącej jednostki daty. Metodom tym przesyła się jako pierwszy argument odpowiednią podklasę klasy Calendar, a jako drugi argument łańcuch identyfikujący jednostkę danych. Łańcuch ten musi zawierać jedną z następujących wartości: "year" (rok), "month" (miesiąc), "week" (tydzień), "day" (dzień), "hour" (godzina), "minute"

    277

    PEAR. Programowanie w PHP

    (minuta) lub "second" (sekunda). Kolejny przykład jest rozwiniętą wersją kodu z poprzedniego przykładu. Tym razem dodaliśmy do nagłówka kalendarza strzałki, które umożliwiają użytkownikowi przejście do poprzedniego lub następnego miesiąca. // pobieramy informacje o dacie z żądania lub używamy bieżącej daty

    $y = isset($_GET['year']) ? $_GET['year'] : date('Y'); $m = isset($_GET['month']) ? $_GET['month'] : date('m'); $month = new Calendar_Month_Weekdays($y, $m, $firstDay = 1); $month->build(); // Lokalizowany tekst dla nagłówka kalendarza

    $header = strftime('%B %Y', $month->thisMonth('timestamp')); // Obiekt Calendar_URI_Util dla generowania łączy nawigacyjnych

    $uriUtil = new Calendar_Util_Uri('year', 'month'); $nextM = $uriUtil->next($month, 'month'); $prevM = $uriUtil->prev($month, 'month'); echo setDay($endDate->getDaysInMonth()); $driver = Date_Holidays::factory('Christian', $y, $locale); if (Date_Holidays::isError($driver)) { die('Nie udało się utworzyć sterownika: ' . $driver->getMessage()); } $holidays = $driver->getHolidaysForDatespan($startDate, $endDate); if (Date_Holidays::isError($holidays)) { die('Błąd podczas pobierania świąt: ' . $holidays->getMessage()); } // tworzymy tablicę wybranych dni zawierającą dekorowane obiekty // dla metody build()

    $selection = array(); foreach ($holidays as $holiday) { $date = $holiday->getDate(); $day = new Calendar_Day($date->getYear(), $date->getMonth(), $date->getDay()); $selection[] = new Calendar_Decorator_Holiday($day, $holiday); } $month = new Calendar_Month_Weekdays($y, $m, $firstDay = 1); $month->build($selection); // Lokalizowany tekst dla nagłówka kalendarza

    $header = strftime('%B %Y', $month->thisMonth('timestamp')); // Klasa Calendar_URI_Util generująca łącza nawigacyjne

    $uriUtil = new Calendar_Util_Uri('year', 'month'); $nextM = $uriUtil->next($month, 'month'); $prevM = $uriUtil->prev($month, 'month'); echo isEmpty()) { echo ' '; } else { if ($day->isSelected()) { echo ''. $day->thisDay() . ''; } else { echo ''.$day->thisDay().'';

    281

    PEAR. Programowanie w PHP

    } } if ($day->isLast()) { echo "\n"; } } echo '';

    Jak widać, cały ten przykład zawiera nie więcej niż sto wierszy kodu, a tworzy bardzo estetyczny kalendarz w formie tabelki, który nie tylko zawiera odpowiednie łącza nawigacyjne, ale również podświetla święta. Gdybyśmy spróbowali poprawić ten przykład, staranniej rozdzielając kod CSS, HTML i PHP, to byłby nawet bardziej zwięzły. Jakie wspaniałe efekty można osiągnąć za pomocą pakietów dostępnych w sekcji Date and Time (data i czas) repozytorium PEAR! Wynik działania tego skryptu przedstawiliśmy na kolejnym rysunku. Można by co najwyżej dodać jeszcze parę wierszy kodu poprawiających estetykę naszego kalendarza.

    Rysunek 5.4. Gotowa tabela kalendarza

    Podsumowanie Jak widać, sekcja Date and Time (data i czas) repozytorium PEAR oferuje trzy znakomite pakiety oprogramowania. Każdy z pakietów został starannie zaprojektowany, dzięki czemu pisane za ich pomocą aplikacje będą działały naprawdę szybko i efektywnie. Wielką zaletą trzech omawianych pakietów jest to, że można z nich korzystać razem bez obaw, iż kod napisany za pomocą jednego będzie niekompatybilny z drugim. Zarówno pakiet kalendarza PEAR::Calendar, jak i pakiet świąt Date_Holidays przystosowane są do korzystania z klas daty oferowanych przez pakiet PEAR::Date. Oczywiście funkcje daty i czasu oferowane przez sam język PHP będą działać szybciej, niemniej jeśli potrzebujemy obiektowego interfejsu API, który byłby jednocześnie wygodny w użyciu i oferował odpowiednio bogaty zestaw możliwości, to wspomniane pakiety są zapewne najlepszym rozwiązaniem.

    282

    Skorowidz @deprecated, 222 _call(), 112, 113, 117, 220 _sleep(), 129, 130 , 64 , 64 , 129

    A addCompiledTranslationFile(), 260 addFilter(), 256 addFormat(), 74 addRow(), 65 addSeconds(), 239 addSpan(), 245 addStylesheet(), 137 addTab(), 140 addTranslationFile(), 260 adjust(), 273 adres URL, 201 after(), 240 Album, 103 alterTable(), 44 Amazon, 190 associate ID, 192 dodatkowe usługi, 198 dokumentacja interfejsu API, 194 identyfikator partnera, 192

    ItemLookup(), 199 ItemSearch(), 198 przeszukiwanie witryny, 192 rozpatrywanie odpowiedzi od usługi, 196 tworzenie konta, 191 analiza dane RSS, 169 dane XML, 143 API Google, 182 API Metabase, 16 API SAX, 143 aplikacje Mozilla, 133 WWW, 175 appendChild(), 137, 139, 140 application ID, 200 arkusze kalkulacyjne Excela, 71 arkusze stylów CSS, 137 Artist, 103 ASP.NET, 84 ATTRIBUTE_CLASS, 126, 165 ATTRIBUTE_KEY, 126, 165 ATTRIBUTE_TYPE, 126, 165 ATTRIBUTES_ARRAYKEY, 159, 165 ATTRIBUTES_KEY, 127 ATTRIBUTES_PARSE, 158, 165 ATTRIBUTES_PREPEND, 165 autoExecute(), 41 auto-increment fields, 34 autoPrepare(), 40

    PEAR. Programowanie w PHP

    B baza danych, 15 indeksy, 46 ograniczenia, 45 tabele, 44 tworzenie kopii bazy, 58 before(), 240 beginTransaction(), 41 BIFF5, 71 bind(), 86 bindParam(), 38 bindParamArray(), 39 bindValue(), 39 Bitmap(), 82 blog, 185 buffered_result_class:, 53 build(), 270 buildManipSQL(), 40

    C Calendar, 65, 264 adjust(), 273 build(), 270, 279 dekoratory, 265, 271, 274 diagram UML, 266 dokonywanie wyboru, 271 fetch(), 270 fetchAll(), 270 generowanie danych w formie graficznej, 275 getMessage(), 273 getTimestamp(), 270 getUnit(), 273 getValidator(), 272 getValue(), 273 isEmpty(), 267 isFirst(), 267 isLast(), 267 isSelected(), 271 kalendarz w formie tabel z opcjami nawigacyjnymi, 277 klasy dat, 265, 266 klasy dekoratorów, 274 klasy sprawdzające, 265, 272 mechanizmy, 265 modyfikowanie działania standardowych klas, 274 nextWeek(), 268, 269

    284

    obsługa błędów wykrytych podczas sprawdzania dat, 273 pobieranie informacji, 268 prevWeek(), 268, 269 renderowanie, 267 setTimestamp(), 270 sprawdzanie poprawności obiektów, 272 standardowe dekoratory, 275 tabelowe klasy dat, 265, 267 thisWeek(), 268, 269 toString(), 273 tworzenie, 270 tworzenie obiektów, 268 wprowadzanie poprawek, 273 Calendar_Day, 266, 268, 279 Calendar_Decorator, 271, 274 Calendar_Decorator_Textual, 275 Calendar_Decorator_Uri, 275, 277 Calendar_Decorator_Weekday, 275 Calendar_Decorator_Wrapper, 275 Calendar_Hour, 266 Calendar_Minute, 266 Calendar_Month, 266 Calendar_Month_Weekdays, 267 Calendar_Month_Weeks, 267 Calendar_Second, 266 Calendar_Util_Uri, 277 Calendar_Validation_Error, 273 Calendar_Validator, 272 Calendar_Week, 267 Calendar_Year, 266 CalendarDay, 275 call_user_func_array(), 229 callback, 49 cDataHandler(), 147, 155 cell(), 96 CEST, 246 CET, 246 CLASSNAME_AS_TAGNAME, 126 COMMENT_KEY, 127 commit(), 41 compare(), 240, 244 COMPLEXTYPE, 165 concat(), 46 ConfigReader, 148 connect(), 21 CONTENT_KEY, 127, 165 Content-Type, 83 convertTZ(), 249

    Skorowidz

    convertTZbyID(), 249 count(), 32 createConstraint(), 45 createDatabase(), 43, 61 createDocument(), 137 createElement(), 137, 138 createEndElment(), 107 createStartElement(), 106, 107, 109 createTable(), 44 createTag(), 107 CSS, 137 CSV, 83 cudzysłowy, 31 CURRENT_TIMESTAMP, 46 czas GMT, 235 Czas Letni, 246 Czas Środkowoeuropejski, 246 Czas Uniwersalny, 246 czcionki, 96

    D dane RSS, 169 DataGrid, 84 DATAGRID_RENDERER_TABLE, 89 DataSource, 85, 87 Datatype, 43 Date, 235, 236, 249, 250 addSeconds(), 239 after(), 240 before(), 240 compare(), 240 convertTZ(), 249 convertTZbyID(), 249 Date_Span, 242 Date_Timezone, 247 equals(), 240 formatowanie dat, 241 getDate(), 237 getDayName(), 238 getDayOfWeek(), 238 getDaysInMonth(), 238 getNextDay(), 238 getPrevDay(), 238 getQuarterOfYear(), 238 getTime(), 237 getWeekInMonth(), 238 getWeekOfYear(), 238 inDaylightTime(), 249

    isFuture(), 238 isLeapYear(), 238 isPast(), 238 modyfikowanie obiektów daty, 239 porównywanie dat, 240 przedziały czasu, 245 setDate(), 239 setDay(), 239 setHour(), 239 setMinute(), 239 setMonth(), 239 setSecond(), 239 setTZ(), 249 setTZByID(), 249 setYear(), 239 strefy czasowe, 248 subtractSeconds(), 239 toUTC(), 249 tworzenie obiektu daty, 236 wydobywanie informacji o czasie, 237 zmienne zastępcze, 241 Date and Time, 235 DATE_FORMAT_ISO, 237 DATE_FORMAT_ISO_BASIC, 237 DATE_FORMAT_ISO_EXTENDED, 237 DATE_FORMAT_ISO_EXTENDED_ ¦MICROTIME, 237 DATE_FORMAT_TIMESTAMP, 237 DATE_FORMAT_UNIXTIME, 237 Date_Holidays, 67, 250, 264 addCompiledTranslationFile(), 260 addDriver(), 257 addTranslationFile(), 260 daty, 254 dodawanie pliku językowego, 260 factory(), 251 filtrowanie wyników, 255 filtry negatywne, 255 filtry pozytywne, 255 filtry predefiniowane, 255 filtry złożone, 255 getHoliday(), 262 getHolidayDate(), 255 getHolidayForDate(), 258, 262 getHolidays(), 262 getHolidayTitle(), 255, 262 getHolidayTitles(), 262 getInstalledDrivers(), 252 getInstalledFilters(), 256

    285

    PEAR. Programowanie w PHP

    Date_Holidays getInternalHolidayNames(), 253 identyfikacja świąt, 253 instancja sterownika, 251 internacjonalizacja, 259 isError(), 252 isHoliday(), 258 łączenie sterowników świąt, 257 nazwy wewnętrzne, 253 pobieranie lokalizowanych danych wyjściowych, 261 removeDriver(), 257 staticSetProperty(), 262 tłumaczenie nazw świąt na inne języki, 259 tworzenie sterowników, 252 Date_Holidays_Driver, 260 getHolidaysForDateSpan(), 279 Date_Holidays_Driver_Composite, 257 Date_Holidays_Filter, 256 Date_Holidays_Filter_Blacklist, 255 Date_Holidays_Filter_Composite, 256 Date_Holidays_Filter_whitelist, 255 Date_Holidays_Holiday, 253, 254, 279 getDate(), 254 getInternalName(), 254 getTitle(), 254 toArray(), 254 Date_Span, 242 add(), 243 compare(), 244 equal(), 244 format(), 245 formatowanie przedziałów czasu, 245 greater(), 244 greaterEqual(), 244 isEmpty(), 245 konwersja przedziałów czasu, 244 lower(), 244 lowerEqual(), 244 modyfikowanie obiektów, 243 porównywanie przedziałów czasu, 244 setFromArray(), 243 setFromDateDiff(), 243 setFromDays(), 243 setFromHours(), 243 setFromMinutes(), 243 setFromSeconds(), 243 setFromString(), 243 subtract(), 243 toDays(), 244

    286

    toHours(), 244 toMinutes(), 244 toSeconds(), 244 Date_Timezone, 246, 247 getDefault(), 247 getDSTLongName(), 248 getDSTSavings(), 248 getDSTShortName(), 248 getID(), 248 getLongName(), 248 getRawOffset(), 248, 250 getShortName(), 248 hasDaylightTime(), 248 isEqual(), 248 isEquivalent(), 248 isValidID(), 247 pobieranie informacji na temat strefy czasowej, 247 porównywanie obiektów stref czasowych, 248 setDefault(), 247 tworzenie obiektu, 247 daty, 235 Calendar, 264 czas GMT, 235 Date, 235 Date_Timezone, 246 formatowanie, 241 kalendarz, 264 porównywanie, 240 przedział czasu, 242 strefy czasowe, 246 święta, 254 DBAL, 16 DECODE_FUNC, 166 DEFAULT_CLASS, 165 DEFAULT_TAG, 126, 129 deklaracja XML, 108 dekorator, 265, 274 deserializacja etykiet, 166 DISCO, 219 disconnect(), 23 dispatch map, 214 dni świąteczne, 250 DOCTYPE, 125 DOCTYPE_ENABLED, 125 doGoogleSearch(), 184 dokumenty DISCO, 219 PDF, 93 WSDL, 216, 217

    Skorowidz

    dokumenty XML, 99 atrybuty, 101, 122 element nadrzędny, 101 generowanie, 106 informacje o nagraniach, 102 konwersja, 124 pakiety PEAR, 100 prawidłowo sformułowany, 101 przetwarzanie, 142, 155 tworzenie, 101, 106 tworzenie z drzewa obiektów, 127 XML_FastCreate, 110 XML_Serializer, 118 XML_Util, 106 XML_XUL, 133 zestaw znaków, 101 znaczniki, 101 dokumenty XUL, 133, 134 tworzenie, 136 DOM, 136 DOM-API, 136 dropConstraint(), 45 DSN, 20 DST, 246, 247, 250 dumpDatabase(), 58, 60

    E ENCODE_FUNC, 127 ENCODING_SOURCE, 166 ENCODING_TARGET, 166 endElement(), 147 ENTITIES, 126 equal(), 244 equals(), 240 examples.getStateName(), 177 Excel 2003, 83 Excel_SpreadSheet_Writer, 71 addFormat(), 74, 75 arkusz kalkulacyjny, 72 BIFF5, 71 Bitmap(), 82 CSV, 83 deseń na pierwszym planie, 76 dodawanie formatowania, 74 Excel 2003, 83 FgColor, 76 format Excela, 71

    formatowanie liczb, 77 formaty, 79 formuły, 79 freezePanes(), 80 generator faktur, 81 insertBitmap(), 82 kolory, 75 komórki, 73 nagłówki Content-Type, 83 obramowania, 80 obrazki, 80 opcje formatowania, 74 OpenDocument, 84 przygotowanie strony do wyświetlenia, 74 rowcolToCell(), 73, 80 send(), 72 setCustomColor(), 76 techniki tworzenia arkuszy, 83 wiele arkuszy kalkulacyjnych, 80 writeBlank(), 82 writeFormula(), 80 writeNote(), 82 writeNumber(), 82 wypełnianie barwnym deseniem, 77 exec(), 25 executeMultiple(), 39 exGetAssoc(), 42 expat, 142 ext/dom, 143 ext/simplexml, 143 ext/xml, 143 ext/xmlreader, 143 Extended, 35, 43 extends, 153

    F factory(), 21, 94, 251 faultCode(), 179, 213 feed, 169 fetch mode, 23 fetch(), 270, 272 fetchAll(), 25, 270 fetchCol(), 26 fetchOne(), 25 fetchRow(), 25 FgColor, 76 file_get_contents(), 156

    287

    PEAR. Programowanie w PHP

    File_PDF, 93 cell(), 96 czcionki, 96 factory(), 94 kolory, 95 komórki, 96 nagłówki, 97 newLine(), 97 orientacja strony, 94 setDrawColor(), 96 setFillColor(), 96 setFont(), 96 setXY(), 93 stopki, 97 tworzenie strony, 94 fitToPages(), 74 FORCE_ENUM, 166 foreach, 108 format CSV, 83 Excel, 71 NNSV, 243 OpenDocument, 84 PDF, 92 tabele HTML, 64 XML, 99, 101 format(), 245 formatowanie daty, 241 kod XML, 115 przedziały czasu, 245 formuły, 79 free(), 38 freezePanes(), 80 Function, 43, 46 funkcje zwrotne PHP, 49

    G generator faktur, 81 generowanie arkusz kalkulacyjny, 71 dokumenty XML, 102 pliki Excela 2003, 83 pliki PDF, 92 getAll(), 27, 28 getArtists(), 218, 225 getAssoc(), 28, 42, 43 getAverageAge(), 55

    288

    getCol(), 31 getDate(), 237, 254 getDay(), 237 getDayName(), 238 getDayOfWeek(), 238 getDaysInMonth(), 238 getDBInstance(), 56 getDebugOutput(), 33, 49 getDefault(), 247 getDefinitionFromDatabase(), 58, 60 getDSTLongName(), 248 getDSTSavings(), 248 getDSTShortName(), 248 getHelp(), 196 getHoliday(), 262 getHolidayDate(), 255 getHolidayDates(), 254 getHolidayForDate(), 258, 262 getHolidayProperties(), 260 getHolidays(), 254, 262 getHolidayTitle(), 255, 262 getHolidayTitles(), 254, 262 getHour(), 237 getID(), 248 getInstalledDrivers(), 252 getInternalHolidayNames(), 253 getInternalName(), 254 getLongName(), 248 getMessage(), 273 getMinute(), 237 getMonth(), 237 getNextDay(), 238 getOne(), 31 getPaging(), 87 getPrevDay(), 238 getQuarterOfYear(), 238 getRawOffset(), 248, 250 getRecords(), 209, 210, 213, 218, 225 getRecordsService(), 210 getRootName(), 156 getRow(), 27 getSecond(), 237 getSerializedData(), 119 getShortName(), 248 getStateName(), 177 getTime(), 237 getTimestamp(), 270 getTitle(), 254 getUnit(), 273

    Skorowidz

    getValidator(), 272 getValue(), 273 getWeekInMonth(), 238 getWeekOfYear(), 238 getXMLDeclaration(), 108 getYear(), 237 GMT, 235, 246 Google, 176, 182 klucz API, 182 SOAP, 182 Google_Package, 176 greater(), 244 greaterEqual(), 244 GUESS_TYPES, 166

    H hasDaylightTime(), 248 Help(), 194 hideGridlines(), 74 HTM, 69 HTML_Table, 65, 89 addRow(), 65 definiowanie pojedynczych komórek, 67 kalendarz, 65 setCellAttributes(), 66 setColAttributes(), 66 setRowAttributes(), 66 HTML_Table_Matrix, 69 next(), 69 sterowniki, 69 HTTP, 177, 185 HTTP_Request, 199, 204, 205 HTTP_Serializer, 199

    I I18N, 260 identyfikacja świąt, 253 IGNORE_KEYS, 166 IGNORE_NULL, 126 inDaylightTime(), 249 indeksy, 46 INDENT, 125 INDENT_ATTRIBUTES, 126 insertBitmap(), 82 instalacja MDB2, 19 instrukcje preparowane, 37

    internacjonalizacja, 260 inTransaction(), 41 introspekcja, 227 invokeArgs(), 229 isEmpty(), 245, 267 isEqual(), 248 isEquivalent(), 248 isError(), 252 isFirst(), 267 isFuture(), 238 isHoliday(), 258 isLast(), 267 isLeapYear(), 238 isPast(), 238 isSelected(), 271 isValid(), 272 isValidDay(), 272 isValidHour(), 272 isValidID(), 247 isValidMinute(), 272 isValidMonth(), 272 isValidName(), 110 isValidSecond(), 272 isValidYear(), 272 ItemLookup(), 199 ItemSearch(), 192, 196, 198 Iterator, 32, 55 iteratory, 32, 55

    J język PHP, 15 XUL, 133

    K kalendarz, 65, 264 dni świąteczne, 67 klucz API Google, 182 klucz główny, 45 kod SQL, 17 komórki tabeli, 64 komponent DataGrid, 84 konwersja dane CSV na XLS, 88 przedziały czasu, 244 kopia bazy danych, 58

    289

    PEAR. Programowanie w PHP

    L Label, 104 libxml2, 99 libxsl, 99 LIMIT, 17, 35 limitQuery(), 35 limity zapytań, 35 LINEBREAKS, 125 listDatabases(), 46 listTableFields(), 46 listTables(), 46 listTableViews(), 46 listUsers(), 46 listy, 46 loadFile(), 32 loadModule(), 18, 42, 56 lower(), 244 lowerEqual(), 244

    Ł łączenie się z bazą danych, 20

    M magiczne metody, 112 Manager, 43 mapa rozsyłania, 214 mapowanie kodu XML na obiekty, 160 MDB2, 15 alterTable(), 44 autoExecute(), 41 autoPrepare(), 40 beginTransaction(), 41 bindParam(), 38 bindParamArray(), 39 bindValue(), 39 buforowana klasa wyników, 53 buildManipSQL(), 40 commit(), 41 concat(), 46 connect(), 21 createConstraint(), 45 createDatabase(), 43 createTable(), 44 cudzysłowy, 31 Datatype, 43 debug, 33

    290

    debugowanie, 33 definiowanie trybu pobierania danych, 23 disconnect(), 23 dropConstraint(), 45 exec(), 25 executeMultiple(), 39 exGetAssoc(), 42 Extended, 43 factory(), 21 fetchAll(), 25 fetchCol(), 26 fetchOne(), 25 fetchRow(), 25 free(), 38 Function, 43, 46 funkcje, 18, 46 getAll(), 27, 28 getAssoc(), 28, 42, 43 getCol(), 31 getDebugOutput(), 33, 49 getOne(), 31 getRow(), 27 historia, 16 indeksy, 46 informacje na temat tabeli, 47 instalacja, 19 instalacja sterownika, 19 instrukcje preparowane, 37 interfejs API, 18 inTransaction(), 41 iteratory, 32, 55 klasa pobierająca dane, 51 klasa sterownika, 21 klasa wyników, 52 limity zapytań, 35 listy, 46 loadFile(), 32 loadModule(), 18, 42, 56 log_line_break, 33 ładowanie modułów, 42 łączenie się z bazą danych, 20 Manager, 43 moduły, 42, 56 modyfikowanie tabeli, 44 Native, 43 nextId(), 34 nextResult(), 26 now(), 46 obsługa subselektów, 36

    Skorowidz

    ograniczenia, 45 określanie typów danych, 29, 31 określanie typów danych podczas pobierania wyników, 30 opcje, 21 parametry z nazwami, 38 persistent, 22 pobieranie danych, 25, 26 podzapytania, 37 podzapytania nieskorelowane, 37 połączenia trwałe, 22 portability, 22 prepare(), 38 prędkość, 17 przeprojektowanie bazy danych, 43 query(), 25 queryAll(), 26 queryCol(), 26, 31 queryOne(), 26, 31 queryRow(), 26 quote(), 31 quoteIdentifier(), 31 rejestracja w dzienniku, 49 replace(), 36 Reverse, 43, 47 rollback(), 41 rowCount(), 26 rozłączanie się z bazą danych, 23 sekwencje, 34 setFetchMode(), 23 setOption(), 21, 33, 49 setOptions(), 21 setResultTypes(), 30 singleton(), 21 sterowniki, 18, 19, 43 subSelect(), 36 subselekty, 36 substring(), 46 tableInfo(), 47, 48 tablice sekwencji, 34 transakcje, 41 tryb pobierania danych, 23 tworzenie bazy danych, 43 tworzenie instancji obiektu, 21 tworzenie iteratorów, 55 tworzenie modułów, 56 tworzenie tabeli, 44 typy danych, 29 ujmowanie danych w cudzysłowy, 31

    warstwa abstrakcji kodu SQL, 34 warstwy abstrakcji, 16 wiązanie danych, 38 własne rozszerzenia, 49 wykonywanie wielu wierszy na raz, 39 wykonywanie zapytań, 25 wyszukiwanie błędów, 33 zastępowanie zapytań, 36 MDB2_BufferedIterator, 32, 55 MDB2_Driver_mysql, 21 MDB2_Extended, 43 MDB2_FETCHMODE_FLIPPED, 23 MDB2_FETCHMODE_OBJECT, 51, 52 MDB2_Iterator, 32, 55 MDB2_Module_Common, 43, 56 MDB2_PORTABILITY_ALL, 22 MDB2_PORTABILITY_LOWERCASE, 22 MDB2_Result, 25, 52 MDB2_Result_Common, 52 MDB2_Result_mysql, 25, 52, 53 MDB2_Schema, 57 createDatabase(), 61 dumpDatabase(), 58, 60 getDefinitionFromDatabase(), 58, 60 instalacja, 4, 57 instancja, 57 tworzenie kopii bazy danych, 58 zmiana bazy danych, 61 MDB2_SCHEMA_DUMP_ALL, 60 MDB2_SCHEMA_DUMP_CONTENT, 60 MDB2_SCHEMA_DUMP_STRUCTURE, 60 MDB2_Statement, 38, 40 mod_rewrite, 226 MODE, 126 model DOM, 136, 138 moduły MDB2, 42 modyfikowanie tabeli, 44

    N NAMESPACE, 126 Native, 43 nazwa DSN, 20 łańcuch, 20 tablica, 20 newLine(), 97 nextId(), 34 nextResult(), 26 nextWeek(), 268, 269

    291

    PEAR. Programowanie w PHP

    NNSV, 243 non-correlated sub queries, 37 now(), 46

    O obiekty danych, 15 obsługa strefy czasowe, 246 treść RSS, 169 odbicie, 227 odwołanie, 25 oferowanie usług WWW, 208 mapa rozsyłania, 214 obsługa błędów, 213 REST, 223 SOAP, 216 XML_Serializer, 223 XML-RPC, 208 zarządzanie błędami, 221 opcode cache, 18 OpenDocument, 84 overloading, 42

    P package.search(), 181 PAGER, 87 pakiety oprogramowania, 11 pamięć podręczna kodów operacji, 18 parent::_construct(), 153 parse(), 147 PDF, 92 PDO, 15 PEAR, 11 pear install, 19 PEAR::Calendar, 65, 264 PEAR::Date, 235 PEAR::MDB, 15, 16 PEAR_Error, 41, 147, 192 persistent, 22 PHP, 15 PHP Data Objects, 15 pliki PDF, 92 pobieranie danych z bazy danych, 25 podzapytania, 37 nieskorelowane, 37 połączenie z bazą danych, 20

    292

    porównywanie daty, 240 obiekty stref czasowych, 248 przedziały czasu, 244 portability, 22 POST, 200 prepare(), 38 prepared statements, 37 PREPEND_ATTRIBUTES, 126 prevWeek(), 268, 269 primary key, 45 print_r(), 27, 47 printArea(), 74 protokoły REST, 185 SOAP, 182 XML-RPC, 177 przeciążanie, 42, 112 przedział czasu, 242 przestrzenie nazw XML, 107 przeszukiwanie blog, 185 witryna Amazon.com, 192 przetwarzanie dokumentów XML, 142 rozszerzenia PHP5, 143 XML_Parser, 143 XML_Unserializer, 155

    Q query(), 25 queryAll(), 26 queryCol(), 26, 31 queryOne(), 26, 31 queryRow(), 26 quote(), 31 quoteIdentifier(), 31

    R RDBMS, 17 RDF Site Summary, 169 Really Simple Syndication, 169 recordAlbum(), 103 RecordLabel, 217 reflection, 227 rejestracja w dzienniku, 49 removeFilter(), 256 render(), 86

    Skorowidz

    REPLACE, 36 replace(), 36 ResponseGroup, 196, 197 REST, 176, 185 adres URL, 201 błędy, 231 oferowanie usług WWW, 223 przeszukiwanie blogów, 185 tworzenie usługi, 199, 225 Yahoo!, 200 żądania HTTP, 228 RETURN_RESULT, 126, 166 Reverse, 43, 47 reverse engineering, 43 reverseEntities(), 110 rewind(), 32 Rich Site Summary, 169 rollback(), 41 ROOT_ATTRIBS, 126 ROOT_NAME, 126, 129 rowcolToCell(), 73, 80 rowCount(), 26 rozłączenie z bazą danych, 23 RSS, 91, 169 analiza danych, 169

    S SAX, 142, 143 SCALAR_AS_ATTRIBUTES, 126, 129 SeekableIterator, 32 sekwencje, 34 SELECT, 37 sendFault(), 229, 230 sendResult(), 229, 230 sequence tables, 34 serializacja, 118 serialize(), 118, 119 Services_Amazon, 191, 198 Services_AmazonECS4, 191, 194, 196 Services_Ebay, 176 Services_Google, 183, 216 doGoogleSearch(), 184 Iterator, 183 klucz API, 183 pamięć podręczna, 184 search(), 183 Services_Technorati, 185 cosmos, 189 keyInfo(), 189

    klucz API, 186 użycie, 186 Services_Webservice, 217, 219 Services_Webservice_SOAP, 219 Services_Yahoo, 69 setCellAttributes(), 66 setCellContens(), 68 setColAttributes(), 66 setCustomColor(), 76 setDate(), 239 setDay(), 239 setDefault(), 247 setDrawColor(), 96 setFetchMode(), 23, 52 setFillColor(), 96 setFillstart(), 70 setFont(), 96 setFooter(), 74 setFormatter(), 92 setFromArray(), 243 setFromDateDiff(), 243 setFromDays(), 243 setFromHours(), 243 setFromMinutes(), 243 setFromSeconds(), 243 setFromString(), 243 setHeader(), 74 setHour(), 239 setInputFile(), 147 setLandscape(), 74 setMargins(), 74 setMinute(), 239 setMonth(), 239 setOption(), 21, 33, 46, 49, 53, 121 setOptions(), 21, 121 setPaper(), 74 setPortrait(), 74 setPrintScale(), 74 setResultTypes(), 30 setRowAttributes(), 66 setSecond(), 239 setSection(), 162 setters, 161 setTimestamp(), 270 setTZ(), 249 setTZByID(), 249 setXY(), 93 setYear(), 239 SGV, 100

    293

    PEAR. Programowanie w PHP

    siatka danych, 84 singleton(), 21 slacarval(), 214 SML, 57 SOAP, 175, 176, 182, 201, 220 informacje o błędzie, 183 oferowanie usług WWW, 216 sprawdzanie poprawności obiektów kalendarza, 272 Spreadsheet::WriteExcel, 71 Spreadsheet_Excel_Writer, 88 SQL, 17 startElement(), 147, 155 staticSetProperty(), 262 strefy czasowe, 246 Date, 248 pobieranie informacji, 247 stronicowanie wyników, 87 Structures_DataGrid, 84 bind(), 86 DataSource, 85 dodawanie kolumn, 91 formatowanie siatki danych, 89 poszerzanie możliwości DataGrid, 90 render(), 86 renderery, 85, 88 siatka danych, 86 sterownik RSS, 91 stronicowanie wyników, 87 źródła danych DataSource, 85, 87 Structures_DataGrid_Column, 92 Structures_DataGrid_Columns, 92 subSelect(), 36 subselekty, 36 substring(), 46 subtractSeconds(), 239 subtractSpan(), 246 SUM(), 80 system zarządzania bazą danych, 17

    Ś Środkowoeuropejski Czas Letni, 246 święta, 250, 253, 254 pobieranie informacji, 254 sprawdzanie, 258 tłumaczenie nazw na języki, 259 wyliczanie, 254

    294

    T tabele HTML, 64 format tworzenia, 64 HTML_Table, 65 komórki, 64 tableInfo(), 47, 48 tablice sekwencji, 34 TAG_AS_CLASSNAME, 165 TAG_MAP, 165 TAGMAP, 126 techniki tworzenia arkuszy kalkulacyjnych, 83 Technorati, 185 Technorati cosmos, 189 thisWeek(), 268, 269 tłumaczenie nazw świąt na języki, 259 toArray(), 254 toDays(), 244 toHours(), 244 toMinutes(), 244 toSeconds(), 244 toString(), 273 toUTC(), 249 toXML(), 111 transakcje, 41 tworzenie aplikacje Mozilli, 133 arkusz kalkulacyjny, 72 baza danych, 43 dokumenty XML, 101, 110, 118 dokumenty XUL, 136 formuły, 79 instancja obiektu MDB2, 21 iteratory, 55 kopia bazy danych, 58 moduły, 56 obiekty przechowujące informacje o nagraniach, 102 tabele, 44 usługi WWW oparte na REST, 199 usługi XML-RPC, 209 TYPEHINTS, 126, 131 typy danych MDB2, 29

    U ujmowanie wartości w cudzysłowy, 31 unserialize(), 118, 157, 206 UPDATE, 36

    Skorowidz

    URL, 201 UrlFetcher, 130 usługi REST, 225 usługi WWW, 175 Amazon, 190 API Google, 182 faultCode(), 179 korzystanie z usług, 176 oferowanie usług, 208 przeszukiwanie blogów, 185 ResponseGroup, 196 REST, 176, 185, 200 rozpatrywanie odpowiedzi od usługi, 196 Services_Amazon, 191 Services_Google, 183 Services_Technorati, 185 SOAP, 175, 176, 182 WSDL, 217 XML_RPC_Client, 178 XML_RPC_decode(), 180 XML_RPC_Message, 179 XML_RPC_Response, 179 XML_RPC_Value, 179 XML-RPC, 177 Yahoo!, 200 zarządzanie błędami, 221 usort(), 240 UTC, 246

    V VMWare, 18

    W warstwa abstrakcji baza danych, 15, 16 interfejs bazy danych, 16 kod SQL, 17 typy danych, 17 web logi, 169 Web Service Describing Language, 216 WHITESPACE, 166 workbook, 74 writeBlank(), 82 writeFormula(), 80 writeNote(), 82 writeNumber(), 82 WSDL, 216, 217

    WWW, 175 wykonywanie zapytań, 25 wyliczanie świąt, 254 wyszukiwanie błędów, 33 wyświetlanie danych, 63 tabele HTML, 64

    X XML, 99, 100 XML Remote Procedure Cal, 175 XML Schema, 100 XML_Beautifier, 115 XML_DECL_ENABLED, 125 XML_ENCODING, 125 XML_FastCreate, 100, 110 factory(), 111 kodowanie znaków, 114 problemy, 117 przeciążanie, 112 sterowniki, 115 toXML(), 111 tworzenie dokumentu XML, 116 zagnieżdżanie znaczników, 113 XML_Handler, 147 XML_Parser, 100, 143, 145, 168 cDataHandler(), 147, 155 dodawanie kodu do funkcji zwrotnych, 149 endElement(), 147 implementacja funkcji zwrotnych, 146 konstruktor, 149 konwersja kodowania, 154 opcje konfiguracyjne, 152 parse(), 147 przesuwanie kursora, 144 setInputFile(), 147 startElement(), 147, 155 sygnatury metod, 146 unikanie dziedziczenia, 153 XML_Parser_Simple, 155 XML_RPC_Client, 178 XML_RPC_decode(), 180 XML_RPC_Message, 179, 210, 211, 213, 214, 216 XML_RPC_Response, 179, 210 XML_RPC_Server, 211, 215 XML_RPC_Value, 179, 210, 214 XML_RSS, 169 feed, 169 kanały, 171

    295

    PEAR. Programowanie w PHP

    XML_RSS, lista z informacjami o najnowszych publikacjach, 172 wersja 2 RSS, 172 XML_Serializer, 100, 118 _sleep(), 129 DEFAULT_TAG, 125 dodawanie atrybutów, 122 dodawanie informacji o typie, 131 getSerializedData(), 119 informacje o typie, 131 konwersja danych, 124 oferowanie usług WWW, 223 opcje, 121, 125 przebieg pracy, 119 przesyłanie obiektów, 129 serialize(), 119, 121 setOption(), 121 setOptions(), 121 tablice indeksowane, 123 tworzenie dokumentu XML z drzewa obiektów, 127 XML_Serializer_Tag, 124 XML_SERIALIZER_OPTION, 121 XML_Tree_Node, 115 XML_Unserializer, 100, 155, 168, 205 analiza atrybutów, 158 ATTRIBUTE_CLASS, 165 ATTRIBUTE_KEY, 165 ATTRIBUTE_TYPE, 165 ATTRIBUTES_ARRAYKEY, 159, 165 ATTRIBUTES_PARSE, 158, 165 ATTRIBUTES_PREPEND, 165 COMPLEXTYPE, 165 CONTENT_KEY, 165 DECODE_FUNC, 166 DEFAULT_CLASS, 165 deserializacja etykiet, 166 ENCODING_SOURCE, 166 ENCODING_TARGET, 166 file_get_contents(), 156 FORCE_ENUM, 166 getRootName(), 156 getters, 164 GUESS_TYPES, 166 IGNORE_KEYS, 166 mapowanie kodu XML na obiekty, 160

    296

    opcje, 165 RETURN_RESULT, 166 setSection(), 162 setter, 164 TAG_AS_CLASSNAME, 165 TAG_MAP, 165 unserialize(), 157 WHITESPACE, 166 XML_UNSERIALIZER_OPTION_ ¦COMPLEXTYPE, 160 znaczniki, 161 XML_UNSERIALIZER_OPTION_ ¦COMPLEXTYPE, 160 XML_Util, 100, 106 createEndElment(), 107 createStartElement(), 106, 107 createTag(), 107 getXMLDeclaration(), 108 isValidName(), 110 reverseEntities(), 110 XML_XUL, 133, 136 addStylesheet(), 137 addTab(), 140 appendChild(), 137, 139, 140 arkusze stylów CSS, 137 createDocument(), 137 createElement(), 137, 138 kontrolka listy, 135 tworzenie dokumentów XUL, 136 tworzenie karty z zakładkami, 140 tworzenie skryptu, 136 zakładki, 140 XML_XUL_Element, 137, 140 XML_XUL_Element_Tree, 138 XML_XUL_Element_Treeitem, 139 XML-RPC, 175, 176, 177, 201 oferowanie usług WWW, 208 XML-RPC-Server, 210 XUL, 133

    Y Yahoo Image Search, 69 Yahoo!, 200 identyfikator aplikacji, 200 interfejs API, 202 usługi REST, 200

    Skorowidz

    Z zarządzanie błędami, 221 zastępowanie zapytań, 36 zmiana bazy danych, 61 zmienne zastępcze, 241 znacznik czasu systemu Unix, 237 znaczniki XML, 101

    Ź źródła danych DataSource, 87

    297

    , 64 , 64