post_ico4

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

Tworząc różnego rodzaju aplikacje natrafiamy na poważny problem utrzymania dobrej organizacji kodu – przejrzystej oraz łatwej w rozbudowie. Z pomocą przychodzą nam wzorce projektowe, które wymuszają na nas pewną organizację kodu aplikacji. W świecie aplikacji www najbardziej popularny jest wzorzec MVC. Jego ideę pokażę w praktyce – pisząc prosty system artykułów.

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


Żeby w pełni zrozumieć ideę tego wzorca projektowego czytelnik musi mieć solidne podstawy znajomości PHP oraz potrafić programować obiektowo.

Trochę teorii…

Model-View-Controller został zaprojektowany w 1979 roku przez norweskiego programistę Trygve Reenskaug pracującego wtedy nad językiem Smalltalk w laboratoriach Xerox i początkowo nosił nazwę Model-View-Editor.

Ideą tego wzorca jest rozdzielenie kodu odpowiedzialnego za przetworzenie danych od kodu odpowiedzialnego za ich wyświetlanie.

Model-View-Controller zakłada podział aplikacji na trzy główne części:

  • Model – jest pewną reprezentacją problemu bądź logiki aplikacji.
  • Widok – opisuje, jak wyświetlić pewną część modelu w ramach interfejsu użytkownika.
  • Kontroler – przyjmuje dane wejściowe od użytkownika i reaguje na jego poczynania, zarządzając aktualizacje modelu oraz odświeżenie widoków.

Brzmi strasznie, ale w praktyce okazuje się, że to wcale nie jest takie trudne …

No to zaczynamy!

Na samym początku stwórzmy szkielet katalogów:

config/
controller/
model/
view/
templates/

Mając hierarchię katalogów stwórzmy szkielet plików wzorca MVC:

controller/controller.php

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

/**
 * This class includes methods for controllers.
 *
 * @abstract
 */
abstract class Controller{

    /**
     * It redirects URL.
     *
     * @param string $url URL to redirect
     *
     * @return void
     */
    public function redirect($url) {

    }
    /**
     * It loads the object with the view.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadView($name, $path='') {

    }
    /**
     * It loads the object with the model.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadModel($name, $path='') {

    }
}

model/model.php

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

/**
 * This class includes methods for models.
 *
 * @abstract
 */
abstract class Model{
    /**
     * object of the class PDO
     *
     * @var object
     */
    protected $pdo;

    /**
     * It sets connect with the database.
     *
     * @return void
     */
    public function  __construct() {

    }
    /**
     * It loads the object with the model.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadModel($name, $path='') {

    }
    /**
     * It selects data from the database.
     *
     * @param string $from Table
     * @param <type> $select Records to select (default * (all))
     * @param <type> $where Condition to query
     * @param <type> $order Order ($record ASC/DESC)
     * @param <type> $limit LIMIT
     * @return array
     */
    public function select($from, $select='*', $where=NULL, $order=NULL, $limit=NULL) {

    }
}

view/view.php

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

/**
 * This class includes methods for views.
 *
 * @abstract
 */
abstract class View{

    /**
     * It loads the object with the model.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadModel($name, $path='') {

    }
    /**
     * It includes template file.
     *
     * @return void
     */
    public function render() {

    }
    /**
     * It sets data.
     *
     * @param string $name
     * @param mixed $value
     *
     * @return void
     */
    public function set($name, $value) {

    }
    /**
     * It gets data.
     *
     * @param string $name
     *
     * @return mixed
     */
    public function get($name) {

    }
}

Pliki te zawierają zarys podstawowych metod, które wykorzystamy w tworzeniu systemu artykułów używając wzorca Model-View-Controller. Klasy oznaczyłem jako abstrakcyjne, gdyż będą one dziedziczone po bardziej specjalistycznych elementach – wykonujących już konkretne czynności. W dalszej części będziemy je sukcesywnie wypełniać kodem.

Tworzenie kodu aplikacji zacznijmy od controller/controller.php

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

/**
 * This class includes methods for controllers.
 *
 * @abstract
 */
abstract class Controller{

