post_ico4

MVC w praktyce – tworzymy system artykułów. cz. 3

W ostatniej części artykułu o wzorcu MVC stworzymy pozostałe elementy prostego systemu artykułów.

Uwaga. Pojawił się zaktualizowany cykl o MVC – przejdź


Dobrą praktyką przy budowaniu aplikacji z użyciem wzorca MVC jest „rozbicie” całego kodu na poszczególne, mniejsze moduły. W poprzedniej części stworzyliśmy fragmenty kodu do obsługi kategorii, teraz zajmiemy się artykułami.

Tworzymy kontroler artykułów

Na początek tworzymy kontroler controllers/articles.php:

<?php
/**
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version: 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */

include 'controller/controller.php';

class ArticlesController extends Controller{

    public function index() {
        $view=$this->loadView('articles');
        $view->index();
    }
    public function one() {
        $view=$this->loadView('articles');
        $view->one();
    }
    public function add() {
        $view=$this->loadView('articles');
        $view->add();
    }
    public function insert() {
        $model=$this->loadModel('articles');
        $model->insert($_POST);
        $this->redirect('?task=articles&action=index');
    }
    public function delete() {
        $model=$this->loadModel('articles');
        $model->delete($_GET['id']);
        $this->redirect('?task=articles&action=index');
    }
}

I tu także przeanalizujmy reakcje dla następujących adresów URL:

  • ?task=articles&action=index – zostanie wywołana metoda index(), która inicjuje obiekt widoku articles, następnie zostaje wywołana metoda index()
  • ?task=articles&action=one – zostanie wywołana metoda one(), która inicjuje obiekt widoku articles, następnie zostaje wywołana metoda one()
  • ?task=articles&action=add – zostanie wywołana metoda add(), która inicjuje obiekt widoku articles, następnie zostaje wywołana metoda add()
  • ?task=articles&action=insert – zostanie wywołana metoda insert(), która inicjuje obiekt modelu articles, następnie zostaje wywołana metoda insert()
  • ?task=articles&action=delete – zostanie wywołana metoda delete(), która inicjuje obiekt modelu articles, następnie zostaje wywołana metoda delete()

Musimy jeszcze zmodyfikować plik index.php, by skrypt wywoływał kontroler Articles:

<?php

if($_GET['task']=='categories') {
    include 'controller/categories.php';
    $ob = new CategoriesController();
    $ob->$_GET['action']();
} else if($_GET['task']=='articles') {
    include 'controller/articles.php';
    $ob = new ArticlesController();
    $ob->$_GET['action']();
} else {
    include 'controller/articles.php';
    $ob = new ArticlesController();
    $ob->index();
}

Mamy już kontroler. Teraz przejdźmy do modelu.

Tworzymy model artykułów

model/articles.php:

<?php
/**
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version: 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */

include 'model/model.php';

class ArticlesModel extends Model{

    public function getAll() {
        $query="SELECT a.id, a.title, a.date_add, a.autor, c.name FROM articles AS a LEFT JOIN categories AS c ON a.id_categories=c.id";
        $select=$this->pdo->query($query);
        foreach ($select as $row) {
            $data[]=$row;
        }
        $select->closeCursor();

        return $data;
    }
    public function getOne($id) {
        $query="SELECT a.id, a.title, a.date_add, a.autor, c.name, a.content FROM articles AS a LEFT JOIN categories AS c ON a.id_categories=c.id where a.id=".$id;
        $select=$this->pdo->query($query);
        foreach ($select as $row) {
            $data[]=$row;
        }
        $select->closeCursor();

        return $data;
    }
    public function insert($data) {
        $ins=$this->pdo->prepare('INSERT INTO articles (title, content, date_add, autor, id_categories) VALUES (
            :title, :content, :date_add, :autor, :id_categories)');
        $ins->bindValue(':title', $data['title'], PDO::PARAM_STR);
        $ins->bindValue(':content', $data['content'], PDO::PARAM_STR);
        $ins->bindValue(':date_add', $data['date_add'], PDO::PARAM_STR);
        $ins->bindValue(':autor', $data['author'], PDO::PARAM_STR);
        $ins->bindValue(':id_categories', $data['cat'], PDO::PARAM_INT);
        $ins->execute();
    }
    public function delete($id) {
        $del=$this->pdo->prepare('DELETE FROM articles where id=:id');
        $del->bindValue(':id', $id, PDO::PARAM_INT);
        $del->execute();
    }
}

Tak jak w przypadku kategorii, metody w ArticlesModel dodają, usuwają oraz poobierają dane z bazy danych.

Tworzymy widok artykułów

