Zpět

Začínáme s PHP - díl 4.

MySQL podruhé

Dnes se naučíme pracovat s IP adresami, probereme základy zabezpečení SQL a pár dalších triků a funkcí.

Začněme rovnou příkladem

Minule jsme si napsali jednoduchoučké počítadlo přístupů, které při každém načtení stránky zvětšilo jedno číslo v databázi o 1 a zobrazilo ho. Tentokrát se pustíme do něčeho složitějšího: do ankety. V našem případě to bude udělátko, které vypíše jednu otázku a několik možných odpovědí a když některou z nich návštěvník vybere, v příslušné škatulce přibude jeden bod a anketa se zamkne, aby jeden člověk nemohl hlasovat víckrát než jednou.

Co k tomu budeme potřebovat?

Řekněme, že anket budeme chtít mít na stránce několik, vzájemně nezávislých a s různými otázkami i odpověďmi. To znamená, že musíme navrhnout tabulky, do kterých se nám vejde toto:

Text otázky bude nějaký vhodný textový řetězec (stačí velice krátký, třeba TINYTEXT nebo nějaký VARCHAR), plus nějaký identifikační kód, který určuje, ke které anketě otázka patří. Dejme tomu, že si ankety budeme číslovat, takže ten kód může být třeba celé číslo; předpokládám, že nebudeme provozovat tisíce anket najednou, takže nám postačí i ten nejmenší TINYINT. Pro účely optimalizace ho můžeme označit za primární klíč (PRIMARY KEY), pro účely pohodlí můžeme ještě dodat AUTO_INCREMENT, aby se každé nově přidané anketní otázce automaticky přiřadil kód o 1 větší než té předchozí, ale nutné to není - pokud anket není moc, může být výhodnější číslovat si je ručně.

Odpovědi do stejné tabulky nacpat nemůžeme, protože nevíme, kolik jich ve které anketě bude, a proměnný počet sloupců se udělat nedá. Sice bychom si mohli připravit sloupce třeba pro dvacet odpovědí a u menších anket některé z nich nechat nevyužité, ale podle zákona schválnosti bychom stejně jednou zjistili, že jich potřebujeme nejméně dvacet jedna. Takže to uděláme jinak, odpovědi půjdou do samostatné tabulky.

Texty odpovědí a počty hlasů bude nejlepší držet hezky pohromadě v jedné tabulce. Odpověď bude zase nějaký krátký text, počet hlasů nějaké dostatečně velké přirozené číslo - třeba INT UNSIGNED. Potom samozřejmě potřebujeme kód ankety, abychom věděli, ke které to patří.

Mimochodem, tím nám vznikne přímo ukázkový vztah neboli relace (odtud pojem "relační databáze") mezi tabulkou otázek a tabulkou odpovědí. Vztah je typu 1:n, tedy k jedné otázce z první tabulky se váže libovolný počet odpovědí z tabulky druhé. Zároveň si tohle uspořádání můžeme představit i jako stromovitou hierarchickou strukturu: otázku jako kořen a odpovědi jako větve na stejné úrovni. To je dobré si zapamatovat: jakoukoli hierarchii můžeme v relační databázi nasimulovat tak, že si každý uzel pamatuje svého jediného nadřízeného.

Konec terminologické odbočky, zpátky k tabulce. Bude nám text, počet hlasů a kód ankety stačit? Teoreticky možná ano, ale je potřeba si uvědomit, že až návštěvník odešle svůj hlas, budeme ho muset nějak jednoznačně a pokud možno úsporně identifikovat. Posílat přes odkaz nebo formulář celý text odpovědi je krajně nepraktické, nehledě na to, že texty klidně můžou být v několika anketách stejné (jako třeba odpověď "Ano"). Také potřebujeme něco, podle čeho by se odpovědi v anketě řadily - ne vždy to chceme podle abecedy. Takže přidáme ještě unikátní identifikační kód, nejlépe číselný (v takovém případě můžeme s výhodou použít auto_increment, protože na odpovědi ruční číslování určitě potřebovat nebudeme).

Pojistku proti vícenásobnému hlasování si necháme na později, první pokus si pro jednoduchost napíšeme bez ní. Předem prozradím, že jenom přidáme třetí tabulku a pár podmínek navíc, první dvě tabulky zůstanou beze změny.

Takhle nějak by tedy mohly vypadat příkazy pro vytvoření tabulek otázek a odpovědí:

CREATE TABLE otazky
(
Otazka TINYTEXT,
KodAnkety TINYINT PRIMARY KEY
)

CREATE TABLE odpovedi
(
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
KodAnkety TINYINT,
PocetHlasu INT UNSIGNED,
Odpoved TINYTEXT
)

Místo Tinytextu jsme stejně dobře mohli použít VARCHAR(něco), klidně i kratší než těch 255 znaků. Také jsme mohli u všech důležitých položek (kromě klíčů, tam je to zbytečné) dodat NOT NULL, aby se nedaly vložit prázdné. Ale to už jsou jenom drobnosti, které na další postup nemají žádný vliv. Na velikosti písmen v názvech sloupců nezáleží, takže např. PocetHlasu můžeme později psát třeba samými malými písmeny, nebude to vadit.

Tabulky máme, co dál?

Příprava ankety k použití

Dejme tomu, že na svých stránkách o lehké atletice chceme rozjet anketu o nejoblíbenější sport. Otázka je tedy celkem jasná, kód ankety si zvolíme třeba 1, protože je to naše první anketa. Vložení otázky do tabulky, by mohlo vypadat např. takto:

