Programowanie w języku PHP

Sonda na bazie danych

  • 2011-02-24 15:40
  • 7 komentarzy
  • 5586 odsłon
  • Oceń ten tekst jako pierwszy
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.</nobr>

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

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

7 komentarzy

Forskolin 2016-11-24 15:06

I really impressed after read this because of some quality work and informative thoughts
http://www.sizegeneticsguides.com/

Coldpeer 2007-10-27 15:47

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 :)

pawkow 2007-10-27 13:00

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

Coldpeer 2007-10-26 22:40

Ok, dzisiaj (26-10-2007) kod i artykuł zostały napisane od nowa.

Coldpeer 2007-09-06 13:10

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.

dethim 2007-08-13 09:22

Trzeba by tu dopisać opcje sprawdzania wyników głosowania...

Coldpeer 2007-03-19 16:38

Ahh, jak teraz sobie tak patrzę na ten kod, to nie wiem, czy śmiać się, czy płakać...