Pozostaje nam jeszcze stworzenie widoku. Plik view/articles.php:

<?php
/**
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version: 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */

include 'view/view.php';

class ArticlesView extends View{
    public function  index() {
        $art=$this->loadModel('articles');
        $this->set('articles', $art->getAll());
        $this->render('indexArticle');
    }
    public function  one() {
        $art=$this->loadModel('articles');
        $this->set('articles', $art->getOne($_GET['id']));
        $this->render('oneArticle');
    }
    public function add() {
        $cat=$this->loadModel('categories');
        $this->set('catsData', $cat->getAll());
        $this->render('addArticle');
    }
}

Metoda index() generuje szablon HTML z listą wszystkich artykułów. Metoda one() wyświetla tylko jeden artykuł. Z kolei add() ma za zadanie wygenerować formularz dodawania artykułu. Warto zauważyć, że metoda ta pobiera listę kategorii z modelu CategoriesModel.

A wiec poszczególne warstwy aplikacji mogą wykorzystywać instancje wielu modeli. W dobrze zaprojektowanej aplikacji manipulacja danymi jest bardzo prosta – wystarczy załadować odpowiedni model i wykorzystywać jego publiczne metody.

Do pełni działającego skryptu brakuje nam jeszcze tylko plików szablonu.

Aktualizujemy plik templates/header.html.php:

<html>
    <head>
        <title>System newsów</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <link href="css/style.css" rel="stylesheet" type="text/css" />
    </head>
    <body>
        <ul>
            <li><a href="?task=categories&amp;action=add">Dodaj kategorię</a></li>
            <li><a href="?task=categories&amp;action=index">Lista kategorii</a></li>
            <li><a href="?task=articles&amp;action=add">Dodaj artykuł</a>
            <li><a href="?task=articles&amp;action=index">Lista artykułów</a>
        </ul>

templates/addArticle.html.php:

<? include 'templates/header.html.php'; ?>

<h1>Dodaj artykuł</h1>
<form action="?task=articles&amp;action=insert" method="post">
    Tytuł: <input type="text" name="title" /><br />
    Autor: <input type="text" name="author" /><br />
    Data dodania: <input type="text" name="date_add" value="<?= date("Y:m:d"); ?>" /><br />
    Treść:<br />
    <textarea name="content"></textarea><br />
    Kategoria: <select name="cat" size="0">
        <? foreach($this->get('catsData') as $cats) { ?>
            <option value="<?= $cats['id'] ;?>"><?= $cats['name']; ?></option>
        <? } ?>
    </select><br />
    <input type="submit" value="Dodaj" />
</form>

<? include 'templates/footer.html.php'; ?>

templates/indexArticle.html.php:

<? include 'templates/header.html.php'; ?>

<h1>Lista artykułów</h1>
<table>
    <tr>
        <td>Tytuł</td>
        <td>Data dodania</td>
        <td>Autor</td>
        <td>Kategoria</td>
        <td>&nbsp;</td>
    </tr>
    <? foreach($this->get('articles') as $articles) { ?>
    <tr>
        <td><a href="?task=articles&amp;action=one&amp;id=<?= $articles['id']; ?>"><?= $articles['title']; ?></a></td>
        <td><?= $articles['date_add']; ?></td>
        <td><?= $articles['autor']; ?></td>
        <td><?= $articles['name']; ?></td>
        <td><a href="?task=articles&amp;action=delete&amp;id=<?= $articles['id']; ?>">usuń</a></td>
    </tr>
    <? } ?>
</table>

<? include 'templates/footer.html.php'; ?>

templates/oneArticles.php:

<? include 'templates/header.html.php'; ?>

    <? foreach($this->get('articles') as $articles) { ?>
<h1><?= $articles['title']; ?></h1>
autor: <?= $articles['autor']; ?>, data dodania: <?= $articles['date_add']; ?><br />
Kategoria: <?= $articles['name']; ?>

<p><?= $articles['content']; ?></p>
    <? } ?>

<? include 'templates/footer.html.php'; ?>

Zakończenie

Tak stworzyliśmy działający skrypt oparty na wzorcu MVC. Dzięki rozdzieleniu warstwy logiki od formy prezentacji dalsza rozbudowa staje się znacznie prostsza. Możemy teraz dodać szablon HTML, bez potrzeby zaglądania w kontrolery oraz modele. Jest to szczególnie przydatne przy pracy nad większymi aplikacjami – osoby odpowiedzialne za wygląd mogą pracować praktycznie bez pomocy programistów.

Cały kod można pobrać stąd.