INSERT INTO otazky VALUES ('Jaký je váš nejoblíbenější sport?',1)

Vkládání odpovědí do druhé tabulky není o moc složitější:

INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Lyžování')
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Plavání')
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Běhání')
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Geohashing')
INSERT INTO odpovedi VALUES (NULL, 1, 0, 'Gaučing')

Hodnota NULL v prvním sloupci bude díky auto_incrementu automaticky nahrazena vzestupnou číselnou řadou (1, 2, 3...). Jednička u kódu ankety je jasná - odpovědi patří k výše uvedené otázce číslo jedna. Nula je počáteční počet hlasů, teoreticky bychom sem mohli dát i nějaké jiné číslo (pokud třeba přenášíme hlasy odjinud nebo pokud chceme statistiku trochu popostrčit vhodným směrem :-) ). Text odpovědi je jasný.

A teď co s tím. Buď tyto příkazy pustíme přímo přes nějaké webové rozhraní k MySQL (např. PHPMyAdmin), které obvykle bývá k dispozici; v takovém případě jenom pozor na kódování české diakritiky. Nebo bychom mohli každý řádek obalit do mysql_query, všechno uložit do jednoho PHP skriptu, ten nahrát na server a přes prohlížeč spustit. Tím by sice odpadly potenciální problémy s češtinou, ale kdo se s tím má pořád tak zdlouhavě patlat. Protože lenost je matka pokroku a protože se tím naučíme spoustu nových věcí, vyrobíme si na tvorbu anket administrátorské rozhraní se vším všudy. Ale protože to není ani nezbytně nutné ani úplně triviální, necháme si to až úplně na konec a nejdřív si napíšeme skript, který anketu zobrazí a zpracuje hlasy od návštěvníků.

Zobrazení ankety

Pro jednoduchost zatím necháme stranou estetickou stránku věci (obvyklý rámeček kolem ankety a podobně) a vypíšeme jenom nejdůležitější údaje uspořádané v neviditelné tabulce. První řádek (resp. záhlaví) tabulky bude vyhrazen pro otázku. Další řádky budou rozdělené do dvou buněk, v levé bude text odpovědi zároveň fungující jako klikatelný odkaz pro odeslání hlasu a v pravé bude aktuální počet hlasů pro tuto odpověď. Dopadnout to může dejme tomu takhle:

$cislo=1; //číslo ankety
$vysledek=mysql_query("SELECT otazka FROM otazky WHERE kodankety=$cislo",$spojeni);
$radek=mysql_fetch_row($vysledek);
echo '<table><tr><td colspan="2">'.$radek[0].'</td></tr>';

Proměnnou $cislo jsme si zavedli proto, aby se následující kód nemusel opisovat pro každou anketu zvlášť. Nakonec si z toho stejně uděláme funkci a číslo jí můžeme pohodlně předat jako parametr.

Druhým příkazem jsme si do proměnné $vysledek z databáze vytáhli všechny otázky, které mají kód ankety 1. Taková je samozřejmě jenom jedna, takže výsledek bude tabulka o velikosti 1x1. $spojeni je jako obvykle proměnná, kterou jsme dostali od funkce mysql_connect.

Třetí příkaz z návratové tabulky vytáhne první (a zároveň i jediný) řádek. Bude to pole všehovšudy s jedním prvkem, takže nám nehrozí popletení indexů a je jednodušší použít fetch_row, která dává pole indexované čísly.

Poslední příkaz vypíše HTML kód pro první řádek tabulky a do něj vloží z připraveného pole první (resp. nultou) položku - text otázky.

Pozn.: Uvnitř řetězců v uvozovkách se jména proměnných automaticky nahrazují jejich hodnotou (viz první díl), takže místo textu "$cislo" se v příkazu objeví jednička.

Otázka byla jednoduchá, teď odpovědi:

$vysledek=mysql_query("SELECT id, pocethlasu, odpoved FROM odpovedi WHERE kodankety=$cislo ORDER BY id ASC",$spojeni);
while ($radek=mysql_fetch_array($vysledek)):
 echo '<tr><td>';
 echo '<a href="anketa.php?hlasujpro='.$radek['id'].'">'.$radek['odpoved'].'</a>';
 echo '</td><td>';
 echo $radek['pocethlasu'];
 echo '</td></tr>';
endwhile;
echo '</table>';

Pozor, tady se nám v SQL objevila nová konstrukce: ORDER BY neboli "seřaď podle". Tím jsme řekli, že v návratové tabulce $vysledek chceme mít řádky uspořádané podle hodnoty ve sloupci id, a to vzestupně (ASC jako ascending). Vzestupně znamená, že první řádek bude mít id nejmenší a poslední největší. Opačně by bylo DESC (descending). ASC je výchozí hodnota, takže by nám stačilo napsat jenom order by něco. Řadit samozřejmě můžeme podle čehokoli (texty by se braly abecedně). Kdybychom řazení neuvedli, výběr by sice asi přišel v takovém pořadí, v jakém byl do databáze vložen (což je to, co teď zrovna chceme), ale obecně se na to nedá moc spoléhat.

Následuje cyklus, který bude postupně vybírat z tabulky výsledků jednotlivé řádky a předávat je přes pole $radek, odkud si je vyzvedneme a po jednotlivých položkách zobrazíme.

