PHP

Enostaven MySQL wrapper za PHP

nedelja, 22. april 2007 dan zemlje

Opažam, da ima večina začetnikov težave z escapanjem narekovajev v MySQL poizvedbah, skrbi jih SQL injection (ko izvedo zanj), probleme z njim je imel tudi samooklicani "prvi slovenski internetni časopis". Ponujam enostavno rešitev.

SQL wrapperjev za PHP obstaja malo morje, večina je glomaznih, tale podpira sicer samo MySQL, je pa zelo enostaven. Podobno zadevo uporabljam tudi v mojih projektih, s kar nekaj naprednimi možnostmi, ki pa bi precej zakomplicirale tole, pa jih bom rajši preskočil.

Najprej DB razred:

class DB {

protected $connection;

public function __construct(
$dbHost,
$dbUser,
$dbPass,
$dbDatabase
) {
if ($this->connection = mysql_connect(
$dbHost,
$dbUser,
$dbPass
))
mysql_select_db(
$dbDatabase,
$this->connection
);
else
die('Cannot connect!');
}

public function query(
$sqlExpression,
$paramList = NULL
) {
$sqlExpression = preg_replace(
'!\$([1-9][0-9]*)!e',
'$this->getParam($paramList[\\1-1])',
$sqlExpression
);
return new DB_RESULT(
mysql_query(
$sqlExpression,
$this->connection
)
);
}

protected function getParam(
$value
) {
if (is_numeric($value))
$returnVal = $value;
else
$returnVal =
'\''.
mysql_real_escape_string(
$value,
$this->connection
).
'\'';
return $returnVal;
}

}

Nobenih hudih posebnosti, konstruktor razreda se priklopi na podatkovni strežnik, metoda query() pa izvede poizvedbo, parametre poizvedbi ji podamo kot array. V poizvedbi parametre označimo z znakom $ in zaporedno številko takoj za njim, recimo: $1, $2, $3, ... Poglejmo primer:

$DB = new DB(
'localhost', // database host
'foo', // database user
'bar', // database password
'foobar' // database name
);

$result = $DB->query(
'SELECT '.
'`id`, `username`, `password` '.
'FROM '.
'`users` '.
'WHERE '.
'`username` = $1',
array(
'vini'
);
);

Kaj smo storili? Najprej smo se seveda priklopili na podatkovni strežnik, nato pa smo izvedli poizvedbo, v kateri smo uporabili en parameter, vrednost smo mu pa določili v prvem elementu arraya. Pozor, v PHPju je prvi element arraya v bistvu nulti element ($array[0]), tukaj sem izbral ljudem bolj berljivo metodo, da je prvi element tudi označen s številko 1.

Metoda query() je poskrbela tudi za escapanje narekovajev, seveda samo v parametrih.

Še implementacija razreda DB_RESULT:

class DB_RESULT {

protected $result;

public function __construct(
$result
) {
$this->result = $result;
}

public function fetch_result(
$row,
$col
) {
return
mysql_result(
$this->result,
$row,
$col
);
}

public function fetch_assoc() {
return
mysql_fetch_assoc(
$this->result
);
}

public function free() {
mysql_free_result(
$this->result
);
}
}

Spet zelo enostavno, objekt razreda DB_RESULT nam vrne metoda query() razreda DB. Implementira tri metode za manipulacijo z rezultatom MySQL poizvedbe. Metoda fetch_result() je ekvivalent funkcije mysql_result(), metoda fetch_assoc() je ekvivalent funkcije mysql_fetch_assoc(), metoda free() pa ekvivalent funkcije mysql_free(). Razlika je le v tem, da metodam ni potrebno podajati parametra z rezultatom poizvedbe, saj se le-ta hrani interno v objektu.

Primer, če kar nadaljujemo prejšnjega, ko že imamo rezultat poizvedbe po tabelu users iz katerega smo želeli izbrskati uporabnika z uporabniškim imenom "vini":

if ($row = $result->fetch_assoc()) {
echo
'ID uporabnika '.
$row['username'].
' je: '.
$row['id'];
}
$result->free();

