RkBlog

Hardware, programming and astronomy tutorials and reviews.

Prosty Blog - Początek

Do tej pory nauczyliśmy się:
- Ciąć szablony HTML na widoki
- Tworzyć kontrolery, widoki i modele
- Operować na bazie danych
- Tworzyć formularze, walidować dane

Cel

- Stworzyć prosty Blog zawierający:
- Moduł Newsów
- Stronicowanie newsów na stronie głównej
- Słowa kluczowe
- Wyszukiwarka
- Moduł Komentarzy
- Dodawanie komentarzy przez użytkowników do danego newsa

WYKONANIE

UWAGA użytkownicy PHP4
W przykładach może pojawić się wywołanie więcej niż 1 metody na raz:
$obiekt->metoda1()->metoda2();
W PHP5 to zadziała lecz nie w PHP4. Należy rozbijać takie wywołania
$x = $obiekt->metoda1();
$x->metoda2();

Cięcie szablonu

Jako szablon wybrałem jesienny "Autumn 05" (link w warsztacie cięcia szablonów). Jest on podobny do Metrohackera. Zmiany wymagało:
: - ścieżka do pliku CSS (umieszczony w /views)
- usunięcie kodu z index.html:
<?xml version="1.0" encoding="UTF-8"?>
- Szablon na starcie wyglądał tak:
darmowy hosting obrazków
- Korzystając przy tym z kontrolera "pomocniczego":
<?php
class Blog extends Controller
{
function index()
        {
        $this->load->view('index');
        }
}
- Po tych zmianach zmieniłem nazwię pliku na index.php i rozpocząłem cięcie. Rozdzieliłem komórkę lewą i "newsa" ze środka do oddzielnych plików (left.php i main.php)
- Po rozdzieleniu i ładowaniu obu pomocniczych widoków szablon wyglądał tak:
darmowy hosting obrazków
- Wszystko gotowe. Dokonałem jeszcze drobych poprawek w głównym szablonie zmieniając tytuł i takie tam by otrzymać:
darmowy hosting obrazków

Generyczny główny szablon

Teraz rozszerzymy nieco "cięcie" szablonów do widoków. Otóż widok index.php to szkielet strony, niezmienny dla wszystkich modułów. Zmieniać się będzie zawartość głównej kolumny i ew. lewych bloków. Obecnie index.php ładuje widok "main" i dla każdego kontrolera np. wyświetlającego newsy czy komentarze trzeba byłoby tworzyć kopie i "main" (co jest oczywiste) jak i widoku "index" by ładował odpowiednią wersję widoku "main".
Po prostu
< ?PHP $this->load->view('main'); ?>
Zastąpię
< ?PHP echo $content ?>
I pod zmienną $content zawsze będę podstawiał wynik odpowiedniego widoku "main" :) Próba wyświetlenia szablonu po podmianie wyrzuci "Undefined variable: content" - co zrozumiałe - zmienna $content nie istnieje, kontroler jej (jeszcze) nie przekazuje. Nie należy panikować.

Odwiedzamy phpMyAdmin

By stworzyć tabele można skorzystać z phpMyAdmina albo jak go nie mamy z metody $this->db>query() wykonującej nasze zapytanie SQL. Jeżeli chcesz skorzystać z tej drugiej opcji to musisz najpierw skonfigurować dane bazy danych w CI o czym napiszę za chwilę. Jeżeli korzystasz z bazy SQLite czy PostgreSQL zapytania będą podobne lecz nie identyczne. Najpierw tabele. Dla modułu newsów skorzystam z rozbudowanej wersji tabeli z poprzedniego warsztatu. Tabela tworzona poleceniem SQL:
CREATE TABLE kurs_news (
  news_id smallint(5) unsigned NOT NULL auto_increment,
  news_title varchar(255) default NULL,
  news_text text,
  news_author varchar(255) default NULL,
  news_date int unsigned NOT NULL,
  news_keywords varchar(255) default NULL,
  PRIMARY KEY  (news_id)
) ENGINE=MyISAM;
Zawiera trzy dodatkowe pola - news_author na nazwę autora newsa, news_date - data publikacji, news_keywords - słowa kluczowe.
Tabela komentarzy wygląda podobnie:
CREATE TABLE kurs_comments (
  com_id smallint(5) unsigned NOT NULL auto_increment,
  news_id smallint(5) unsigned NOT NULL,
  com_title varchar(255) default NULL,
  com_text text,
  com_author varchar(255) default NULL,
  com_date int unsigned NOT NULL,
  PRIMARY KEY  (com_id)
) ENGINE=MyISAM;
Nie ma pola na słowa kluczowe ale jest pole news_id. Będzie ono przechowywało numer ID news do którego jest to komentarz. Unikalność wartości pola auto_increment pozwala nam na tak łatwe rozwiązanie problemu.