Tvar hlasovacího odkazu jsem si zvolil, teoreticky by nemusel vypadat zrovna takhle. Po kliknutí přejde na skript anketa.php, což bude předpokládám přímo tenhle, který zrovna píšeme. Zároveň metodou Get předá proměnnou $_GET['hlasujpro'] a v ní hodnotu položky id od dané odpovědi.

Teoreticky není nutné, aby se odpovědi dělaly zrovna takhle jako odkazy. Také by anketa mohla být formulář, kde by se odpověď vybrala přepínačem (select nebo input typu radio) a odesílala by se tlačítkem, klidně i přes Post místo Get. Ale prakticky vzato je to jenom kosmetický detail a na způsob zpracování hlasu to nemá vliv.

Nakonec uzavřeme tabulku a jsme hotovi.

Zpracování hlasu

Návštěvník naší stránky si tedy nechal zobrazit anketu, klikl na jednu odpověď a my se s ní teď musíme vypořádat. Uděláme vlastně úplně totéž, jako v případě počítadla: najdeme v databázi jedno číslo a zvýšíme ho o 1:

if (isset($_GET['hlasujpro']))
 mysql_query("UPDATE odpovedi SET pocethlasu=pocethlasu+1 WHERE id=".$_GET['hlasujpro'],$spojeni);

Ovšem POZOR, tentokrát je v tom jeden velikánský háček: co kdyby nějakého chytráka napadlo prohlédnout si text odkazu, zamyslet se a do adresního řádku ručně naťukat třeba tohle:

http://nase.stranka.cz/anketa.php?hlasujpro=1%20OR%205%3d5

%20 je kód mezery a %3d kód rovnítka, rozkódování proběhne automaticky. Po přímém dosazení do příkazu by databáze dostala tohle:

UPDATE odpovedi SET pocethlasu=pocethlasu+1 WHERE id=1 OR 5=5

Podtržením je zvýrazněn celý zadaný vstup. Nepříjemné, že? Odkliknutí všech odpovědí ve všech anketách sice nemá žádný praktický smysl, ale zkuste si představit, že by takhle někdo převezl třeba podmínku testující administrátorské heslo.

Této technice útoku se říká SQL injection a je to asi nevětší nebezpečí, s jakým se v běžném životě setkáme a kterému se musíme vyhnout.

V našem případě je řešení celkem jednoduché. Očekáváme číselný kód, ale text '1 OR 5=5' má k číslu daleko, tak ho můžeme rovnou vyloučit. Kontrolu číselnosti zajistí funkce is_numeric, která vrací true v případě, že se parametr skládá pouze z číslic, případně s nějakým tím znaménkem nebo desetinnou tečkou:

if (is_numeric($_GET['hlasujpro']))
 mysql_query("UPDATE... atd., viz výše);
 else die('Co to na mě zkoušíš, podvodníku?');

Jiným způsobem zabezpečení je tzv. escapování, kdy se před všechny mezery, středníky, apostrofy, uvozovky a podobné potenciálně nebezpečné znaky předřadí znak \ (zpětné lomítko). Ale to má smysl především pro texty a podrobně si ho probereme v příštím díle.

Pojistka proti vícenásobnému hlasování

Anketa by nám teď měla fungovat, ale má jednu nevýhodu: kdokoli do ní může naklikat libovolný počet jakýchkoli hlasů. To se nám ale nelíbí, chtěli bychom, aby každý mohl hlasovat jenom jednou. Jak to udělat?

Máme v podstatě dvě možnosti: buď si zapamatovat IP adresu hlasujícího počítače, nebo v něm nechat sušenku (cookie). Cookies mají výhodu v jednoznačnosti (nastaví se opravdu jenom na ten jeden počítač, ze kterého se hlasovalo, a nezablokuje anketu jiným), ale nevýhodu v tom, že si je uživatel může kdykoli smazat a hlasovat znovu. IP adresy sice nemusí být jednoznačné (víc počítačů může mít stejnou) ani trvalé (na stejném počítači se může měnit), ale zase jsou jednodušší na obsluhu a tak nějak blbuvzdornější. Proto si první ochranu založíme právě na IP adresách.

Postup bude celkem jednoduchý: vytvoříme si další tabulku, do které budeme ukládat IP adresy počítačů, ze kterých se hlasovalo, spolu s kódem ankety, aby nám odhlasování v jedné nezablokovalo všechny ostatní:

CREATE TABLE ipadresy
(
ip VARCHAR(15),
kod TINYINT
)

Položka ip je vlastní IP adresa. Od serveru ji dostaneme dostaneme v podobě textového řetězce obsahujícího čtyři čísla v rozsahu 0..255, oddělená tečkami (např. '123.255.20.1'). Varchar(15) jsem si zvolil proto, že na víc než 15 znaků to vyjít nemůže; teoreticky by stejně dobře posloužil třeba Tinytext.

Položka kod odpovídá kódu ankety. Záměrně jsem ji nepojmenoval KodAnkety, protože tuhle tabulku můžeme zároveň využít třeba pro počítadla přístupů (viz minule), stačí jim přidělit nějaké kódy, které se nebudou tlouct s existujícími anketami. Ale to už nechám na vás.

Aktuální IP adresu návštěvníka najdeme v superglobálním poli $_SERVER, konkrétně pod názvem $_SERVER['REMOTE_ADDR'] (pozor, všechna písmena jsou velká!). To je všechno, co potřebujeme, můžeme se pustit do práce. Po odhlasování uložíme adresu do tabulky následujícím způsobem:

mysql_query("INSERT INTO ipadresy VALUES ('".$_SERVER['REMOTE_ADDR']."',$cislo)",$spojeni);

Povšimněte si apostrofů připravených okolo místa, do kterého vkládáme adresu - bez nich by to nešlo, je to text. Vzhledem ke složitosti zápisu téhle proměnné jsem ji radši připojil pomocí teček, automatickému rozbalování zas až tak moc nevěřím :-].