    /**
     * It redirects URL.
     *
     * @param string $url URL to redirect
     *
     * @return void
     */
    public function redirect($url) {
        header("location: ".$url);
    }
    /**
     * It loads the object with the view.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadView($name, $path='view/') {
        $path=$path.$name.'.php';
        $name=$name.'View';
        try {
            if(is_file($path)) {
                require $path;
                $ob=new $name();
            } else {
                throw new Exception('Can not open view '.$name.' in: '.$path);
            }
        }
        catch(Exception $e) {
            echo $e->getMessage().'<br />
                File: '.$e->getFile().'<br />
                Code line: '.$e->getLine().'<br />
                Trace: '.$e->getTraceAsString();
            exit;
        }
        return $ob;
    }
    /**
     * It loads the object with the model.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadModel($name, $path='model/') {
        $path=$path.$name.'.php';
        $name=$name.'Model';
        try {
            if(is_file($path)) {
                require $path;
                $ob=new $name();
            } else {
                throw new Exception('Can not open model '.$name.' in: '.$path);
            }
        }
        catch(Exception $e) {
            echo $e->getMessage().'<br />
                File: '.$e->getFile().'<br />
                Code line: '.$e->getLine().'<br />
                Trace: '.$e->getTraceAsString();
            exit;
        }
        return $ob;
    }
}

Metoda redirect() służy do przekierowania strony na wskazany adres. Z kolei metody loadModel() i loadView() inicjują obiekty klas widoku oraz modelu.

Plik model/model.php będzie wyglądać tak:

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

/**
 * This class includes methods for models.
 *
 * @abstract
 */
abstract class Model{
    /**
     * object of the class PDO
     *
     * @var object
     */
    protected $pdo;

    /**
     * It sets connect with the database.
     *
     * @return void
     */
    public function  __construct() {
        try {
            require 'config/sql.php';
            $this->pdo=new PDO('mysql:host='.$host.';dbname='.$dbase, $user, $pass);
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }
        catch(DBException $e) {
            echo 'The connect can not create: ' . $e->getMessage();
        }
    }
    /**
     * It loads the object with the model.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadModel($name, $path='model/') {
        $path=$path.$name.'.php';
        $name=$name.'Model';
        try {
            if(is_file($path)) {
                require $path;
                $ob=new $name();
            } else {
                throw new Exception('Can not open model '.$name.' in: '.$path);
            }
        }
        catch(Exception $e) {
            echo $e->getMessage().'<br />
                File: '.$e->getFile().'<br />
                Code line: '.$e->getLine().'<br />
                Trace: '.$e->getTraceAsString();
            exit;
        }
        return $ob;
    }
    /**
     * It selects data from the database.
     *
     * @param string $from Table
     * @param <type> $select Records to select (default * (all))
     * @param <type> $where Condition to query
     * @param <type> $order Order ($record ASC/DESC)
     * @param <type> $limit LIMIT
     * @return array
     */
    public function select($from, $select='*', $where=NULL, $order=NULL, $limit=NULL) {
        $query='SELECT '.$select.' FROM '.$from;
        if($where!=NULL)
            $query=$query.' WHERE '.$where;
        if($order!=NULL)
            $query=$query.' ORDER BY '.$order;
        if($limit!=NULL)
            $query=$query.' LIMIT '.$limit;

        $select=$this->pdo->query($query);
        foreach ($select as $row) {
            $data[]=$row;
        }
        $select->closeCursor();

        return $data;
    }
}

Konstruktor klasy Model ma nawiązać połączenie z bazą danych (używamy do tego PDO). Metoda loadModel() ma za zadanie stworzyć obiekt z klasą modelu. Natomiast select() będziemy używać do pobieranie danych.

Dodajmy jeszcze kod do pliku view/view.php:

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

/**
 * This class includes methods for views.
 *
 * @abstract
 */
abstract class View{