Modele

Model newsów w porównaniu z ostatnim warsztatem poszerzy się o jedną metodę:
<?php
<?PHP
class News extends Model
{
function News()
        {
        parent::Model();
        }
function get_news()
        {
        // Wszystkie newsy sortowane malejąco po news_id
        $this->db->orderby("news_id", "desc");
        return $this->db->get('news');
        }
function get_news_by_id($id)
        {
        // Pobiera określony news
        $this->db->where('news_id', $id);
        return $this->db->get('news');
        }
function get_news_by_keyword($keyword)
        {
        // Wszystkie newsy z danym słowem kluczowym sortowane malejąco po news_id
        $this->db->like($keyword);
        $this->db->orderby("news_id", "desc");
        return $this->db->get('news');
        }
function add_news($data)
        {
        // dodanie newsa
        return $this->db->insert('news', $data);
        }
function update_news($id, $data)
        {
        // zmiana newsa o podanym numerze news_id
        $this->db->where('news_id', $id);
        return $this->db->update('news', $data);
        }
function delete_news($id)
        {
        // skasowanie newsa o podanym news_od
        $this->db->where('news_id', $id);
        return $this->db->delete('news');
        }
 }
?>
Mamy metodę get_news_by_keyword, która zwraca newsy dla danego słowa kluczowego. W naszym przykładzie nie stosujemy kategorii lecz luźne słowa kluczowe, które obecnie są "trendy" :) Po prostu jest to łańcuch zawierający jakieś słowa oddzielone przecinkami. Zapytanie z warunkiem LIKE zwróci te newsy, które mają w polu "news_keywords" podany element (słowo kluczowe).
Model komentarzy wygląda już nieco inaczej (comments.php):
<?php
class Comments extends Model
{
function Comments()
        {
        parent::Model();
        }
function get_comments_for_news($news_id)
        {
        // Komentarze dla danego newsa
        $this->db->where('news_id', $news_id);
        $this->db->orderby("com_id", "asc");
        return $this->db->get('comments');
        }
function add_comment($data)
        {
        // dodanie komentarza
        return $this->db->insert('comments', $data);
        }
function delete_comment($id)
        {
        // skasowanie komentarza o podanym ID
        $this->db->where('com_id', $id);
        return $this->db->delete('comments');
        }
function are_comments_for_news($news_id)
        {
        // ile jest komentarzy dla danego newsa
        // tego Active Records nie potrafi
        return $this->db->query("SELECT COUNT(*) AS comnumber FROM ".$this->db->dbprefix."comments WHERE news_id = '".$news_id."'");
        }
 }
Zrezygnowałem z opcji edycji komentarzy, mało przydatne (jak na razie). Mamy metodę are_comments_for_news, która zwraca liczbę komentarzy dla danego newsa (określonego jego numerem ID). SELECT COUNT to najwydajniejszy sposób liczenia wierszy jaki zostałyby zwrócone. Pobranie np. numerów id i wykonanie funkcji PHP "count" na tablicy wyników to najgorsze rozwiązanie. Fraza AS comnumber oznacza że wynik będzie dostępny pod podanym aliasem (traktować przy wyświetlaniu tak jakby to było zwykłe pole).

Konfiguracja