$cislo na konci je kód ankety. Ovšem pozor - kde jsme ho vlastně vzali? V tuhle chvíli máme po ruce jenom id odeslané odpovědi, proměnnou $_GET['hlasujpro']. Nezbývá, než si ho ještě před pokusem o uložení ajpiny v příslušné tabulce najít:

$vysledek=mysql_query("SELECT kodankety FROM odpovedi WHERE id=".$_GET['hlasujpro'],$spojeni);
if ($radek=mysql_fetch_row($vysledek)) $cislo=$radek[0];
  else die('Tuhle odpověď nemáme v databázi!');

Prvním příkazem jsme získali buď tabulku 1×1 obsahující hledaný kód, nebo nic, pokud odpověď s daným id nebyla v databázi nalezena (to se může stát snad jenom při ručním hraní s adresním řádkem). Samozřejmě předpokládám, že proměnnou $_GET['hlasujpro'] už touhle dobou máme důkladně zkontrolovanou a víme, že je to platné číslo. Jestli ne, ať vás ani nenapadne strkat ji do SQL!

Druhý příkaz se pokusí z načtené jednobuňkové minitabulky vytáhnout hledaný kód. Kdyby se mu to nepovedlo, zahlásí chybu a ukončí skript (to samozřejmě není jediná možnost, jak se s chybou vyrovnat).

Shrneme to: podle čísla odpovědi jsme si našli kód ankety a spolu s IP adresou jsme ho uložili do databáze. Tím máme hotovou poslední fázi zpracování hlasu. Ještě se ale musíme vrátit na začátek, do okamžiku, kdy jsme dostali číslo odpovědi, zkontrolovali ho a teď se rozmýšlíme, jestli tenhle hlas započítáme nebo jestli už má daná adresa odhlasováno. Bude to chtít kontrolu, jestli už máme tuhle adresu uloženou a hlas zahodíme, nebo jestli ji ještě nemáme, takže ji uložíme a hlas započítáme:

$vysledek=mysql_query("SELECT * FROM ipadresy WHERE ip='".$_SERVER['REMOTE_ADDR']."' AND kod=".$cislo,$spojeni);
if (mysql_num_rows($vysledek)==0) ...tenhle tu ještě nebyl, jeho hlas uložíme...
  else ...už ho tu máme uloženého, další hlasy od něj ignorujeme...

První příkaz zkusí z tabulky IP adres vytáhnout kombinaci zpracovávané ankety a aktuální adresy návštěvníka. Pokud tam taková kombinace není (tj. ještě nebylo hlasováno), vrátí prázdnou tabulku s nulovým počtem řádků. Pokud tam je, vrátí nám jednořádkovou tabulku. Dovnitř do ní vůbec nemusíme koukat (stejně víme, co tam je - přesně to, co jsme zadali do té podmínky). Pozor, nestačí ověřit $vysledek na true nebo false - true dostaneme vždycky, i když se nevrátí žádná data! False by se objevilo jenom při chybě.

Pozn. č. 1: teoreticky samozřejmě můžeme místo té hvězdičky napsat jméno některého sloupce, ale výsledný efekt by byl stejný: buď dostaneme něco nebo nic.

Pozn. č. 2: ještě by se dal použít příkaz SELECT COUNT(*) FROM ..., který by nám dal počet řádků vyhovujících dané podmínce. Výsledek by se ale vrátil v jednoprvkové tabulce, kterou by stejně bylo potřeba pokaždé přečíst a vyhodnotit, takže bychom si tím práci spíš přidělali. Použití COUNT(*) si podrobněji probereme někdy příště, až ho budeme skutečně potřebovat.

Poslední drobnost, ke které se uložené adresy dají použít, je to, že anketu, ve které už se nedá hlasovat, vykreslíme rovnou bez klikatelných odkazů, aby to návštěvníky nemátlo - takovéhle věci je lepší se dozvědět na první pohled a ne až po zdlouhavém kliknutí. To bude záležitost toho kousku, kde vypisujeme jednotlivé odpovědi: prostě v takovém případě vynecháme <a>...</a>.

Stop, mám v tom guláš! Jak to všecho patří dohromady?

Pravda, zatím jsme probírali jednotlivé detaily a celek se ztrácí kdesi v nedohlednu. Tak tedy: všechno máme v jednom skriptu, který dělá tohle:

  1. Přijetí hlasu ($_GET['hlasujpro']). Pokud přišel, zkontrolujeme jeho platnost (is_numeric), najdeme k němu kód ankety a obě čísla si uložíme do nějakých vhodných proměnných. Pokud nepřišel nebo není platný, přeskočíme jeho zpracování.
  2. Kontrola, jestli tenhle návštěvník ve zvolené anketě může hlasovat.
  3. Zpracování hlasu (pokud přišel, je platný a může se hlasovat):
    1. Zvyš počet hlasů u zvolené odpovědi.
    2. Návštěvníkovu IP adresu ulož do tabulky adres.
  4. Cyklus pro každou anketu, kterou na téhle stránce máme:
    1. Zkontroluj, jestli tenhle návštěvník v téhle anketě může hlasovat.
    2. Pokud může, vypiš anketu s odpověďmi ve formě aktivních odkazů. Pokud ne, vypiš odpovědi jako neaktivní text.

