Modev View Controller framework своїми руками

Опубліковано: 2009-08-28   21:08:30

PHPДекілька днів тому з великим задоволенням прочитав статтю про паттерн Спостерігач (Observer). І вирішив, що треба таки написати статтю про дуже розповсюджений зараз паттерн проектування Model View Controller (Модель Відображення Контроллер, скорочено MVC). З тих самих пір як я познайомився з ним більше двох років тому на прикладі фреймворку Ruby on Rails, я використовую самей цей паттерн в проектуванні бульш-менш великих проектів. І впевнений, що його використання дозволяє значно покращити зрозумілість програмного коду, спростити його рефакторинг та подальшу підтримку. Думаю, що після невеличкого приклада, всі хто ще не знайомий з цим паттерном будуть вимушені погодитись з цим.

Зараз цей паттерн використовується в CMS Joomla! та Livestreet, в .NET framework теж було додано інструментарій для роботи саме в рамках цієї ідеології, Swing для Java вимагає (наскільки це можливо) писати саме на основі такого паттерну, та ще й існує багато фреймворків, основаних на ньому: вищезгаданий Ruby on Rails, Django, Code Igniter, Zend Framework, Spring. Тому розумыння MVC є одною з необхідних речей для будь-якого програміста, що вважає себе професіоналом, оскільки дозволить значно легше розібратись з будь-яким з вищеперерахованих та ще десятків інших готових рішень..

В якості прикладу я напишу простенький php-фреймворк, на основі якого розроблю елементарний двигунець для блога (так-так, я "містер-оригінальність" :)) з можливістю додавати, видаляти та переглядати статті. Хоча й слово "розроблю" виглядає тут занадто пафосно ;

Модель (Model)

Першим елементом паттерну MVC є модель. Модель - спосіб збереження та органіації данних всередені проекту написаного в рамках паттерну MVC. Просто кажучи, це і є см набір данних та методи для роботи з цим набором, такі як зебреження та отримання інформації з БД (файлу, мережі), доступ до окремого елемента і т. п.

Поясню детальніше на прикладі класу, що дозволяє вибирати дані з таблиці та поводить себе як проста нетипізована колекція (файл model.php):

/**
* Class for working with data from database
*/
class Model implements Iterator {
/** Database connection */
protected static $con = null;
/** Database table */
protected $table = null;
/** Items */
protected $var = null;

/* Iterator imterface methods */
/** Go to the begining of the array */
public function rewind() {
return reset($this->var);
}
/** Returns value of the current arrays element */
public function current() {
return current($this->var);
}
/** Returns value of the end arrays element */
public function end() {
return end($this->var);
}
/** Returns key of the current arrays element */
public function key() {
return key($this->var);
}
/** Go to the next element and return it's value */
public function next() {
return next($this->var);
}
/** Returns valid */
public function valid() {
$var = $this->current() !== false;
return $var;
}
/** Return count of selected elements */
public function count() {
return sizeof($this->var);
}
/** Get element with specified index */
public function get($index) {
return (isset($this->var[$index]))? $this->var[$index]: null;
}
/** clear data */
public function clear() {
$this->var = array();
}

/**
* Constructor
*/
public function __construct($table) {
// Create conneciton if needed
if (self::$con == null) {
self::$con = mysql_connect('localhost:3306', 'username', 'password')
or die('Could not connect: '.mysql_error());
mysql_select_db('blog');
}
$this->table = $table;
}
/**
* Destructor
*/
public function __destruct() {
if (self::$con == null)
mysql_close(self::$con);
}

/**
* Item's selector
*/
public function select($query = '') {
// Select items
$query = ($query and !empty($query))? $query: 'SELECT * FROM '.$this->table;
$res = mysql_query($query)
or die('Could not select items: '.mysql_error().'</br> on query '.$query);
// Clear previously selected data
$this->clear();
// Fetch selected data
while ($this->var[] = mysql_fetch_object($res)) ;
}

}