Teraz warto odwiedzić katalog konfiguracyjny naszego projektu. Z autoload.php:
$autoload['libraries'] = array('database', 'validation');
$autoload['helper'] = array('url', 'form');
database.php - podajemy dane bazy danych. Powyższe zapytania SQL tworzą tablice kurs_nazwa co oznacza (w modelach nie ma kurs_) prefiks:
$db['default']['dbprefix'] = "kurs_";
routes.php nazwa domyślnego kontrolera. U mnie blog:
$route['default_controller'] = "blog";

Kontrolery

Zabieramy się za nasz kontroler Blog. Listowanie wszystkich newsów na stronie głównej zrobić łatwo:
<?php
class Blog extends Controller
{
// konstruktor
function Blog()
        {
        parent::Controller();
        $this->response = array();
        }
function index()
        {
        $this->load->model('News');
        $query = $this->News->get_news();
        $content = '';
        // czy są jakieś wiersze?
        if ($query->num_rows() > 0)
                 {
                foreach($query->result() as $item)
                                {
                                $content .= $this->load->view('news_loop', $item, True);
                                }
                }
        else
                {
                // brak newsów
                $content = '<h1>Brak newsów</h1>';
                }
        // przekazanie danych do szablonu
        $this->response['content'] = $content;
        $this->load->view('index', $this->response);
        }
}
Tablica, która zawsze będzie trafiała do widoku index jest $this->response - globalna w klasie tablica zainicjowana w konstruktorze. (może okazać się to pomocne w dalszych etapach). Odnośnie metody index - ładujemy model News i wywołujemy metodę $query = $this->News->get_news(); co zwraca uchwyt do wyników. Za pomocą num_rows upewniamy się czy jakieś dane są, jeżeli tak to możemy otworzyć pętlę foreach i je wyświetlić. Widok news_loop to nic innego jak przerobiony widok main w postaci:
<h1><?PHP echo $news_title ?></h1>
<p class="date"><?PHP echo date("Y-m-d", $news_date); ?></p>
<p><?PHP echo $news_text; ?></p>
<div class="rule"></div>
Obecnie strona główna naszego bloga pokaże:
darmowy hosting obrazków
Teraz musimy dodać możliwość dodawania i edycji wpisów. Wszystko co ma znaleźć się w tzw. "Panelu Admina" umieścimy w oddzielnym kontrolerze o nazwie Admin:
<?php
class Admin extends Controller
{
// konstruktor
function Admin()
        {
        parent::Controller();
        $this->response = array();
        }
function index()
        {
        $this->response['content'] = '<h1><a href="'.site_url('admin/news_add').'">Dodaj Newsa</a></h1>';
        // wylistujmy sobie newsy (tytuły) z linkami do edycji i kasowania
        $this->load->model('News');
        $query = $this->News->get_news();
        if ($query->num_rows() > 0)
                 {
                 $this->response['content'] .= '<table width="100%" border="1" cellspacing="3" cellpadding="3">';
                foreach($query->result() as $item)
                                {
                                $this->response['content'] .= $this->load->view('news_loop_admin', $item, True);
                                }
                $this->response['content'] .= '</table>';
                }
        $this->load->view('index', $this->response);
        }
        
// dodanie newsa
function news_add()
        {
        $data["tytul"] = array('name' => 'tytul');
        $data['tresc'] = array('name' => 'tresc', 'rows' => 8, 'cols' => 50);
        $data['autor'] = array('name' => 'autor');
        $data['keywords'] = array('name' => 'keywords');
        
        $rules['tytul'] = "required|max_length[250]|xss_clean";
        $rules['tresc'] = "required|xss_clean";
        $rules['keywords'] = "required|max_length[250]|xss_clean";
        $this->validation->set_rules($rules);
        
        if ($this->validation->run() == FALSE)
                {
                $data['tytul']['value'] = $this->input->post('tytul');
                $data['tresc']['value'] = $this->input->post('tresc');
                $data['autor']['value'] = $this->input->post('autor');
                $data['keywords']['value'] = $this->input->post('keywords');
                $this->response['content'] = $this->load->view('news_add', $data, True);
                }
        else
                {
                $this->load->model('News');
                $this->News->add_news(array('news_title' => $this->input->post('tytul'), 'news_text' => $this->input->post('tresc'), 'news_author' => $this->input->post('autor'), 'news_date' => time(), 'news_keywords' => $this->input->post('keywords')));
                $this->response['content'] = '<h1>Dane zapisane</h1><META HTTP-EQUIV="Refresh" CONTENT="1; URL='.site_url('admin').'">';
                }
        $this->load->view('index', $this->response);
        }

// edycja newsa
function news_edit()
        {
        $this->load->model('News');
        $id = $this->uri->segment(3);
        IF(isset($id) and is_numeric($id))
                {
                $ar = $this->News->get_news_by_id($id)->result_array();
                
                $data["tytul"] = array('name' => 'tytul');
                $data['tresc'] = array('name' => 'tresc', 'rows' => 8, 'cols' => 50);
                $data['autor'] = array('name' => 'autor');
                $data['keywords'] = array('name' => 'keywords');
                
                $rules['tytul'] = "required|max_length[250]|xss_clean";
                $rules['tresc'] = "required|xss_clean";
                $rules['keywords'] = "required|max_length[250]|xss_clean";
                $this->validation->set_rules($rules);
                
                if ($this->validation->run() == FALSE)
                        {
                        IF(strlen($this->input->post('tytul')) > 0)
                                {
                                $data['tytul']['value'] = $this->input->post('tytul');
                                $data['tresc']['value'] = $this->input->post('tresc');
                                $data['autor']['value'] = $this->input->post('autor');
                                $data['keywords']['value'] = $this->input->post('keywords');
                                }
                        else
                                {
                                $data['tytul']['value'] = $ar[0]['news_title'];
                                $data['tresc']['value'] = $ar[0]['news_text'];
                                $data['autor']['value'] = $ar[0]['news_author'];
                                $data['keywords']['value'] = $ar[0]['news_keywords'];
                                }
                        $this->response['content'] = $this->load->view('news_edit', $data, True);
                        }
                else
                        {
                        $this->News->update_news($id, array('news_title' => $this->input->post('tytul'), 'news_text' => $this->input->post('tresc'), 'news_author' => $this->input->post('autor'), 'news_date' => time(), 'news_keywords' => $this->input->post('keywords')));
                        $this->response['content'] = '<h1>Zmiany zapisane</h1><META HTTP-EQUIV="Refresh" CONTENT="1; URL='.site_url('admin').'">';
                        }
                }
        else
                {
                $this->response['content'] = '<h1>Niepoprawny URL</h1>';
                }
        $this->load->view('index', $this->response);
        }

// usunięcie newsa
function news_delete()
        {
        $id = $this->uri->segment(3);
        IF(isset($id) and is_numeric($id))
                {
                $this->load->model('News');
                $this->News->delete_news($this->uri->segment(3));
                $this->response['content'] = '<h1>News skasowany</h1><META HTTP-EQUIV="Refresh" CONTENT="1; URL='.site_url('admin').'">';
                $this->load->view('index', $this->response);
                }
        }
// usunięcie komentarza
function com_delete()
        {
        $id = $this->uri->segment(3);
        IF(isset($id) and is_numeric($id))
                {
                $this->load->model('Comments');
                $this->Comments->delete_comment($this->uri->segment(3));
                $this->response['content'] = '<h1>Komentarz skasowany</h1><META HTTP-EQUIV="Refresh" CONTENT="1; URL='.site_url('admin').'">';
                $this->load->view('index', $this->response);
                }
        }
}
5 metod, pierwsza, index jest niejako główną stroną Panelu Admina i zawiera link do formularza dodawania Newsów oraz listę newsów wraz z linkami do edycji i kasowania. Widok news_loop_admin wygląda tak:
<tr><td><?PHP echo $news_title ?></td><td width="100"><center>[<a href="<?PHP echo site_url('admin/news_edit/'.$news_id); ?>">Edytuj</a>]</center></td><td width="100">[<a href="<?PHP echo site_url('admin/news_delete/'.$news_id); ?>" >Kasuj</a>]</td></tr>
Przy linki kasowania mamy dodatkow trochę javascriptu. Funkcja JS confirm wyświetli okienko z pytaniem czy "Usnąć News?" i dwoma przyciskami "Anuluj" i "OK". W przypadku kliknięcia "Anuluj" nic się nie stanie.
Następnie mamy metodę dodającą dane, co już było przerabiane we wcześniejszym warsztacie, oraz drugą edytującą, która ma jedną większą zmianę:
IF(strlen($this->input->post('tytul')) > 0)
Jako że chcemy mieć dotychczasowe dane do edycji to musimy je przypisać do formularza jeżeli tej nie był jeszcze wysłany. Funkcja strlen zwraca rozmiar podanego łańcucha. Jeżeli formularz nie został wysłany to długość łańcucha z jakiegoś jego pola na pewno będzie równa 0. Jeżeli jest większa znaczy że formularz został wysłany ale pojawiły się problemy - przypisujemy wartości z samego formularza.
W kodzie pojawia się:
$this->uri->segment(3)
Metoda $this->uri>segment(ID) zwróci wartość danego segmentu URLa. Np. index.php/foo/bar rozbija się na składowe: 1 to "foo", 2 to "bar". Wewnątrz CI nie ma tablic _POST, _GET, _cookie czy _SERVER (ze względów bezpieczeństwa i nie tylko). By pobrać określony element trzeba użyć owej metody. W naszym przypadku do kasowania i edycji newsów potrzebny jest numer ID danego newsa. Zakładamy URLe postaci index.php/news_OPERACJA/NUMER_ID i gotowe :)
Widok news_add.php
<h1>Dodaj news</h1>
<center><?=$this->validation->error_string; ?></center>

