post_ico4

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

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

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

src/Controller/Article.php

<?php
namespace RacyMind\MVCWPraktyce\Controller;


/**
 * Kontroler do artykułów.
 * @package RacyMind\MVCWPraktyce\Controller
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */
class Article extends \RacyMind\MVCWPraktyce\Engine\Controller
{
    /**
     * Wyświetla listę artykułów
     */
    public function index()
    {
        $model = new \RacyMind\MVCWPraktyce\Model\Article();
        $articles = $model->getAll();
        $view = new \RacyMind\MVCWPraktyce\View\Article();
        $view->articles = $articles;
        $view->renderHTML('index', 'front/article/');
    }
    /**
     * Wyświetla jeden artykuł
     */
    public function one()
    {
        $model = new \RacyMind\MVCWPraktyce\Model\Article();
        $article = $model->getOne($_GET['id']);
        $view = new \RacyMind\MVCWPraktyce\View\Article();
        $view->article = $article;
        $view->renderHTML('one', 'front/article/');
    }

    /**
     * Wyświetla formularz i dodaje artykuł
     */
    public function add()
    {
        $model = new \RacyMind\MVCWPraktyce\Model\Article();
        if (!empty($_POST)) {
            $model->insert($_POST);
            $this->redirect($this->generateUrl('article/index'));
        } else {
            $modelCategory=new \RacyMind\MVCWPraktyce\Model\Category();
            $view = new \RacyMind\MVCWPraktyce\View\Article();
            $view->categories=$modelCategory->getAll();
            $view->renderHTML('add', 'front/article/');
        }
    }

    /**
     * Usuwa artykuł
     */
    public function delete()
    {
        $model = new \RacyMind\MVCWPraktyce\Model\Article();
        $model->delete($_GET['id']);;
        $this->redirect($this->generateUrl('article/index'));
    }
}

Kontroler artykułów posiada 4 akcje:

  • index() – metoda ta pobiera z modelu wszystkie artykuły i przekazuje je do widoku. Na koniec ładuje szablon HTML
  • one() – metoda ta pobiera z modelu jeden artykuł i przekazuje go do widoku. Na koniec ładuje szablon HTML
  • add() – metoda wyświetla szablon z formularzem dodawania artykułu oraz przekazuje dane z tablicy $_POST do modelu.
  • delete() – wywołuje metodę modelu usuwającą artykuł z bazy danych. Następnie przekierowuje użytkownika na listę artykułów.

W jaki sposób wywołać odpowiednie akcje kontrolera?

W mojej przykładowej aplikacji odpowiedzialny jest za to router. W pliku config-router.php znajduje się taki fragment kodu:

$collection->add('article/delete', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly/usun/<id>?',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'delete',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    ),
    array(
        'id' => '\d+'
    ),
    array(
        'id' => 0
    )
));
$collection->add('article/one', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly/wyswietl/<id>?',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'one',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    ),
    array(
        'id' => '\d+'
    ),
    array(
        'id' => 0
    )
));
$collection->add('article/add', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly/dodaj',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'add',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    )
));
$collection->add('article/index', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'index',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    )
));
$collection->add('homepage', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'index',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    )
));

Router porównuje adres URL strony z powyższymi regułami. Jeżeli jakaś reguła pasuje do adresu URL wywołuje metodę odpowiedniego kontrolera. Uwaga. Jeżeli skrypt znajdzie pasującą regułę nie sprawdza już kolejnych, a więc najbardziej ogólne reguły adresu URL powinny być na końcu.

Tworzymy model artykułów

Model artykułów będzie w pliku src/Model/Article.php.

<?php

namespace RacyMind\MVCWPraktyce\Model;

/**
 * Model artykułów.
 * @package RacyMind\MVCWPraktyce\Model
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */
class Article extends \RacyMind\MVCWPraktyce\Engine\Model
{
    /**
     * Zwraca z bazy danych wszystkie artykuły
     * @return array|null Tablica z artykułami
     */
    public function getAll()
    {
        $query = $this->pdo->query("SELECT a.id, a.title, a.date_add, a.author, c.name FROM articles AS a LEFT JOIN categories AS c ON a.id_categories=c.id");
        $items = $query->fetchAll(\PDO::FETCH_ASSOC);
        if (isset($items)) {
            return $items;
        } else {
            return null;
        }
    }

    /**
     * Zwraca z bazy danych jeden artykuł
     * @param int $id ID artykułu
     * @return array|null Tablica z danymi jednego artykułu
     */
    public function getOne($id)
    {
        $query = $this->pdo->query("SELECT a.id, a.title, a.date_add, a.author, c.name, a.content FROM articles AS a LEFT JOIN categories AS c ON a.id_categories=c.id where a.id=".(int)$id);
        $items = $query->fetchAll(\PDO::FETCH_ASSOC);
        if (isset($items[0])) {
            return $items[0];
        } else {
            return null;
        }
    }

