Sonda na bazie danych
Artykuł opisuje jak zrobić dość rozbudowany skrypt www sondy (ankiety) oparty na relacyjnej bazie danych.
Jako, iż najpopularniejszym językiem server-side jest obecnie PHP, kod zdecydowałem się napisać właśnie
w nim, a dokładniej w wersji 5.
Ponieważ dostawałem wiele pytań od osób nieznających się na programowaniu o sposób wdrożenia skryptu, poświęciłem temu zagadnieniu ostatni punkt artykułu.
Struktura systemu opiera się o dwie główne tabele w bazie. Są to "pytania" i "odpowiedzi":
Dla przykładu stwórzmy jakąś ankietę:
Skrypt korzysta z relacyjnej bazy danych do przechowywania ankiet i ich wyników. Może wykorzystywać różne bazy danych, zależnie od sterownika; bardzo prosta i elementarna klasa dla bazy MySQL, o jaką ja się opierałem:
Użyte zapytania SQL powinny działać także pod PostgreSQL i SQLite (nie wiem jak inne bazy), w przypadku powyższej klasy wystarczy zmienić nazwy funkcji, np. mysql_query na pg_query. Po więcej odsyłam do manuala PHP. Oczywiście, można też skorzystać z PDO, ADOdb, PEAR::DB, czy - w przypadku MySQL - mysqli.
I w końcu najdłużej wyczekiwana część artykułu, mianowicie kod skryptu sondy. Jest to klasa, dla której interfejs (nie jest on konieczny do działania skryptu) można zaprojektować następująco:
Jak już wspominałem, do działania wymagany jest PHP5, aczkolwiek przepisanie na oficjalnie nierozwijanego już i przestarzałego PHP4 nie jest większym problemem. Aby tego dokonać, należy:
Sam wiem, iż najlepiej się analizuje, czyta i sprawdza działanie kodu pokazanego w całości, aniżeli wiele krótkich listingów z omawianą szczegółowo zasadą działania. Kod jest prosty i posiada najważniejsze komentarze, tak więc raczej nie powinno być niejasności.
Jak teraz tego użyć? Ano najprościej w ten sposób:
Jeśli nie chcemy wyświetlić listy z innymi sondami znajdującymi się w bazie, wystarczy przed wyświetleniem
(metoda display) dać:
Domyślnie inne ankiety sortowane są od najnowszej do najstarszej. Aby odwrócić kolejność:
Załóżmy, że plik nazwaliśmy poll.php. Takie wywołanie wyświetli najnowszą sondę. Aby wyświetlić
ankietę o id równym 5, należy link sformułować następująco: poll.php?id=5. Jeśli wyjdziemy
poza zakres istniejących rekordów w bazie, lub wpiszemy coś innego niż liczbę, skrypt wyświetli stosowny
komunikat. Pod tym względem system jest więc idiotoodporny.
Możliwości skryptu w łatwy sposób można poszerzyć. Najprościej stworzyć nową funkcję w powyższej klasie, można też utworzyć
klasę dziedziczącą po powyższej.
Oto przykład jak dodać mechanizm komentarzy do każdej z ankiet. Napiszemy tutaj klasę dziedziczącą. Stwórzmy pierw tabelę w bazie danych:
A oto kod wraz ze sposobem użycia:
Wyobraźmy sobie, że chcemy dodać dodatkowe pola do formularza, np. przy głosowaniu spisujemy imię, nazwisko i e-mail respondenta. Tym razem dodamy nową funkcję do klasy Poll. Tworzymy tabelę w bazie:
I dodajemy funkcję:
Tablica $this->new_fields zawiera funkcje, które mają być włączone do formularza HTML ankiety, w tym wypadku trzy dodatkowe pola tekstowe. Musimy dodać przed metodą $this->display naszą funkcję (metodę), tak więc inicjalizacja będzie wyglądać następująco:
Ponieważ dostawałem wiele pytań od osób nieznających się na programowaniu o sposób wdrożenia skryptu, postanowiłem napisać tutaj kilka słów rozjaśnienia.
Pierw należy dodać do bazy danych zapytania SQL, jak określono w punkcie "Zapytania SQL" (czyli 2x CREATE TABLE). Jeśli chcemy dodać ankietę, to posłużmy się zapytaniem INSERT, przykład również został wyieniony w owym punkcie. W przypadku bazy MySQL i posiadania popularnego klienta www phpMyAdmin (alternatywnie w PostgreSQL phpPgAdmin) nalezy kliknąć na kwadracik "SQL", wkleić zapytania i je wykonać.
Kod PHP składa się z klasy Database (ta przedstawiona przeze mnie jest bardzo prosta i nie oferuje żadnych nowych funkcjonalności, aniżeli byśmy z niej nie korzystali) i klasy Poll (lub jeszcze klas dziedziczących, np. w przykładzie PollComm). Pierw musi znaleźć się klasa Database, następnie Poll, a dopiero potem reszta.
Na przykład ankieta z drugiego przykładu "Rozbudowy" może wyglądać tak.
W razie wątpliwości, propozycji czy innych pytań proszę śmiało pisać, najlepiej na e-mail. Jeśli zdecydowałeś(aś) się umieścić skrypt na swojej stronie, miłym gestem będzie poinformowanie mnie o tym.
Autor artykułu: Jędrzej "Coldpeer" Czarnecki
coldpeer (at) gmail.com
coldpeer.jogger.pl
Jako, iż najpopularniejszym językiem server-side jest obecnie PHP, kod zdecydowałem się napisać właśnie
w nim, a dokładniej w wersji 5.
Ponieważ dostawałem wiele pytań od osób nieznających się na programowaniu o sposób wdrożenia skryptu, poświęciłem temu zagadnieniu ostatni punkt artykułu.
Spis treści
1 Możliwości
2 Zapytania SQL
3 Obsługa bazy danych
4 Kod skryptu
5 Rozbudowa
5.1 Komentarze użytkowników
5.2 Dodatkowe pola formularza
6 Dla osób chcących wykorzystać kod
1 Możliwości
2 Zapytania SQL
3 Obsługa bazy danych
4 Kod skryptu
5 Rozbudowa
5.1 Komentarze użytkowników
5.2 Dodatkowe pola formularza
6 Dla osób chcących wykorzystać kod
Możliwości
* Obsługa wielu sond- Dowolna ilość wariantów odpowiedzi dla poszczególnych ankiet
- Ustalenie od kiedy, do kiedy ma być możliwość głosowania
- Możliwość wstrzymania głosowania w ankiecie
- Ograniczenie wielokrotnego głosowania w danej sondzie przez jednego użytkownika za pomocą ciasteczek
- Wyświetlenie linków do innych sond
- Łączna frekwencja jak i poszczególnych wariantów odpowiedzi, obliczenie procenta danej opcji, przedstawienie wyników na słupkowym wykresie
- Obsługa wielu baz danych
- Łatwa rozbudowa kodu
Zapytania SQL
Struktura systemu opiera się o dwie główne tabele w bazie. Są to "pytania" i "odpowiedzi":
CREATE TABLE poll_questions ( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, date_add datetime NOT NULL, date_begin datetime NOT NULL, date_end datetime NOT NULL, stop INT NOT NULL DEFAULT 0 ); CREATE TABLE poll_answers ( id_answer INT NOT NULL PRIMARY KEY AUTO_INCREMENT, id_poll INT NOT NULL, answer VARCHAR(255) NOT NULL, votes INT NOT NULL DEFAULT 0 );
Dla przykładu stwórzmy jakąś ankietę:
INSERT INTO poll_questions VALUES( NULL, 'Twój ulubiony język programowania?', now(), now(), '2020-03-01', 0 ); INSERT INTO poll_answers VALUES (NULL, 1, 'C/C++', 0), (NULL, 1, 'Java', 0), (NULL, 1, 'PHP', 0), (NULL, 1, 'Python', 0), (NULL, 1, 'Inny', 0);
Obsługa bazy danych
Skrypt korzysta z relacyjnej bazy danych do przechowywania ankiet i ich wyników. Może wykorzystywać różne bazy danych, zależnie od sterownika; bardzo prosta i elementarna klasa dla bazy MySQL, o jaką ja się opierałem:
class Database { public function __construct() { mysql_connect('localhost', 'user', 'password'); mysql_select_db('base_name'); } public function query($sql) { return mysql_query($sql); } public function numrows($sql) { return mysql_num_rows($sql); } public function fetch($sql) { return mysql_fetch_array($sql); } }
Użyte zapytania SQL powinny działać także pod PostgreSQL i SQLite (nie wiem jak inne bazy), w przypadku powyższej klasy wystarczy zmienić nazwy funkcji, np. mysql_query na pg_query. Po więcej odsyłam do manuala PHP. Oczywiście, można też skorzystać z PDO, ADOdb, PEAR::DB, czy - w przypadku MySQL - mysqli.
Kod skryptu
I w końcu najdłużej wyczekiwana część artykułu, mianowicie kod skryptu sondy. Jest to klasa, dla której interfejs (nie jest on konieczny do działania skryptu) można zaprojektować następująco:
interface iPoll { public function display(); // zwraca caly przeparsowany kod HTML sondy public function other($id); // inne sondy }
Jak już wspominałem, do działania wymagany jest PHP5, aczkolwiek przepisanie na oficjalnie nierozwijanego już i przestarzałego PHP4 nie jest większym problemem. Aby tego dokonać, należy:
- pozbyć się wielopoziomowych odwołań do składowych (np. stworzyć zmienną $base = $this->db; i odwoływać się $base->query())
- pozbyć się modifikatorów dostępu, tj. z funkcji usunąć słowa kluczowe public, a we właściwościach zastąpić public na var
- zmienić nazwę konstruktora z __construct na taką, jaką nosi nazwa klasy (Poll)
Sam wiem, iż najlepiej się analizuje, czyta i sprawdza działanie kodu pokazanego w całości, aniżeli wiele krótkich listingów z omawianą szczegółowo zasadą działania. Kod jest prosty i posiada najważniejsze komentarze, tak więc raczej nie powinno być niejasności.
/** * Description: Advanced poll script written in PHP5 using relational database. * Author: Jędrzej "Coldpeer" Czarnecki * E-mail: coldpeer (at) gmail.com * WWW: http://coldpeer.jogger.pl * Licensed: GNU GPL v3 * Copyright: (c) 2007 */ class Poll { public $db; public $other = true; // czy pokazywac inne sondy public $desc_sort = true; // sortowanie innych sond od najnowszych public $id; // id sondy public $new_fields = array(); // funkcje z nowy polami do formularza public $no_add = false; // nie dodawac (np. ktoras z funkcji z $new_fields mowi, ze dane niepoprawne) public function __construct() { $this->db = new Database(); } public function display() { $sql = $this->db->query('SELECT q.id, q.title, q.date_begin, q.date_end, q.stop, a.id_answer, a.answer, a.votes, (SELECT sum(votes) FROM poll_answers WHERE id_poll = q.id GROUP BY id_poll) as sum FROM poll_questions as q, poll_answers as a WHERE q.id = a.id_poll AND q.id = ' . (!isset($_GET['id']) ? '(SELECT max(id) FROM poll_questions)' : (int)$_GET['id'])); if($this->db->numrows($sql) > 0) { $now = date('Y-m-d'); while($row = $this->db->fetch($sql)) { if($_POST['vote'] && !$this->no_add) { $row['sum']++; if($row['id_answer'] == $_POST['vote']) $row['votes']++; } if(!$b) { $this->id = $row['id']; if($row['stop'] == 1 || $_POST['vote'] && !$this->no_add) $noform = true; // podstawowe dane o ankiecie $ret .= '<b>' . $row['title'] . '</b><p />Łącznie oddano głosów: ' . $row['sum']. '<br />Data rozpoczęcia: ' . $row['date_begin'] . '<br />Data zakończenia: ' . $row['date_end']; if($row['date_end'] <= $now) $ret .= '<p />Ankieta się już zakończyła.'; elseif($row['stop'] == 1) $ret .= '<p />Glosowanie w ankiecie zostało wstrzymane.'; $ret .= '<p />'; // wyswietlenie formularza if(!isset($_COOKIE['poll' . $this->id]) && $row['date_end'] > $now && !$noform) { $ret .= '<form action="" method="post">'; foreach($this->new_fields as $v) $ret .= $v; $form = true; } elseif(isset($_COOKIE['poll' . $this->id]) && $row['date_end'] > $now && !$noform) { $ret .= 'Głosowałeś już w tej sondzie.<p />'; } // user zaglosowal if($_POST['vote'] && !$this->no_add) { $ret .= 'Twój głos został dodany.<p />'; if(!isset($_COOKIE['poll' . $this->id])) { $this->db->query('UPDATE poll_answers SET votes=votes+1 WHERE id_answer='.$_POST['vote']); setcookie('poll' . $this->id, $this->id, time()+3600 * 3600 * 30); // 22 lata } $noform = true; } $b = true; } // wyswietlenie wariantow odpowiedzi if($form) $ret .= '<input type="radio" name="vote" value="' . $row['id_answer'] . '" /> ' . $row['answer'] . '<br />'; else { $ret .= $row['answer'].', ' . $row['votes'] . ' glosow, ' . ($row['sum'] > 0 ? round($row['votes']*100/$row['sum']) : 0) . '% ' . '<div style="background: red; height: 10px; width: ' . ($row['votes'] == 0 || $row['sum'] == 0 ? 5 : round($row['votes'] * 200 / $row['sum'])) . 'px"></div><br />'; } } if($form) $ret .= '<br /><input type="submit" name="submit" value="Głosuj!" /></form>'; if($this->other) $ret .= '<p /><b>Inne sondy</b><p />' . $this->other($this->id); } else $ret = 'Nie ma takiej sondy w bazie.'; return $ret; } public function other($id) { $sql = 'SELECT id, title FROM poll_questions WHERE id <> ' . $id . ' ORDER BY id ' . ($this->desc_sort ? 'DESC' : 'ASC'); $sql = $this->db->query($sql); if($this->db->numrows($sql) > 0) { $ret = '<ul>'; while($row = $this->db->fetch($sql)) $ret .= '<li><a href="' .$_SERVER['PHP_SELF'] . '?id=' . $row['id'] . '">' . $row['title'] . '</a></li>'; return $ret . '</ul>'; } else return '(brak)'; } }
Jak teraz tego użyć? Ano najprościej w ten sposób:
$poll = new Poll; echo $poll->display();
Jeśli nie chcemy wyświetlić listy z innymi sondami znajdującymi się w bazie, wystarczy przed wyświetleniem
(metoda display) dać:
$poll->other = false;
Domyślnie inne ankiety sortowane są od najnowszej do najstarszej. Aby odwrócić kolejność:
$poll->desc_sort = false;
Załóżmy, że plik nazwaliśmy poll.php. Takie wywołanie wyświetli najnowszą sondę. Aby wyświetlić
ankietę o id równym 5, należy link sformułować następująco: poll.php?id=5. Jeśli wyjdziemy
poza zakres istniejących rekordów w bazie, lub wpiszemy coś innego niż liczbę, skrypt wyświetli stosowny
komunikat. Pod tym względem system jest więc idiotoodporny.
Rozbudowa
Możliwości skryptu w łatwy sposób można poszerzyć. Najprościej stworzyć nową funkcję w powyższej klasie, można też utworzyć
klasę dziedziczącą po powyższej.
Komentarze użytkowników
Oto przykład jak dodać mechanizm komentarzy do każdej z ankiet. Napiszemy tutaj klasę dziedziczącą. Stwórzmy pierw tabelę w bazie danych:
CREATE TABLE poll_comments ( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, id_poll INT NOT NULL, DATE datetime NOT NULL, nick VARCHAR(30) NOT NULL, content text NOT NULL );
A oto kod wraz ze sposobem użycia:
class PollComm extends Poll { public function comments() { // dodajemy komentarz if($_POST['send']) { if($this->db->query('INSERT INTO poll_comments VALUES(NULL, ' . $this->id . ', now(), "' . htmlspecialchars($_POST['nick']) . '","' . htmlspecialchars($_POST['content']).'")')) $ret = 'Komentarz został dodany do bazy.'; else $ret = 'Wystąpił błąd podczas próby dodania komentarza.'; } // lista komentarzy $sql = $this->db->query('SELECT nick, date, content FROM poll_comments WHERE id_poll = ' . $this->id); $ret .= '<p /><b>Komentarze (' . $this->db->numrows($sql) . ')</b><p />'; if($this->db->numrows($sql) > 0) { while($row = $this->db->fetch($sql)) { $ret .= '<b>' . $row['nick'] . '</b> napisał (' . $row['date'] . '):<br />' . nl2br($row['content']) . '<p />'; } } else $ret .= '(nie ma jeszcze żadnych komentarzy)'; // wyswietlamy wszystko i formularz return $ret . '<p /><b>Dodaj komentarz:<b><form action="" method="post">Nick: <input type="text" name="nick" /><br />' . '<textarea name="content" cols="40" rows="10"></textarea><br /><input type="submit" name="send" value="Wyślij" /></form>'; } } $poll = new PollComm; echo $poll->display(); echo $poll->comments();
Dodatkowe pola formularza
Wyobraźmy sobie, że chcemy dodać dodatkowe pola do formularza, np. przy głosowaniu spisujemy imię, nazwisko i e-mail respondenta. Tym razem dodamy nową funkcję do klasy Poll. Tworzymy tabelę w bazie:
CREATE TABLE poll_responders ( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT , id_poll INT NOT NULL, name VARCHAR(20) NOT NULL, surname VARCHAR(30) NOT NULL, email VARCHAR(150) NOT NULL );
I dodajemy funkcję:
public function responder()
{
$ret = 'Imię: <input type="text" value="' . $_POST['name'] . '" name="name" /><br />'
. 'Nazwisko:<input type="text" value="' . $_POST['surname'] . '" name="surname" /><br />'
. 'E-mail: <input type="text" value="' . $_POST['email'] . '" name="email" /><p />';
if($_POST['submit'])
{
if($_POST['vote'] && $_POST['name'] && $_POST['surname']
&& $_POST['email'] && !isset($_COOKIE['poll' . $this->id]))
{
if(strlen($_POST['name']) > 20 || strlen($_POST['surname']) > 30 || strlen($_POST['email']) > 150)
{
$ret = '<p />Imię nie może być dłuższe niż 20 znaków, nazwisko niż 30, a e-mail niż 150.<p />' . $ret;
$error = true;
}
if(!preg_match("/^([a-zA-Z0-9_'+*$%\^&!\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9:]{2,4})+$/", $_POST['email']))
{
$ret = '<p />Podany adres e-mail jest nieprawidłowy.<p />' . $ret;
$error = true;
}
if($error || !$this->db->query('INSERT INTO poll_responders VALUES(NULL,(SELECT '
. (!isset($_GET['id']) ? 'max(id)' : 'id') .' FROM poll_questions'
. (isset($_GET['id']) ? ' WHERE id = ' . (int)$_GET['id'] : ''). '),"'
. $_POST['name'] . '","' . $_POST['surname'] . '","' . $_POST['email'] . '")'))
$this->no_add = true;
} else{
$this->no_add = true;
$ret = '<p />Muisz wypełnić wszystkie dane<p />' . $ret;
}
}
return $ret;
}Tablica $this->new_fields zawiera funkcje, które mają być włączone do formularza HTML ankiety, w tym wypadku trzy dodatkowe pola tekstowe. Musimy dodać przed metodą $this->display naszą funkcję (metodę), tak więc inicjalizacja będzie wyglądać następująco:
$poll = new Poll; $poll->new_fields[] = $poll->responder(); // przy następnych funkcjach analogicznie echo $poll->display();
Dla osób chcących wykorzystać kod
Ponieważ dostawałem wiele pytań od osób nieznających się na programowaniu o sposób wdrożenia skryptu, postanowiłem napisać tutaj kilka słów rozjaśnienia.
Pierw należy dodać do bazy danych zapytania SQL, jak określono w punkcie "Zapytania SQL" (czyli 2x CREATE TABLE). Jeśli chcemy dodać ankietę, to posłużmy się zapytaniem INSERT, przykład również został wyieniony w owym punkcie. W przypadku bazy MySQL i posiadania popularnego klienta www phpMyAdmin (alternatywnie w PostgreSQL phpPgAdmin) nalezy kliknąć na kwadracik "SQL", wkleić zapytania i je wykonać.
Kod PHP składa się z klasy Database (ta przedstawiona przeze mnie jest bardzo prosta i nie oferuje żadnych nowych funkcjonalności, aniżeli byśmy z niej nie korzystali) i klasy Poll (lub jeszcze klas dziedziczących, np. w przykładzie PollComm). Pierw musi znaleźć się klasa Database, następnie Poll, a dopiero potem reszta.
Na przykład ankieta z drugiego przykładu "Rozbudowy" może wyglądać tak.
W razie wątpliwości, propozycji czy innych pytań proszę śmiało pisać, najlepiej na e-mail. Jeśli zdecydowałeś(aś) się umieścić skrypt na swojej stronie, miłym gestem będzie poinformowanie mnie o tym.
październik 2007
Autor artykułu: Jędrzej "Coldpeer" Czarnecki
coldpeer (at) gmail.com
coldpeer.jogger.pl
Kategoria: PHP
6 komentarzy
odzwyczaiłem się już od pisania kodu w ten sposób ;) jakieś szablony, active record :) Pokaz jakieś demo, chętnie popatrzę jak to działa :D
<b>Ok, dzisiaj (26-10-2007) kod i artykuł zostały napisane od nowa.</b>
Hm? Możesz rozszerzyć myśl? Przecież wyniki są pokazywane, czy chodzi Ci o pokazywanie wyników, kiedy jeszcze się nie głosowało w danej ankiecie? To nie problem dodać. Zdaje się wystarczy tylko zamiana:
$tresc = formularz();
w warunku skomentowanym w /* WYNIKI/FORMULARZ */ -> /* formularz */ na:
$tresc = formularz() . '<p />' . wyniki();
Ogólnie mówiąc, to i tak wypadałoby go napisać od nowa.
Ahh, jak teraz sobie tak patrzę na ten kod, to nie wiem, czy śmiać się, czy płakać...
Chciałbym zauważyć, iż to tylko sposób jak taki kod można napisać. Każdy kto będzie chciał go wykorzystać, przerobi go do własnych potrzeb, jak chociażby wspomniane szablony. Mały sens byłby, gdybym napisał ten skrypt w oparciu o np. Smarty, bo nie o to tutaj przecież chodzi. A demo? Hmm... Chyba można się po krótkiej obserwacji domyślić jak działa, kto będzie chciał i potrzebował, ten sprawdzi u siebie :)