<?php echo form_open('admin/news_add'); ?>
<table width="90%" border="0" cellspacing="3" cellpadding="3">
<tr><td width="180"><B>Tytuł</B></td><td><?php echo form_input($tytul); ?></td></tr>
<tr><td width="180"><B>Treść</B></td><td><?php echo form_textarea($tresc); ?></td></tr>
<tr><td width="180"><B>Autor</B></td><td><?php echo form_input($autor); ?></td></tr>
<tr><td width="180"><B>Słowa Kluczowe</B></td><td><?php echo form_input($keywords); ?></td></tr>
<tr><td> </td><td><?php echo form_submit('submit', 'Zapisz'); ?></td></tr>
</table>
<?php echo form_close(); ?>

<div class="rule"></div>
Widok news_edit.php:
<h1>Edytuj news</h1>
<center><?=$this->validation->error_string; ?></center>

<?php echo form_open('admin/news_edit/'.$this->uri->segment(3)); ?>
<table width="90%" border="0" cellspacing="3" cellpadding="3">
<tr><td width="180"><B>Tytuł</B></td><td><?php echo form_input($tytul); ?></td></tr>
<tr><td width="180"><B>Treść</B></td><td><?php echo form_textarea($tresc); ?></td></tr>
<tr><td width="180"><B>Autor</B></td><td><?php echo form_input($autor); ?></td></tr>
<tr><td width="180"><B>Słowa Kluczowe</B></td><td><?php echo form_input($keywords); ?></td></tr>
<tr><td> </td><td><?php echo form_submit('submit', 'Zapisz'); ?></td></tr>
</table>
<?php echo form_close(); ?>

<div class="rule"></div>
Teraz gdy dodamy kilka newsów nasz blog zacznie powoli wyglądać:
darmowy hosting obrazkówdarmowy hosting obrazkówdarmowy hosting obrazków

Do Zrobienia

- Logowanie do PA
- Dodawanie i usuwanie komentarza
- Wyszukiwarka
- Inne fajne dodatki
RkBlog

Kurs Code Igniter, 14 July 2008, Piotr Maliński

Comment article