Наведений вище класс має поле $var, призначене для збереження данних, та набір методів що дозволяють переміщуватись в рамках цього набору елементів. Сам масив данних заповнюється через виклик методу select(), що виконує запит до бази данних і за замовчуванням обираж всі елементи з таблиці для данної моделі. Як можна побачити метод написано таким чином, що запит можна легко змінити. Саме підключення до БД виконує конструктор, який також встановлює ім'я таблиці з якох за замовчуванням обираються данні. Для початку такого набору методів цілком вистачить і монжна перейти до наступного елементу цього паттерну.

Представлення (View)

Представлення - спосіб яким будуть представлені дані користувачу. У випадку простого блога, це буде набір php файлів, що відповідають різним функціям системи. Наприклад для списку повідомлень в блозі (файл messages.index.php):

<h1>Messages list</h1>

<?php
foreach ($GLOBALS['messages'] as $message){
?>
<h2><?php echo $message->title; ?></h2>
<p><?php echo $message->body; ?></p>
<?php
}
?>

<a href="dispatcher.php?con=messages&act=create"title="createnewitem">Create new message</a>

Однак використання паттерну MVC не зводиться тільки но написанням web-орієнтованих проектів, тому в якості представлення можуть виступати інші класи чи функції, що наприклад будують GUI. Чи наприклад в рамках того ж web-програмування, може викорситовуватись якийсь шаблонізатор. Тому для уніфікації нашого фреймворку напишемо класс, що буде відповідати за роботу з представленнями (файл view.php):

/**
* View class
*/
class View {
/**
* Show element
*/
public static function show($controller, $method) {
include $controller.'.'.$method.'.php';
}
}

Тепер замінивши метод show() можна переорієнтувати фреймфорк на іншу модель відображення данних.

Котролер (Controller)

Контролер - частина проекту, що выдповідає за формування логіки роботи проекту. Тобто саме тут виконується операції з моделлю, та підготовка данних для представлення користувачу. Для поставленої задачі для початку вистачить ось такого простого класу (файл MessagesController.php):

class MessagesController {
/** Model for this controller */
private $model;
/**
* Constructor
*/
public function __construct() {
$this->model = new Model('messages');
}
/**
* Select method
*/
public function index() {
$this->model->select();
$GLOBALS['messages'] = $this->model;
View::show('messages', 'index');
}
}

Наведений вище класс під час ініціалізації створює необхідну йому для роботи модель. Також він має метод для вибірки елементів з моделі, що зберігає цю вибірку в глобальну змінну та викликає метод для завантаження представлення.

Зберемо все до купи

Залишається об'єднати всі прошарки в єдиний елемент проект. Для цього створимо файл:dispatcher.php, що буде точкою входу:

include 'model.php';
include 'view.php';
$controller = $_REQUEST['con']? $_REQUEST['con']: 'messages';
$action = $_REQUEST['act']? $_REQUEST['act']: 'index';
$class = ucfirst($controller.'Controller');
include $class.'.php';
$controller = new $class();
$controller->$action();

Також необхідно створити на локальному MySQL сервері користувача uasername з паролем password (чи замінити відповідні елементи в model.php), буза данних blog та таблицю messages з трьома полями:

CREATE TABLE `blog`.`messages` (
`id` int(11) NOT NULL auto_increment COMMENT 'Messsage index',
`title` varchar(255) NOT NULL COMMENT 'Message title',
`body` text NOT NULL COMMENT 'Message text',
PRIMARY KEY (`id`)
) ENGINE=MyISAM COMMENT='Table for messages'

Набравши в адресному рядку браузеру щось на зразок http://localhost/dispatcher.php ми маємо побачити сторінку з одним посиланням "Create new message". Якщо перейти за цим посиланням, то отримаємо повідомлення:

Warning: View::include(messages.create.php) [view.include]: failed to open stream: No such file or directory in /var/www/grandse/view.php on line 10

Warning: View::include() [function.include]: Failed opening 'messages.create.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/grandse/view.php on line 10

Так.. До цього повернемось трошки пізінше, а поки що змусимо наш проект хоча б щось нам показувати. Для цього достатньо лише додати запис в таблицю БД. І цього вже буде достатньо для перегляду списку повідомлень. А от з додаванням нового допису доведеться ще попрацювати.