Pri uporabi tega razreda izklopite magic_quotes_gpc, saj bomo v nasprotnem primeru narekovaje dvojno escapali.

Ta dva razreda sta, kot sem že rekel, zelo enostavna in ne omogočata nekaterih stvari, ki bi jih mogoče skoraj morala, recimo lovljenja napak. Ampak, zakaj bi vam prinesel vse na pladnju? Malo napnite svoje male sive celice, kot bi rekel Hercule Poirot, pa implementirajte te zadeve sami. Seveda lahko rešitev podate v komentarjih, že čakam!

1.
22. april 2007, 11:04

se ti ne zdi da je uporaba regex-a precej pocasna?

2.
22. april 2007, 11:39

Niti ne, na mojem strežniku se 10000 iteracij izvajanja tega regular expressiona zaključi v 0.3 sekunde. Koliko različnih poizvedb pa ti ponavadi izvedeš za povprečen HTTP zahtevek? Še zdaleč ne toliko, jaz redko pridem do številke 10, pri tej številki bi se zaradi regular expressiona vse skupaj upočasnilo za borih 0.0003 sekunde, zanemarljivo, kar se mene tiče.

3.
22. april 2007, 12:06

je res, ampak ce imas v danem trenutku 100k+ userjev hkrati, se pozna :)

4.
22. april 2007, 12:34

100k+ obiskovalcev hkrati ti bo ubilo strežnik tudi brez tega regular expressiona :) Si že doživel kdaj takšen naval na kakšnem svojih strežnikov?

5.
22. april 2007, 14:01

sem :)

6.
22. april 2007, 15:09

To ti pa rahlo težko verjamem, res. Koliko obiskovalcev je bilo pa potem v enem dnevu?

7.
22. april 2007, 16:39

a ni malo škoda časa za izumljanje tople vode? pdo, creole, adodb, adodb lite ...

8.
22. april 2007, 17:18

fett, kot sem napisal zgoraj: "SQL wrapperjev za PHP obstaja malo morje, večina je glomaznih, tale podpira sicer samo MySQL, je pa zelo enostaven."

Tole zgoraj je, kot sem tudi že omenil, manjši del mojega SQL wrapperja, ki pa sem ga šel razvijat zaradi svojih specifičnih zahtev, katerim noben od meni znanih wrapperjev ni ugodil preveč dobro in sem z njimi samo izgubljal čas. Je sicer topla voda, ampak ima okus po limeti :)

Gornja koda tudi ni namenjena znalcem, objavil sem jo predvsem zato, ker so se pojavile želje, da objavim še kaj za začetnike.

9.
22. april 2007, 19:50

v enem dnevu, hm, ne spomnim se ... prisli smo na prvo stran na diggu in je server skoraj pocepnil, ampak je slo skozi ... vec ti pa povem v sredo, ce mi uspe pridt :)

10.
hoho
23. april 2007, 08:36

Ja in Digg ti pošlje naenkrat 100.000 ljudi. No way.

11.
23. april 2007, 11:17

Mogoče bi moral pogledati adodb lite, če si ti bili ostali preveliki. Upoštevati moraš, da z lastnim razvojem dolgoročno obremenjuješ samo sebe -- stvar moraš upgrejdati, razhroščevati in vzdrževati. Namesto, da svoj čas porabljaš za razvoj rešitve/produkta. Pač ni dobra praksa in se v 99% primerih bolj splača vzeti stabilno in že razvito rešitev. Je pa res, da je nekaj čara tudi v takem igračkanju. :)

12.
23. april 2007, 11:23