Powiązane tematy

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (5 głosów, średnia ocen: 3,40 z 5)
Loading...
  • Witam, bardzo fajne tutoriale, wszystko fajnie, ale jak ktoś by chciał skorzystać z kodu, który umieściłeś to się rozczaruje. Mianowicie błędy:
    Notice: Undefined index: task in C:\xampp\htdocs\artykuly_demo\index.php on line 3

    Notice: Undefined index: task in C:\xampp\htdocs\artykuly_demo\index.php on line 7

    Fatal error: Class ‚ArticlesController’ not found in C:\xampp\htdocs\artykuly_demo\index.php on line 12
    No ale to znika jak wpiszę odpowiedni adres, więc np adres – http://localhost/demo/index.php?task=categories&action=index generuje mi taki widok w przeglądarce:
    Notice: Undefined variable: data in C:\xampp\htdocs\demo\model\model.php on line 89
    Dodaj kategorię
    Lista kategorii
    Dodaj artykuł
    Lista artykułów
    Lista kategorii

    Warning: Invalid argument supplied for foreach() in C:\xampp\htdocs\demo\templates\indexCategory.html.php on line 9Nazwa
    Wynika stąd że coś tam brakuje. Jeśli więc możesz to zerknij na to. Ale i tak dziękuję za opisanie tego zagadnienia, z którym wiele osób- w tym także ja – ma problem :)

  • Te błędy wyglądają jakby po prostu nie była inicjowana zmienna task w tablicy $_GET. Pisząc ten tutorial nie bawiłem się w obsługę wyjątków itp. Jest to tylko kod poglądowy, by przybliżyć wzorzec MVC :)

  • x

    mama poprawka kontroler w plik index.php, powinien wyglądać tak:

    $_GET[‚action’]();
    } else if($_GET[‚task’]==’articles’) {
    include ‚controller/articles.php’;
    $ob = new ArticlesController();
    $ob->$_GET[‚action’]();
    } else {
    include ‚controller/articles.php’;
    $ob = new ArticlesController();
    $ob->index();
    }

    ;)

  • xy

    masz błąd w skrypcie wywołującym kontroler Articles,
    poprawna wersja index.php wygląda tak:
    if($_GET[‚task’]==’categories’) {
    include ‚controller/categories.php’;
    $ob = new CategoriesController();
    $ob->$_GET[‚action’]();
    } else if($_GET[‚task’]==’articles’) {
    include ‚controller/articles.php’;
    $ob = new ArticlesController();
    $ob->$_GET[‚action’]();
    } else {
    include ‚controller/articles.php’;
    $ob = new ArticlesController();
    $ob->index();
    }
    a ponieważ brakuję Ci tam include ‚controller/articles.php’; w else wiec paser php nie potrafi odnaleźć ArticlesController() ;)

  • Racja, dzięki za wskazanie niedopatrzenia :)

  • Pawel

    Witam
    czy mógłbyś pokazać jak/gdzie umieścić walidacje danych w MVC?

  • Ja bym umieścił w modelu (w końcu walidacja danych to ich przetwarzanie), ale często spotykana jest także praktyka walidacji w kontrolerze (przed wysłaniem danych do modelu).

  • Pawel

    A mógłbyś zaproponować rozwiązanie tego problemu? tzn jak ma reagować kontroller po odkryciu błędnych danych, oraz jak wysłać informacje o błędzie do widoku oraz jak ją potem wyświetlić.

  • Hmm możesz np zrobić przekierowanie po napotkaniu błędnych danych.

    Przykład:
    if(bledne dane) {
    $this->redirect(„http://jakisadres?error=zly_email”);
    }
    I teraz w odpowiedniej akcji sprawdzasz czy istnieje zmienna error. Jeżeli tak, reagujesz odpowiednio :)

  • Pawel

    tak, tylko wtedy przekierowuje mnie to do innej akcji, a zależy mi żebym dalej pozostał w task=artice&action=add

    chciałbym żeby dalej była wyświetlona zawartość addArticle.html.php(czyli formularz) + opis błędnie wypełnionych danych.

    Kontroler ma sprawdzić poprawność danych i jeżeli są błędne to dalej wczytać action=add z opisanym błędem. I właśnie nie wiem jak to zrobić

  • No to zrobić przekierowanie na task=article&action=add&error=cosTam i wpisane dane ew. zapamiętać

  • MiK

    Czy posiadasz archiwum z poprawionymi i działającymi plikami?

  • malin

    Notice: Undefined index: task in C:\xampp\htdocs\PhpProject8\index.php on line 3
    Notice: Undefined index: task in C:\xampp\htdocs\PhpProject8\index.php on line 7
    Fatal error: Class ‚ArticlesController’ not found in C:\xampp\htdocs\PhpProject8\index.php on line 12

    Mam problem podany wyżej jak go rozwiązać?

  • Pierwsze dwa błedy to brak zmiennej task w adresie. Ostatni błąd to zapewne nie dołączasz pliku z kontrolerem Articles.

  • Andrzej

    Bardzo fajny tutorial (mimo drobnych błędów w codzie ;)).
    Wszystko prosto i czytelnie przedstawione.

    Brawa dla autora.

  • bethart

    Super tutorial! Zastanawiam się nad jedną rzeczą – chciałbym ubogacić ten skrypt o logowanie się – na sesjach i to pojawia się problem – nie mogę jednocześnie zrobić session_start() i wysyłać header z przekierowaniem. Ktoś ma jakiś pomysł jak to inaczej obejść? Może autor?

    Pozdrawiam

    • Powinno pomóc:

      session_start();
      $_SESSION[‚status’] = ‚Updated Poem successfully’;
      session_write_close();
      header(„location: index.php”);

      • bethart

        dzięki, pomogło

  • Struś Pierdziwiatr

    Tutorial zawiera sporo błędów. Błedne ścieżki w includach, błędne otwieranie tagów php i zle skontruowana baza.

    Udało mi się wszystko poprawić. Wrzucam link do poprawionej całości tylko musicie dodać chociaż po jednym rekordzie dla tabeli articles i categories zeby wszystko działało.

    http://speedy.sh/6W42v/mvc.rar

  • bujnos

    w wersji RAR po drugim else w index.php brakuje
    include ‚controller/articles.php’;
    i wywala błąd

  • erochan

    Mam pytanie, czy będzie to poprawne dla wzorca mvc jeśli sprawdzanie w np. $_POST[‚send’] ustawię w kontrolerze coś takiego?

    public function reg(){

    $reg = $this->Model;

    if(isset($_POST[‚send’])){

    $this->save_user();

    }else{

    include ‚View/view_register.php’;

    }

    }
    public function save_user(){

    $reg = $this->Model_reg;

    include ‚View/view_register_success.php’;

    }

    Bawię się teraz z mvc więc czy to będzie poprawne rozwiązanie dla mvc czy raczej tego if-a ustawić w wejściowym pliku(np.index.php)? oczywiście obie metody zadziałają(ta która jest wykonywana i tą o którą się pytam) i wykona się wszystko poprawnie, ale głównie zależy mi o poprawności pisania mvc. Zaczynam od zera sam pisząc więc na początek takie małe skrypciki nie mające żadnego znaczenia ale tak bym wiedział co i jak działa w każdym kroku.

    • Ten if powinien być w kontrolerze. Ja zwykle sprawdzam w kontrolerze czy są jakieś dane do zapisania (np. w tablicy POST). Jeżeli są przetwarzam dane (walidacja itp) i wywołuję odpowiednią metodę modelu.

      • erochan

        Mam jeszcze jedno pytanko, Czy można tak pisać w kontrolerze?

        public function dataedit(){

        $model = $this->Model;

        if(isset($_POST[‚send’])){

        $model->editdata($_POST[‚text1’],$_POST[‚text2’],$_SESSION[‚id’]);
        include ‚View/view_statuseditdata.php’;
        }
        include ‚View/view_editdata.php’;
        }

        Wydaje mi się że jest to poprawne zapisane choć nie wiem. Co do nazywania nazw to ja zawsze mam z tym problem, trochę je dziwnie nazywam ale łatwo mi szukać później gdybym miał coś poprawiać i wiedział na przyszłość do czego co służy.

        p.s. Mam nadzieje że załapię się gdzieś na staż w PHP po magisterce w tym roku ^_^

        • Unikałbym używania include. Poza tym do nazewnictwa najlepiej używać standardu PS-R http://dominikmarczuk.pl/2014/08/psr-4

          PS: W związku z tym, że wpisy o MVC nadal cieszą się popularnością. Mimo, że powstały w 2011 roku, postanowiłem ponownie napisać artykuł o MVC, ale dostosowany do narzędzi dostępnych aktualnie :). W ciągu kilku dni (najdalej w ciągu tygodnia) pojawi się nowy wpis.

          • erochan

            a nie przeszkadza jeśli w kontrolerze robię tą część?
            $model->editdata($_POST[‚text1’],$_POST[‚text2’],$_SESSION[‚id’]);

          • Lepiej przepuścić dane z tablicy POST przez jakiś walidator, ale poza tym może tak być.

  • Yolo

    Oszukista jeden

    • Co masz na myśli?

      • Yolo

        Nie wiem, choć się domyślam

  • Zibi

    Król PHP #oszukista