Jak vidíte, kontrolovat přítomnost adresy v tabulce musíme nejméně dvakrát: jednou na začátku skriptu při zpracovávání hlasu a potom jednou pro každou vykreslovanou anketu.

Kosmetické detaily

Počty hlasů v anketách se obvykle znázorňují graficky pomocí různých sloupečků nebo barevných žížalek. To by v tom byl čert, abychom to nedokázali taky!

Vodorovný pruh se dá nakódovat pomocí HTML elementů hr nebo div, kterým se pomocí CSS vhodně nastaví vlastnosti jako width, background-color, border, filter a podobně. Tady si předvedeme postup sestavení proužku z obrázků (element img), bez použití CSS. Využijeme toho, že se obrázku dají nastavit libovolné rozměry a nemusí se dodržovat ani poměr stran. To znamená, že můžeme mít fyzicky uložený jenom krátký úsek (jestli nepotřebujeme barevné přechody mezi levým a pravým koncem, může to být třeba jenom jednopixelová nudle) a roztáhneme si ho podle potřeby. Pokud máme konce proužku nějak tvarově odlišené (kulaté, stínované, plastické apod.), roztažení by jim uškodilo, takže je musíme uložit zvlášť. Měli bychom tedy celkem tři malé obrázky: pevný levý konec, roztažitelný střed a pevný pravý konec. Dejme tomu, že je máme na serveru uložené pod jmény levykonec.gif, stred.gif a pravykonec.gif a že jsou všechny 10 pixelů vysoké.

První otázka je, jak dlouhé proužky udělat. Nabízí se triviální možnost 1 hlas = 1 pixel, ale to by mělo dvě nevýhody: zaprvé by při malých počtech hlasů byly proužky moc krátké a rozdíly v délkách by nebyly pouhým okem viditelné, zadruhé by se naopak při hodně vysokých počtech anketa neomezeně roztahovala. Potřebujeme tedy nějaký přepočet, který maximální délku omezí a zároveň zajistí, aby každý hlas byl vidět. Nejjednodušší je prohlásit, že odpověď s největším počtem hlasů bude odpovídat maximální šířce proužku, a všechny ostatní šířky trojčlenkou smrsknout nebo roztáhnout v příslušném poměru (tak to dělají například diskusní fóra PHPBB):

 max. počet hlasů    nějaký jiný počet hlasů
------------------ = -----------------------
max. šířka proužku    hledaná šířka proužku

neboli po úpravě:

hledaná šířka proužku = nějaký jiný počet hlasů * max. šířka proužku / max. počet hlasů

Jediná výjimečná situace, na kterou si musíme dát pozor, je počáteční stav ankety, kdy jsou všechny počty a tedy i maximum nulové. Asi nemusím připomínat, že dělení nulou by nedopadlo dobře ;-).

Teď ještě jak to naprogramovat (v proměnné $cislo máme kód ankety):

$vysledek=mysql_query("SELECT MAX(pocethlasu) FROM odpovedi WHERE kodankety=$cislo",$spojeni);
if (!($radek=mysql_fetch_row($vysledek)) or (($maximum=$radek[0])==null]))
  die('Tuhle anketu nemáme na skladě.');

Tady se nám objevuje nová funkce: MAX(sloupec) nám dá největší hodnotu z daného sloupce, výsledek dostaneme ve formě jednobuňkové návratové tabulky. Pokud by dané podmínce (where) neodpovídaly žádné řádky a maximum tedy nebylo z čeho počítat, v návratové tabulce bude hodnota null (pozor, návratová tabulka by existovala a obsahovala by hodnotu, akorát že by ta hodnota byla null - nestačí otestovat fetch na true/false). Obdobně funguje MIN(), která dává minimum.

Další věc, která možná potřebuje trochu vysvětlit (aspoň pro ne-céčkaře), je ten divoký logický výraz v závorce ifu. Takže: nejdřív se do proměnné $radek načte obsah návratové tabulky. Kdyby byla prázdná, celý ten přiřazovací výraz by dal hodnotu false a po negaci true. Druhá část podmínky by se vůbec neuplatnila (na výsledku oru by stejně nic nezměnila), takže by se rovnou provedlo die(). Jestli prázdná nebyla, pokračuje se druhou podmínkou. V ní se naplní proměnná $maximum, výsledná hodnota tohoto přiřazovacího výrazu (která je rovná tomu, co bylo přiřazeno) se hned porovná s nullem a jestli souhlasí, vyjde logické true a voláme die.

Na hlavičku ankety se naše kosmetické úpravy nevztahují, tak se mrkneme rovnou na odpovědi. Dejme tomu, že proužek zobrazíme ve stejné buňce přímo nad otázkou. Zároveň také rovnou provedeme dříve zmíněné vynechání odkazů, pokud už se hlasovalo (to je ta proměnná $muze_hlasovat; předpokládám, že už ji máme připravenou z dřívějška):

$vysledek=mysql_query("SELECT id, pocethlasu, odpoved FROM odpovedi WHERE kodankety=$cislo ORDER BY id ASC",$spojeni);
while ($radek=mysql_fetch_array($vysledek)):
 echo '<tr><td>';
 //proužek:
 echo '<img src="levykonec.gif">';
 if ($maximum==0) $sirka=0; //pojistka proti dělení nulou
             else $sirka=round($radek['pocethlasu']*200/$maximum);
 echo '<img src="stred.gif" height="10" width="'.$sirka.'">';
 echo '<img src="pravykonec.gif"><br>';
 //otázka a počet hlasů:
 if ($muze_hlasovat) echo '<a href="anketa.php?hlasujpro='.$radek['id'].'">';
 echo $radek['odpoved'];
 if ($muze_hlasovat) echo '</a>';
 echo '</td><td>';
 echo $radek['pocethlasu'];
 echo '</td></tr>';