@hoho:
http://en.wikipedia.org/wiki/Slashdot_effect
ne bos verjel, ampak ja.. kajti ni samo digg, ampak potem pridejo zraven se forumi, blogi in vse ostalo.. in stvari pocepnejo :(

13.
23. april 2007, 13:03

fett, razumem tvoje pomisleke. Fora je pa predvsem v tem, da take knjižnice ponavadi sam razvijem precej hitreje, kot se naučim do popolnosti uporabljati neko 3rd party zadevo.

Sem do nedavnega uporabljal adodb lite, ja, pa sem naletel na omejitve, preko katerih nisem mogel, brez da bi hackal knjižnico, kar pa seveda nisem želel, če bi hotel kdaj zadevo še nadgradit.

Kratkoročno je lažje, da uporabim že narejeno rešitev, seveda. Dolgoročno se mi je pa v večini primerov pokazalo, da lahko ustreznejšo (zame) rešitev razvijem kar sam, v čisto doglednem času. Slej ko prej se mi porabljeni čas obrestuje.

Je pa zelo veliko resnice tudi v tvojem zadnjem stavku, razvijanje lastnih rešitev ima svoj čar, pa ne samo to, s tem tudi dodatno razvijam svoje znanje, kar je tudi dolgoročno kar dobra naložba :)

14.
23. april 2007, 15:29

iskreno mislim, da ti manjka izkušenj, drugače ne bi bil mnenja, da se razvijanje takih stvari dolgoročno splača. V resnici je ravno obratno -- lastni razvoj low-level knjižnic se obnese samo na kratki rok. Pri uporabi 3rd party knjižnic je vložek enkraten (učenje), plus čas, izgubljen z nadgradnjami in čas za dopolnjevanje znanja. Na dolgi rok to pomeni ogromen prihranek časa.

Pri uporabi lastne rešitve na dolgi rok začnejo pobirati davek napredek tehnologije (php napreduje, ti moraš knjižnico nadgrajevati), pomanjkanje dokumentacije (pisanje dokumentacije je sicer časovno potratno, ampak čez 2 leti jo boš potreboval), hrošči in še kaj bi se našlo. Znajdeš se v situaciji, ko imaš z nekim projektom delo samo zato, ker uporabljaš neko tako svojo rešitev in jo moraš prilagajati situaciji. Čista škoda časa.

Za konec še razvijalski rek: dober razvijalec zna vse narediti sam, izvrsten razvijalec pa uporabi že obstoječe rešitve.

15.
23. april 2007, 15:50

fett, jaz pa iskreno mislim, da mi izkušenj vsekakor ne manjka :)

Moje izkušnje so pač take, da sem z že izdelanimi rešitvami vedno izgubil čisto preveč časa, v vsaki je manjkal kakšen feature brez katerega nisem mogel.

Se pa tukaj midva rahlo vrtiva v začaranem krogu, imava pač vsak svoje mnenje in mislim, da jaz tebe ne bom prepričal v svoj prav, ti pa mene ne.

Kje si tisti razvijalski rek pobral, ne vem, se pa z njim sicer skoraj popolnoma strinjam, do neke mere. Izvrsten razvijalec uporablja obstoječe rešitve, dokler ga le-te ne začnejo omejevati. Tako počnem tudi sam, skoraj vedno najprej uporabim obstoječe rešitve, dokler ne pridem do nepremostljivih problemov. Takrat z nabranimi izkušnjami spišem svojo knjižnico. Ker točno vem, kaj želim od nje, razvoj sploh ne traja toliko časa, kot bi si mogoče kdo mislil.

16.
23. april 2007, 16:04

ta rek je precej znana zadeva v razvijalskih krogih, izpeljan je iz dolgoletnih izkušenj razvijalcev in se ga uporablja kot argument v točno tej debati. ;)

drugače pa sem mnenja, da to ni debata o mnenjih. Kaj je najboljše za izvedbo neke rešitve ni in ne more biti stvar mnenja, temveč je to rezultat izračuna golih dejstev: poraba časa, znanje, velikost ekipe, stabilnost platforme ... Vsekakor izračun zahteva veliko število dejavnikov in ga je v bistvu izredno težko sploh definirati, vendar to še ne pomeni, da ne obstaja nek najboljši način.

In vendar se v resnici strinjam: obstaja meja, kjer zunanja rešitev postane toliko omejujoča, (slaba, naporna, preslabo podprta), da je bolj optimalno spisati svojo. Razlogov je lahko veliko, ampak v konkretnem primeru (pri vseh teh db abstraction layerjih, ki so na voljo za php) to ne bi smel biti problem.

Če ne drugega, ti manjka recimo iterator čez rezultate. ;)

17.
23. april 2007, 16:28