    /**
     * It loads the object with the model.
     *
     * @param string $name name class with the class
     * @param string $path pathway to the file with the class
     *
     * @return object
     */
    public function loadModel($name, $path='model/') {
        $path=$path.$name.'.php';
        $name=$name.'Model';
        try {
            if(is_file($path)) {
                require $path;
                $ob=new $name();
            } else {
                throw new Exception('Can not open model '.$name.' in: '.$path);
            }
        }
        catch(Exception $e) {
            echo $e->getMessage().'<br />
                File: '.$e->getFile().'<br />
                Code line: '.$e->getLine().'<br />
                Trace: '.$e->getTraceAsString();
            exit;
        }
        return $ob;
    }
    /**
     * It includes template file.
     *
     * @param string $name name template file
     * @param string $path pathway
     *
     * @return void
     */
    public function render($name, $path='templates/') {
        $path=$path.$name.'.html.php';
        try {
            if(is_file($path)) {
                require $path;
            } else {
                throw new Exception('Can not open template '.$name.' in: '.$path);
            }
        }
        catch(Exception $e) {
            echo $e->getMessage().'<br />
                File: '.$e->getFile().'<br />
                Code line: '.$e->getLine().'<br />
                Trace: '.$e->getTraceAsString();
            exit;
        }
    }
    /**
     * It sets data.
     *
     * @param string $name
     * @param mixed $value
     *
     * @return void
     */
    public function set($name, $value) {
        $this->$name=$value;
    }
    /**
     * It gets data.
     *
     * @param string $name
     *
     * @return mixed
     */
    public function get($name) {
        return $this->$name;
    }
}

Metoda loadModel() działa tak samo jak w poprzednich plikach. Metodę render() będziemy wykorzystywać do dołączania plików z kodem HTML.

Tworzymy tabele bazy danych

Na zakończenie tej części stwórzmy tabele kategorii i artykułów w bazy danych:

CREATE TABLE categories(
id integer auto_increment,
name varchar(100),
primary key(id)
);

CREATE TABLE articles(
id integer auto_increment,
title varchar(100),
content text,
date_add datetime,
autor varchar(100),
id_categories integer,
primary key(id),
foreign key(id_categories) references categories(id)
);

W pliku config/sql.php zapiszmy dane dostępu do bazy danych.

<?php
$host='localhost';
$dbase='baza danych';
$user='user';
$pass='hasło';

W kolejnej części cyklu zaczniemy już dokładniej poznawać idee MVC. Stworzymy fragment aplikacji odpowiedzialny za dodawanie kategorii.

Powiązane tematy

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (3 głosów, średnia ocen: 3,67 z 5)
Loading...
  • W artykule źle przedstawiona jest warstwa modelu, nie ma ona obsługiwać danych lecz zwracać je w ujednoliconej formie. Po co ci w View loadModel?

  • Model ma za zadanie pobrać dane z jakiegoś źródła (baza danych, pliki etc) i przekazać je przetworzone do widoku.

    Co do loadModel w widoku, moim zdaniem wygodniej jest odwołać się do modelu bezpośrednio w widoku niż bawić się w przekazywanie danych przez kontroler. Oczywiście są „różne szkoły” implementacji MVC – co komu bardziej pasuje.

  • Posio

    Super, dzięki tobie zacząłem rozumieć zasadę na jakiej opiera sie cały model MVC

  • xxx

    błąd jest w pierwszym przykładzie abstract class controller podane są funkcje do abstract class view

  • @xxx Dzięki! Coś przy kopiowaniu zepsuło się ;)

  • Gość

    „Oczywiście są „różne szkoły” implementacji MVC – co komu bardziej pasuje.”