    /**
     * Dodaje artykuł do bazy
     * @param $data Dane do zapisu
     */
    public function insert($data) {
        $ins=$this->pdo->prepare('INSERT INTO articles (title, content, date_add, author, id_categories) VALUES (
            :title, :content, :date_add, :author, :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(':author', $data['author'], \PDO::PARAM_STR);
        $ins->bindValue(':id_categories', $data['cat'], \PDO::PARAM_INT);
        $ins->execute();
    }

    /**
     * Usuwa artykuł z bazy
     * @param int $id ID artykułu
     */
    public function delete($id) {
        $del=$this->pdo->prepare('DELETE FROM articles where id=:id');
        $del->bindValue(':id', $id, \PDO::PARAM_INT);
        $del->execute();
    }
}

Model ten posiada 4 metody:

  • getAll() – pobiera wszystkie rekordy z tabeli categories
  • getOne() – pobiera jeden artykuł o podanym ID z bazy danych
  • insert() – dodaje nowy artykuł do bazy
  • delete() – usuwa artykuł o podanym ID z bazy danych

Tworzymy widok artykułów

src/View/Article.php

<?php

namespace RacyMind\MVCWPraktyce\View;

/**
 * Widok dla artykułów.
 * @package RacyMind\MVCWPraktyce\View
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */
    class Article extends \RacyMind\MVCWPraktyce\Engine\View{
        public function __construct() {
        }
    }

Widok ten dziedziczy tylko metody po klasie View. W bardziej zaawansowanych aplikacjach konstrukcja taka umożliwia dopisywanie dodatkowych metod dla poszczególnych modułów.

Tworzymy szablony HTML dla artykułów

Na koniec stworzymy jeszcze trzy szablony HTML.

src/template/front/article/index.html.php

<?php $this->getHeader(); ?>
<h1>Lista artykułów</h1>
<table>
    <tr>
        <td>Tytuł</td>
        <td>Data dodania</td>
        <td>Autor</td>
        <td>Kategoria</td>
        <td>&nbsp;</td>
    </tr>
    <?php foreach($this->articles as $item): ?>
        <tr>
            <td><a href="<?php echo $this->generateUrl('article/one', array('id' => $item['id'])); ?>"><?php echo $item['title']; ?></a></td>
            <td><?php echo $item['date_add']; ?></td>
            <td><?php echo $item['author']; ?></td>
            <td><?php echo $item['name']; ?></td>
            <td><a href="<?php echo $this->generateUrl('article/delete', array('id' => $item['id'])); ?>">usuń</a></td>
        </tr>
    <?php endforeach; ?>
    </tbody>
</table>
<?php $this->getFooter(); ?>

src/template/front/article/add.html.php

<?php $this->getHeader(); ?>
<h1>Dodaj artykuł</h1>
<form action="" 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="<?php echo date("Y:m:d"); ?>" /><br />
    Treść:<br />
    <textarea name="content"></textarea><br />
    Kategoria: <select name="cat" size="0">
        <?php foreach($this->categories as $item): ?>
            <option value="<?php echo $item['id'] ;?>"><?php echo $item['name']; ?></option>
        <?php endforeach; ?>
    </select><br />
    <input type="submit" value="Dodaj" />
</form>
<?php $this->getFooter(); ?>

src/template/front/article/one.html.php

<?php $this->getHeader(); ?>
<h1><?php echo $this->article['title']; ?></h1>
autor: <?php echo $this->article['author']; ?>, data dodania: <?php echo $this->article['date_add']; ?><br />
Kategoria: <?php echo $this->article['name']; ?>

<p><?php echo $this->article['content']; ?></p>
<?php $this->getFooter(); ?>

Szablony korzystają z pól i metod klasy \View\ARticle.

Podsumowanie

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.

W porównaniu z kodem z 2011 roku aplikacja jest bardziej czytelna i łatwiejsza do rozbudowy. Dzięki wykorzystaniu przestrzeni nazw oraz standardu PSR-4 struktura plików jest przejrzysta i logiczna. Z kolei composer ułatwia dołączanie zewnętrznych bibliotek.

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

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (4 głosów, średnia ocen: 5,00 z 5)
Loading...
  • Bolus150

    jeżeli ktoś ma na serwerze wrzucone ten skrypt to jak bedzie wyglądało :
    collection = new RacyMindMVCWPraktyceEngineRouterRouteCollection();

  • Bolus150

    Fatal error: Class ‚RacyMindMVCWPraktycesrcEngineRouterRouteCollection’ not found in /virtual/bor.cba.pl/mvc-w-praktyce/config-router.php on line 2
    co zrobić z tyM?

    • Błąd mówi, że nie może znaleźć klasy w załadowanych plikach. Może masz niepoprawne ścieżki w pliku config.php?

  • Dariusz Rorat