fett, ja, to imaš prav, tale primer je čisto odveč, če bi bil njegov namen biti še en SQL wrapper v poplavi mnogih, boljših in zmogljivejših. Pa to ni njegov namen. Njegov namen je bolj na kratkem primeru pokazati enostaven primer takega wrapperja, da se mogoče kakšen začetnik na njem kaj nauči. Lahko, da tudi v tem nisem uspel, ampak potem morava pa zamenjat temo debate :) Sem pa dobil že nekaj odzivov začetnikov, ki se jim zdi zadeva čisto poučna in so se mi zahvalili za ta post.

18.
23. april 2007, 20:19

ja, to je pa verjetno največja vrednost takih poizkusov -- učenje. Če pač (kot v večini primerov) stvar ne preraste v neko stabilno knjižnico, se vsaj programer veliko nauči. :)

19.
23. april 2007, 20:54

hej fett, sem videl, da imaš rojstni dan, ti se pa že cel dan z mano ukvarjaš. Sem kar počaščen :)

No, kakorkoli, vse najboljše! :)

20.
24. april 2007, 01:36

hvala hvala :)

21.
hoho
24. april 2007, 11:35

@had

Ja server počepne.. vendar ni niti približno od Digga 100.000 ljudi naenkrat na serverju.. poglej link iz wikipedie:

http://www.anecdota.org/bandwidth+spikes/
Digg: 10,000 visitors in 24 hours, how my blog got noticed

22.
24. april 2007, 12:31

hoho, tisti članek govori o diggu leta 2005, ko še ni bilo toliko obiskovalcev tam. Če mogoče samo pogledaš tale Alexa graf, boš vedel o čem govorim.

Moramo pa seveda tukaj definirati trenutek, jaz si ga predstavljam bolj kot sekundo, ne kot minuto. Če rečemo, da je ta trenutek, ki ga omenja David, sekunda, potem naj bi digg.com poslal 8.640.000.000 obiskovalcev v 24 urah, kar je rahlo pretirano, kajne? Tudi, če se odločimo, da je ta trenutek minuta, je to še vedno 144.000.000 obiskovalcev v 24 urah. Še vedno pretirano, ja?

David, definiraj trenutek :)

23.
hoho
24. april 2007, 20:09

Ja novejši viri pravijo, da je povprečni digg effect tam med 20-40k obiskovalcev.

Da bi pa 8 milijard ljudi prišlo preko digga v enem dnevu se mi zdi pa malo sumljivo, mislim da so vsaj nekateri izmed njih vmes zamenjali IP :)

24.
24. april 2007, 20:36

trenutek je, ko poljubis najlepso zensko in naslednjo sekundo ugotovis, da je minilo pol dneva ...

sele zdej vidim, da sem se zatipkal gor :) ena nula je prevec, mislil sem napisat 10k+, kot trenutek pa mislim na sekundo ... seveda ne posplosevat na trajanje v nedogled, ko dobis v burstu 10k zahtevkov na s, ti server lahko enostavno pocepne in se zacne vleeeeeeect ... verjetno se vam ne zdi realno, ampak tudi 100k+ na s ni pretirano ... jasno, da en server ne sfolga tega ampak imas zadej kar nekaj zmogljiv serverjev, ki lepo delajo v sozitju in si porazdelijo naloge :)

25.
BS
8. september 2007, 12:16

Sicer ne vem točno, kaj naj bi skripte zgoraj sploh počele, ampak, če je bistvo v "eskejpanju narekovajev", jaz pri prijavi ("Login", Z. Suraski)uporabim siceršnjo PHP funkcijo, ki manipulira z sejami (session), ter vse te reči prepustim njej - ta že, brez da bi ji to kdo kaj posebej naročil, "poeskejpa narekovaje"... potem tudi "SQL injection", ko izve zanje, nima posebnih težav... ehhh

26.
10. september 2007, 16:41

BS, katera session funkcija pa ima kakorkoli opraviti z bazo? SQL injection nima kaj posebej dosti opraviti s sessioni, lahko me pa seveda popraviš in bolj podrobno obrazložiš, kaj si imel v mislih. Zelo močno predvidevam, da si imel v mislih magic_quotes, ja?