endwhile;

Zakončení tabulky a podobné formality už nechám na vás.

200 je požadovaná maximální šířka proužku v pixelech. Hodnotu jsem si zvolil, může to být samozřejmě i cokoli jiného. 10 je požadovaná výška proužku; zadat se musí, jinak by většina prohlížečů roztáhla obrázek i svisle, aby se zachoval poměr stran. Výška proužku by pochopitelně měla být stejná jako výška levého a pravého konce, na které má navazovat.

Použili jsme novou funkci: round(číslo), která dané číslo zaokrouhlí na celé. To je nutné, protože šířka obrázku musí být zadána v celých pixelech.

Poslední věc, která stojí za zmínku, je možnost zabalit obsluhu anket do funkcí (pro podrobnosti viz druhý díl). Pokud si napíšeme jednu funkci pro zpracování hlasu a druhou pro zobrazení jedné ankety (její kód by dostala v parametru), výrazně si zpřehledníme zápis v hlavní části skriptu. Navíc pokud se nám podaří po vyhodnocení hlasu skočit zpátky na původní stránku (napadá mě předat jméno cílové stránky jako další parametr zobrazovací funkce, ale možná existuje i něco pohodlnějšího), můžeme si anketní funkce schovat do samostatného souboru a ten pak podle potřeby includovat a využívat na všech ostatních stránkách.

Administrační rozhraní

Anketní otázky a odpovědi sice můžeme tvořit a upravovat přímo pomocí příkazů SQL, ale to je poměrně pracné a hrozí, že se někde překlepneme. Pokud jste dost líní na to, abyste investovali nemalé množství práce do stránky, která vám následně trošku práce ušetří, čtěte dál :-).

Co všechno budeme potřebovat?

Každý formulář samozřejmě bude mít i svoje odesílací tlačítko.

V následujícím textu postupně uvedu jednotlivé formuláře a příslušné vyhodnocovací algoritmy. Předpokládám, že všechno pojede v jednom skriptu nazvaném tvorbaanket.php. O rozložení na stránku, vzhled a podobné nepodstatné drobnosti už se touhle dobou doufám dokážete postarat sami :-).

Zatím nebudeme řešit přihlašování, ochranu heslem, antispam a podobné věci. Předpokládám, že tenhle jednoúčelový skript budeme nahrávat na server vždycky jenom na chvíli a po použití ho zase smažeme, takže se k němu nikdo nestihne dostat a zneužít nezabezpečený přístup. Ze stejného důvodu se protentokrát můžeme vykašlat i na ochranu proti SQL injekci.

Vytvoření nové ankety

Formulář:

<form action="tvorbaanket.php" method="post">
<input type="text" name="tvorotazka" value="">
<input type="text" name="tvorkod" value="">
<input type="submit" value="Vytvoř anketu">
</form>

Vyhodnocení:

if (isset($_POST['tvorotazka']) and isset($_POST['tvorkod'])):
 //kontrola, jestli uz nahodou neexistuje:
 $vysledek=mysql_query("select * from otazky where kodankety=".$_POST['tvorkod'],$spojeni);
 if (mysql_num_rows($vysledek)==0):
  if (mysql_query("insert into otazky values (".$_POST['tvorkod'].",'".$_POST['tvorotazka']."')",$spojeni))
          echo 'Anketa vytvořena.';
     else echo 'Anketu se nepodařilo vytvořit.';
 else:
  echo 'Anketa s tímhle kódem už existuje, zkus zadat nějaký jiný.';
 endif;
endif;

Pozn. č. 1: nejlepší je, když při chybových situacích (obsazený kód, selhané vložení apod.) zůstane formulář vyplněný tím, co jsme do něj napsali, abychom to nemuseli psát znovu. To by se zařídilo vložením příslušných proměnných do obsahů (value) jejich textových políček při vypisování formuláře. Ale pro přehlednost to tu uvádět nebudu.

Pozn. č. 2: z hlediska pohodlí by bylo nejlepší, aby se nově vytvořená anketa hned automaticky vybrala pro úpravy. Ale to už nechám na vás. Nápověda: vyhodnotíme výstup a pak pomocí příkazu header (viz první díl) skočíme na stránku s adresou upravenou podle následujícího odstavce.

Seznam anket s výběrem

Dejme tomu, že to bude jednoduchý seznam odkazů. Teď ale pozor: informaci o tom, která anketa je vybraná, musíme někudy předat. S odkazy nemáme jinou možnost než metodu Get neboli vepsání hodnoty přímo do adresy stránky. Dejme tomu, že si tu proměnnou pojmenujeme vybranaanketa. Hlavně je potřeba nezapomenout, že ji pak musíme neustále předávat i z dalších formulářů, jinak by se nám výběr ztratil.

Nejdřív vyhodnocení - tohle musíme připravit vždycky:

$vybranaanketa = isset($_GET['vybranaanketa']) ? $_GET['vybranaanketa'] : '';