    Nie szkoła jest jedna. Ludzie po prostu mylą MVC z MVP ;).

    Pozdrawiam.

  • Dzięki bardzo za ten artykuł. Własnie staram się ogarnąć MVC i bardzo mi w tym pomagasz :)

    A co do:
    „Nie szkoła jest jedna. Ludzie po prostu mylą MVC z MVP”

    Haters gonna hate. ;]

  • Błażej

    dla set i get są już wbudowane metody magiczne w php, proponuję zastąpić je np. w ten sposób:
    public function __set($name, $value) {
    $this->data[$name]=$value;
    }

    public function __get($name) {
    if (array_key_exists($name, $this->data)) {
    return $this->data[$name];
    } else {
    return false;
    }
    }

    • Guest

      Metody magiczne set i get są wywoływane tylko dla pól nieistniejących w klasie. Poza tym pogarszają przejrzystość kodu :)

    • Metody magiczne set i get są wywoływane tylko dla pól nieistniejących w klasie. Poza tym pogarszają przejrzystość kodu :)

      • johny bravo

        A właściwości private w klasie? Czy nie dla nich stosuje się get i set ? Np. setName, albo getName.

        • No tak, ale lepiej napisać metody set i get dla każdego potrzebnego pola niż korzystać z magicznych metod __get i __set.

  • Akira

    Przekopałem się przez wiele artykułów polskich i anglojęzycznych na temat wzorców projektowych i frameworków. Tylko ten artykuł a szczególnie przykłady, pozwoliły mi w pełni zrozumieć istotę i zasadę działania MVC. Dz

  • Rafał Kowalski

    czego po wczytaniu pliku index mam biala pusta strone ale to popier…..

  • johny bravo

    Wszystko jasne. Dorobiłem logowanie,ale nie wiem jak zaimplementować stronicowanie. Potrzebna jest nowa klasa, czy wystarczy zmodyfikować model pobierający wpisy z bazy?

    • W metodzie select w Model poza zmienną $limit musisz dopisać zmienną $offset żeby nie pobierać rekordów zawsze od początku. Jak powinno wyglądać LIMIT masz tutaj: http://php.about.com/od/mysqlcommands/g/Limit_sql.htm

      Następnie, na podstawie np. $_GET[‚numer_strony’]*$iloscWpisowNaStronie określasz od jakiego rekordu w bazie ma ci zapytanie pobierać dane. Z kolei ilość stron możesz wyliczyć ze wzoru $iloscRekordowWBazie / $iloscWpisowNaStronie.

      • johny bravo

        Dzięki! Już zrobiłem stronicowanie :). Doskonały artykuł o MVC. Duża wiedza autora :)

  • johny bravo

    Mam jeszcze jedna uwagę i ciekaw jestem co na to autor. Strona startowa systemu newsów zawiera kilka elementów, które ładują się jednocześnie tzn. kilka pierwszych wpisów w skróconej wersji, 5 najnowszych komentarzy, archiwum z podziałem na miesiące i lata, kategorie. Taki typowy blog zawiera te elementy. Jak jednocześnie załadować na stronę to wszystko? Czy trzeba użyć Ajaxa, czy da się to zrobić samym php. Ja wykorzystuję JQuery i Ajax, ale może można to zrobić inaczej…

    • johny bravo

      Już wiem jak to zrobić. Można w oddzielnym pliku stworzyć bezpośrednio obiekt modelu z pominięciem Controllera i View, następnie includować w potrzebnym miejscu. np. kod pobierający ilość komentarzy do danego wpisu itp.

      • Do tego właśnie stworzyłem loadModel w kontrolerze. Dzięki temu możesz w kontrolerze załadować kilka modeli i z nich pobierać potrzebne dane.

  • tomatlover

    Witam..
    Genialny artykuł! Jestem początkującym programistom i artykuł objaśnia mi wiele spraw. Tylko mam mały problem, programuje w C# i chciałbym podobny artykuł znaleźć w tym języku. Możecie mi pomóc w odszukaniu takiego źródła?
    Z góry bardzo dziękuje!

    • Ja niestety nie znam takiego. Ani linijki kodu nie napisałem w C# :)

  • Krzych

    Czy do działania jest potrzebny jakiś .htacces?

    Od razu wywala błąd:

    Notice: Undefined index: task in D:xampphtdocsartykuly_demoindex.php on line 3

    Notice: Undefined index: task in D:xampphtdocsartykuly_demoindex.php on line 7

    Fatal error: Class ‚ArticlesController’ not found in D:xampphtdocsartykuly_demoindex.php on line 12

    • Nie, sądząc po błędach nie podajesz task w adresie, np. ?task=articles

  • Krzysiek

    Bardzo fajny artykuł. Nie rozumiem tylko jednego.
    W widoku jest metoda

    public function set($name, $value) {
    $this->$name=$value;
    }

    a w kontrolerze używasz jej tak:

    $this->set(‚catsData’, $cat->getAll());

    Ale przecież w przykładzie nie masz nigdzie pola
    np:

    private $catsData;

    W jaki więc sposób to zadziała?

    • PHP umożliwia dynamiczne tworzenie zmiennych, a więc nie jest konieczne deklarowanie wszystkich pól klasy,

      Dobrą praktyką jest zazwyczaj unikanie tego typu rozwiązań. Korzystam z niego tylko w widoku, ponieważ moim zdaniem nie ma sensu tworzyć w takiej sytuacji wszystkich pól, które są tylko wyświetlane w szablonie.

      • Krzysiek

        O, nie wiedziałem tego. Dzięki za szybką odpowiedź.

      • erochan

        A mógłbym dostać odpowiedź na moje pytanie? Zadałem je w części 3 :D

        • Odpisałem. W gąszczu maili zawieruszyło się powiadomienie z Disqus i nie zauważyłem twojego komentarza :)

  • Borys

    Mam dwa pytanka. 1. Czy administracja cms’a tez musi mieć oddzielony html od reszty? 2. Czy masz pomysł na zaadoptowanie aplikacji, która już jest napisana kodem strukturalnym. Jest to cms’ który dołącza w zależności od parametru odpowiedni plik php, który zawiera i logike i widok. Masz jakiś pomysł na zaplanowanie echanizmu, który bedzie robił wyjątek dla wybranych parametrów i wczytywał po prostu plik php jako widok i mechanizmy danego modułu? :)

    • Ideą MVC jest oddzielenie poszczególnych warstw aplikacji – niezależnie czy jest to backend, czy frontend.

      W panelu administracyjnym często logika jest zdecydowanie bardziej rozbudowana niż we frontendzie i korzystanie z MVC daje więcej korzyści.

      Jeżeli wszystko jest strukturalne i jeszcze zapytania SQL wymieszane z kodem HTML to sugeruję przepisać kod od 0 :)

      Z takiego dostosowania u mnie jeszcze nic dobrego nie wynikło…

  • Patryk

    Czy w 2016 napisany MVC jest poprawny ?