Розширюємо функціонал

Створимо файл messages.crate.php такого змісту:

<h1>Create new message</h1>

<form method="post">
<input type="hidden"name="con"value="messages"/>
<input type="hidden"name="act"value="save"/>
<label for="title">Title:</label>
<input type="text"name="title"value=""/> </br>
<label for="body">Message body:</label>
<textarea name="body"></textarea> </br>
<input type="submit"value="Addmessage"/>
</form>

Та додамо функцію в контролер:

public function create() {
View::show('messages', 'create');
}

Тепер якщо перейти за посиланням після списку повідомлень, то побачимо форму додавання нового запису. Однак, форма хоч і є, однак додавання повідомлення не спрацює. Тому треба знову ж таки додати необхідний метод в контролер, цього разу для внесення змін:

public function save() {
$this->model->insert(array('title' => $_REQUEST['title'], 'body' => $_REQUEST['body']));
$this->index();
}

Якщо поглянути на клас Model, то можна побачити, що методу insert модель не має. Тому його необхідно створити. Одразу додамо в модель функціонал і для видалення елементів:

public function insert(Array $item) {
$query = 'INSERT INTO '.$this->table.' (';
$values = ') vALUES (';
$is_first = true;
foreach ($item as $name => $value) {
if (!$is_first) {
$query .= ', ';
$values .= ', ';
}
else
$is_first = false;
$query .= $name;
$values .= '\''.mysql_real_escape_string($value).'\'';
}
mysql_query($query.$values.')')
or die('Could not insert item: '.mysql_error());
}

public function remove($field, $value) {
mysql_query('DELETE FROM '.$this->table.' WHERE '.$field.' = \''.mysql_real_escape_string($value).'\'')
or die('Could not remove item: '.mysql_error());
}

Ну що ж, залишається додати відповідні методи в контроллер:

public function save() {
$this->model->insert(array('title' => $_REQUEST['title'], 'body' => $_REQUEST['body']));
$this->index();
}

public function remove() {
if ((int)$_REQUEST['id']>0)
$this->model->remove('id', $_REQUEST['id']);
$this->index();
}

Тепер додавання нового елементу має працювати, а от з видаленням складніше: ми не маємо посилання яке б видаляло елемент. Це легко виправити відредагувавши файл messages.index.php:

<h1>Messages list</h1>

<?php
foreach ($GLOBALS['messages'] as $message){
?>
<h2><?php echo $message->title; ?></h2>
<p><?php echo $message->body; ?></p>
<a href="dispatcher.php?con=messages&act=remove&id=?php echo $message->id; ?>" title="Remove message">Remove</a>
<?php
}
?>

<a href="dispatcher.php?con=messages&act=create" title="create new item">Create new message</a>

Так просто

Так. Насправді все так просто. Витративши всього годину-півтори можна написати такий от простий php framework, що буде реалізовувати паттерн Model View Controller, та дозволить швиденько писати програмний код в рамках цього паттерну. Тай паттерн сам простий і як можно побачити при розумному икористання зменшує як кількість коду необхідного для реалізації рутинних операцій так і дозволяє розділити програму на три частини: дані, логіка та взаємодія з користувачем. Це саме по собі дозволяє зробити код більш зрозумілим та прозорим, причому не тільки тому хто його писав, а ще будь-якій людині, яка витратила годину-дві (а може всього 15 хвилин :)) на те, щоб зрозуміти, що таке той звір MVC.

Доречі, можете завантажити приклад з кінцевим варіантом описаного вище "блогового двигунці". Дякую за увагу!

Коментарі: 2
 

Коментарі:

atbserg2009-12-24 03:51:45 :

Поправте назву статті з Modev на Model. Як людина може писати та роздумувати про те чого навіть і написати правильно не може.

 
GrAndSE2009-12-24 04:00:08 :

Дуже легко. Так само, як легко зробити описку в тексті. Цікаво, що до Вас це читало багато людей, і ніхто не звертав мою увагу на це.

 

Додати коментар

user

email

url

text

Повідомляти про новікоментарі