Pro ilustraci jsem použil céčkovský špek zvaný podmíněný výraz. Obecně se zapisuje jako Podmínka?Hodnota1:Hodnota2 a znamená: pokud je daná Podmínka pravdivá (true), výsledkem výrazu je Hodnota1, jinak Hodnota2. Co se týče priority těchto operátorů, je úplně nejnižší. To mimo jiné znamená, že jestli chcete třeba sestavit řetězec pomocí operátoru "." a jako některé části použijete podmíněné výrazy, musíte je uzavřít do (závorek), jinak by se tečky vyhodnotily jako první, přilepily by vám kousky řetězce k podmínce a druhé hodnotě a vyšel by z toho nesmysl.

A teď ten seznam:

$vysledek=mysql_query("select * from otazky",$spojeni);
if (mysql_num_rows($vysledek)==0):
 echo '<p>Zatím tu žádnou anketu nemáme.</p>';
else:
 echo '<ul>';
 while ($radek=mysql_fetch_array($vysledek))
  if ($vybranaanketa==$radek['kodankety'])
        echo '<li>'.$radek['kodankety'].': '.$radek['otazka'].' (aktuálně vybraná)</li>';
    else echo '<li><a href=tvorbaanket.php?vybranaanketa='.$radek['kodankety'].'">'
              .$radek['kodankety'].': '
              .$radek['otazka'].'</a></li>';
 echo '</ul>';
endif;

Přidání otázky k vybrané anketě

Celkem jednoduchá záležitost. Pozor je potřeba dát jenom na správné ošetření vybrané ankety (předpokládám, že proměnnou $vybranaanketa máme připravenou z předchozího odstavce). Formulář:

if ($vybranaanketa<>''):
 echo '<form action="tvorbaanket.php?vybranaanketa='.$vybranaanketa".' method="post">';
 echo '<input type="text" name="pridejodpoved" value="">';
 echo '<input type="submit" value="Přidej odpověď">';
 echo '</form>';
else:
 echo 'Žádná anketa není vybraná.';
endif;

Tady celkem není co řešit, snad jen by šlo doplnit předvyplnění odpovědi minule zadanou hodnotou v případě chyby nebo přidání políčka pro zadání počátečního počtu hlasů. Ale to už jsou detaily.

Vyhodnocení:

if (($vybranaanketa<>'') and isset($_POST['pridejodpoved']) and ($_POST['pridejodpoved']<>''))
 if (mysql_query("insert into odpovedi values (null, $vybranaanketa, 0, '".$_POST['pridejodpoved']."')",$spojeni))
        echo 'Odpověď vložena.';
   else echo 'Odpověď se nepodařilo vložit.';

Hlášení o úspěšnosti je víceméně zbytečné (odpověď buď uvidíme v následujícím formuláři nebo ne), dal jsem ho sem jenom pro úplnost.

Úpravy vybrané ankety

Tohle je nejsložitější část celého skriptu, ale v podstatě není úplně nezbytně nutná, takže jestli chcete, můžete ji vynechat (jenom to mazání byste pak museli dělat ručně).

Formulář:

if (($vybranaanketa<>''):
 echo '<form action="tvorbaanket.php?vybranaanketa='.$vybranaanketa.'" method="post">';
 //otázka:
 $vysledek=mysql_query("select otazka from otazky where kodankety=$vybranaanketa",$spojeni);
 if ($radek=mysql_fetch_row($vysledek))
         echo 'Otázka: <input type="text" name="editotazka" value="'.$radek[0].'"> '
              .'<a href="tvorbaanket.php?vybranaanketa='.$vybranaanketa.'&smazanketu=jo">Smaž celou tuhle anketu</a>';
    else echo 'Vybraná anketa neexistuje.';
 //odpovědi:
 echo '<br>Odpovědi:<br>';
 $vysledek=mysql_query("select id, odpoved from odpovedi where kodankety=$vybranaanketa order by id asc",$spojeni);
 while ($radek=mysql_fetch_array($vysledek)):
  echo '<input type="text" name="editodpoved'.$radek['id'].'" value="'.$radek['odpoved'].'"> '
       .'<a href="tvorbaanket.php?vybranaanketa='.$vybranaanketa.'&smazodpoved='.$radek['id'].'">Smaž tuhle odpověď</a><br>'
 endwhile;
 echo '<input type="submit" value="Ulož změny">';
 echo '</form>';
else:
 echo 'Žádná anketa není vybraná.';
endif;

Jak vidíte, metody Get a Post se dají kombinovat: kód vybrané ankety posíláme přes adresu (Get) a obsah textových políček jinudy (Post).

Otázku si nejdřív vytáhneme z databáze. Pokud ji tam najdeme (jako že asi jo, jinak bychom se na ni nemohli přes seznam doklikat), vypíšeme ji v textovém políčku. K ní ještě přidáme odkaz na smazání ankety - ten předá proměnnou $_GET['smazanketu'] s hodnotou 'jo'. Na hodnotě nám vlastně ani nezáleží, anketu ke smazání už nám jednoznačně určuje proměnná $vybranaanketa.

Odpovědi se od otázky zas až tak moc neliší, jenom je jich víc, takže musíme použít cyklus. Také je potřeba jednotlivá políčka navzájem nějak odlišit: na to použijeme položku id, kterou připojíme ke jménu (name). Mazací odkaz funguje podobně jako mazadlo otázek, ale tentokrát kód ankety na identifikaci nestačí, proto si pro proměnnou $_GET['smazodpoved'] připravíme id odpovědi.

Tlačítko "Ulož změny" je tu proto, aby bylo čím odeslat hodnoty z textových polí. Kliknutí na nějaký mazací odkaz formulář neodešle, na to pozor.

A nakonec vyhodnocení. Setkáme se tu s něčím, s čím jsme se ještě nesetkali: s neznámým počtem vstupních parametrů, u kterých ani přesně nevíme, jak se jmenují. Začnu kódem a potom si to vysvětlíme:

if (($vybranaanketa<>''):

 //úprava otázky:
 if(isset($_POST['editotazka']))
  mysql_query("update otazky set otazka='".$_POST['editotazka']."' where kodankety=$vybranaanketa",$spojeni);

 //funkce určující, jestli daná věc je jméno proměnné s odpovědí:
 function jetoodpoved($co)
 {
 return(substr($co,0,11)=='editodpoved');
 }

 //úprava odpovědí:
 $klice=array_keys($_POST);
 $klice=array_filter($klice,'jetoodpoved');
 foreach ($klice as $klic):
  $idecko=substr($klic,11);
  mysql_query("update odpovedi set odpoved='".$_POST[$klic]."' where id=$idecko",$spojeni);
 endforeach;

 //případné smazání ankety (tj. otázky a všech odpovědí, které k ní patří):
 if(isset($_GET['smazanketu'])):
  mysql_query("delete from odpovedi where kodankety=$vybranaanketa",$spojeni);
  mysql_query("delete from otazky where kodankety=$vybranaanketa",$spojeni);
  $vybranaanketa=''; //to aby bylo jasné, že tahle anketa už neexistuje
 endif;

 //případné smazání některé odpovědi:
 if(isset($_GET['smazodpoved']))
  mysql_query("delete from odpovedi where id=".$_GET['smazodpoved'],$spojeni);

endif;

Uf. Takže co jsme to vlastně udělali?

  1. Úprava otázky - celkem triviální věc. Pokud nám příslušná proměnná z Postu přišla (tj. pokud jsme se sem dostali odesláním formuláře), nahradíme její hodnotou hodnotu v databázi otázek. O jednoznačnou identifikaci se postará kód vybrané ankety. Úspěšnost už ani netestuju - buď se to povede, nebo se nám ve formuláři objeví zase ta původní hodnota a pak je jasné, že se to nepovedlo. Ale prakticky se to stejně vždycky povede.
  2. Filtrovací funkce - řekne nám, jestli daný řetězec začíná na 'editodpoved'. Funkce substr(Řetězec,Pozice,Délka) vrací úsek z daného Řetězce začínající od dané Pozice (počítáno od nuly) a o dané Délce, nebo pokud Délka není zadaná, tak bere všechno až do konce Řetězce. Filtrovací funkci budeme potřebovat o kousek dál.
  3. Úprava odpovědí - tady už to trošičku houstne. Teoreticky by nebyl problém projít foreachem celé pole $_POST, u každé položky filtrovací funkcí (nebo přímo) otestovat klíč jestli je to jméno odpovědi, a jestli jo, její hodnotu uložit do tabulky. Ale já jsem se rozhodl pro zpestření použít trochu jiný postup. Funkcí array_keys jsem z pole $_POST vytáhl do samostatného pole klíče. Tohle nové pole je indexováno čísly od nuly a původní klíče má jako hodnoty. V dalším kroku jsem použil funkci array_filter, která z daného pole vyhodí všechny prvky, pro které funkce v druhém parametru (v našem případě "jetoodpoved", zadává se takhle jménem jako text) dává hodnotu false. Tím jsme dostali pole, které obsahuje pouze jména proměnných, které nás zajímají. Zbytek už je v podstatě triviální: každé toto jméno vezmeme, vytáhneme z něj id (víme, že začíná na pozici 11 a pokračuje až do konce jména), použijeme ho jako identifikační podmínku a přepíšeme text odpovědi.
  4. Smazání ankety - nejdřív se podíváme, jestli přes Get přišla příslušná proměnná (tj. jestli jsme se sem dostali kliknutím na mazací odkaz). Pokud ano, smažeme všechny otázky a odpovědi, které patří k této anketě. Jak vidíte, nemáme tam nic jako "Opravdu to myslíte vážně?", takže opatrně :-).
  5. Smazání odpovědi - jednodušší než úpravy textů, protože smazat se dá vždycky jenom jedna, navíc její id dostaneme až pod nos. Myslím, že tady není co vysvětlovat.

Ještě jedna obecná poznámka na závěr. Jak vidíte, na čtení dat z databáze používám zásadně proměnnou $vysledek, pro výstupy z mysql_fetch_něco zase $radek. Na konkrétních jménech proměnných samozřejmě nezáleží, chci jenom říct, že jich na to nepoužívám víc než tyhle dvě. Není to nutné, ale má to dvě výhody: zaprvé přehlednost (dvě jména nezapomenu; kdybych měl deset různých v každém skriptu, bylo by to horší) a zadruhé menší nároky na paměť serveru ("recyklace" proměnných v průběhu skriptu znamená, že nezůstávají alokované zbytečně). Brát ohledy na server může v dnešní době supervýkonného hardwaru vypadat jako zbytečnost, ale vemte si, kolik takových skriptů musí každou vteřinu přechroustat. Ono se to nasčítá.

A to je vše, přátelé!

Na to, jaká je to blbost, jsme si toho o anketách napsali až až :-). Doufám, že uvedené příklady aspoň k něčemu budou.

Příště se zaměříme víc na texty a napíšeme si něco užitečnějšího: návštěvní knihu.

Zpět

Reklamy:
„Člověk, který nikdy neplakal nežil opravdový život!“ Jan Werich