    Analizując te kody:
    1. Po co tworzyć odrębne widoki do renderowania templatek w katalogu src/View, mówię o klasach Article oraz Category, które to rozszerzają tylko bazowy View i mają zresztą tylko konstruktory w których i tak nic się nie dzieje? Można przecież użyć tylko bazowej klasy View z katalogu Engine do renderowania określonej templatki (a raczej do wszystkich templatek). Tworzenie tych jeszcze klas widoków wydaje się być bezcelowe (i nie ma takiego podejścia we frameworkach).

    2. Można by się zastanawiać nad rozłożeniem plików i katalogów
    – dać podkatalog app albo application

    – pliki config można by (żeby było chyba bardziej przejrzyście) składować np. w

    app/resources/config
    -pliki templatek można by składować np. w app/resources/views
    – modele i kontrolery aplikacji można by składować np w. app/src/Controller, app/src/Model
    3. Po co rozbijać bazowe templatki na header.html.php i footer.html.php skoro można by użyć tylko base.html.php i tam wstrzykiwać tylko odpowiedni content, no jest jeszcze kwestia jak wstrzykiwać assety ale to inna sprawa.
    4. Podkatalog src/Engine oznacza i jest związany z kodami frameworka (a taki micro-framework przy okazji stworzyłeś :-) ), tez można by się zastanawiać gdzie to składować, bo skoro byłby np. app, to można by użyć np. framework/src albo nawet do tego celu użyć vendor/framework/src.

    5. Trzeba też mieć na względzie (w nawiązaniu do p.1), że to co tu przedstawiasz to obrazuje wzorzec MVP (model-view-presenter) i co również istotne Passive View (templatki-widoki), natomiast warto by się też było zastanowić nad Layout Pattern (stąd uwaga w p3) oraz może nie ActiveRecord ale DataMapper, zamiast tego podejścia w modelach, gdzie używasz tego PDO (a co jeśli użytkownik chciałby użyć MySQLi zamiast tego albo innego silnika baz danych???).

    • 1. W tym bazowym przykładzie może i wygląda ma przerost formy nad treścią. Szkielet ten wykorzystuję w autorskim projekcie i tam już widoki się przydają. Mogę np. wywołać metodę do formatowania daty z poziomu szablonu. Innym rozwiązaniem jest zwracanie danych w innym formacie niż HTML. Wystarczy dopisać metodę w widoku i wywołać z poziomu kontrolera.

      2. Można, to już jak komu wygodniej :). Mi wystarczyło rozbicie na src/ i vendor/

      3. Też można. Jak to będzie rozwiązane ma drugorzędne znaczenie. Od programisty zależy.

      4. Można by z tego kodu dość prosto zrobić „paczkę” do composera. W takiej sytuacji trzymałbym go już w vendor :). Niemniej wygodniej mi jest jak biblioteki zewnętrzne mam w vendor/ a szkielet w src/

      5. W sumie kodowi bliżej do MVP niż czystego MVC. Nie zwracając aż takiej wagi na nazewnictwo jest to po prostu najbardziej wygodna dla mnie implementacja MVC i jego pochodnych.

      Zawsze można dociągnąć biblioteki za pomocą Composera i lekko zmodyfikować klasę Model. Struktura tego mojego mini frameworka jest na tyle elastyczna, że nie powinno to zająć wiele czasu.

  • Jak zabezpieczasz błędne adresy? Na przykład, kiedy otwieramy http://adresaplikacji.com/błędny_url ?

    • Na przykład, w pliku index.php:

      if(!$file) {
      $file = DIR_CONTROLLER.’InfoPage.php’;
      $classController=’RacyMindCRMControllerInfoPage’;
      $method=’error404′;
      }

      • Spróbowałem to zrobić takim warunkiem, dodałem nawet na stałe stronę z error404 do kolekcji i kiedy normalnie wpisuję w adresie to jest ok, ale kiedy wbijam błędny adres to wywala:
        Fatal error: Class name must be a valid object or a string in /index.php on line 17

        Mój index w tym momencie:
        run();

        $file=$router->getFile();
        if(!$file) {
        $file = DIR_CONTROLLER.’Error.php’;
        $classController=’DziobakNPCControllerError’;
        $method=’error404′;
        }
        $classController=$router->getClass();
        $method=$router->getMethod();
        require_once($file);
        $obj = new $classController();
        $obj->$method();

        • Mam to, głupi ja oczywiście. $classController i $method nadpisywal mi późniejszy kod. Czyli dobrze kombinowałem. Dzięki za pomoc i świetny poradnik!

          • Dokładnie. Cieszę się, że udało się rozwiązać problem ;)

      • Dorzucę jeszcze kawałek kolekcji, który używam i działa z błędem 404:
        $collection->add(‚error404′, new DziobakNPCEngineRouterRoute(
        HTTP_SERVER.’error404’,
        array(
        ‚file’ => DIR_CONTROLLER.’Error.php’,
        ‚method’ => ‚error404’,
        ‚class’ => ‚DziobakNPCControllerError’
        )
        ));