Архитектурный паттерн MVC

По всему интернет-миру разбросаны миллионы веб-приложений. Есть совсем простые, есть такие, что сам «архитектор матрицы ногу сломит». Но их объединяет одно — MVC.

Самый популярный архитектурный паттерн в мире среди веб-приложений — модель-представление-контроллер (Model View Controller или просто MVC). Впервые, он был использован ещё в конце 70-х двадцатого века, в приложениях на языке Smalltalk. А затем, его приютили программисты Java и расшарили для всего мира и всех языков программирования. PHP не стал исключением. Сегодня, только малая часть программистов, коллекционирующих раритетный PHP-код может себе позволить не смотреть в сторону MVC.

Таким популярным он стал неспроста. Он просто рождён для создания гибких и масштабируемых приложений, которые легко сопровождать и достраивать.
Цель нашего тьюториала — показать на простом примере, как работает паттерн MVC.

Чтобы выполнить задания, вам потребуются следующие программы:

Программное обеспечение или ресурс Требуемая версия
Denwer (PHP5.3) 3 +
Notepad, или любой текстовый редактор
Mozilla Firefox 3.6 +

Примечания:

  • Мы предполагаем, что у вас есть базовые знания PHP.

Паттерн MVC

Теперь обо всём  по порядку. Сначала раскроем великую тайну аббревиатуры, в которой, очевидно, отражается тот факт, что приложение будет представлять собой три взаимодействующие части:

  • Модель отвечает за управление данными, она сохраняет и извлекает сущности, используемые приложением, как правило, из базы данных и содержит логику, реализованную в приложении.
  • Представление несет ответственность за отображение данных, которые даёт контроллер. С представлением тесно связано понятие шаблона, который позволяет менять внешний вид показываемой информации. В веб-приложении представление часто реализуется в виде HTML-страницы.
  • Контроллер связывает модель и представление. Он получает запрос от клиента, анализирует его параметры и обращается к модели для выполнения операций над данными запроса. От модели поступают уже скомпонованные объекты. Затем они перенаправляются в представление, которое передаёт сформированную страницу контроллеру, а он, в свою очередь, отправляет её клиенту.

Схематично потоки данных в этой модели можно представить так:
потоки данных в MVC

Вход в реальность

Давайте наконец сформулируем реальную задачу. Пусть нам заказали построить сайт социальной сети. В этой гигантской задаче есть маленькая подзадача: используя имеющуюся базу друзей, обеспечить просмотр их полного списка, а также детальную информацию по каждому другу.

Мы не будем сейчас рассматривать архитектуру всей социальной сети. Мы возьмём только маленькую подзадачку, представим всю её серьёзность и применим к ней паттерн MVC.

Как только мы начинаем его использовать, то сразу задумываемся — а как бы нам расположить скрипты нашего решения так, что бы всё было под рукой? Для этого, разместим каждый из трёх разделов нашей MVC-системы по отдельным папкам и, таким образом, получим простую структуру каталогов, в которой легко найти то, что нам нужно. Кроме того, эти три папки поместим в каталог lib, и вынесем его выше корневого веб-каталога www:

/lib
--/controller
---- FrendCnt.php
--/model
---- Frend.php
---- FrendList.php
--/view
---- frendlist.php
---- frendone.php
/www
-- index.php
-- .htaccess

Вынесение каталога lib (содержащего движок нашего сайта) из веб-каталога даёт нам бОльшую защищённость, делая нашу систему недоступной для посягательств шаловливых ручонок взломщиков.

Контроллер

Теперь обо всём по порядку. Начнём с контроллера, так как он первый из трёх компонентов паттерна встречает клиентский запрос, разбирает его на элементы, инициализирует объекты модели. После обработки данных моделью, он принимает её ответ и отправляет его на уровень представления.

В нашем простом примере, контроллер будет сконцентрирован в одном классе FrendCnt. Подробнее его опишем позже.А сейчас немного о точке входа в веб-приложение — это, конечно, будет файл index.php. В нём, мы определим точку отсчёта для подключения наших скриптов. Создадим экземпляр контроллера, и вызовем у него метод, который начнёт обрабатывать HTTP-запрос и определит что делать дальше.

Листинг №1 (файл index.php):

$baseDir = dirname(__FILE__) . '/..';
include_once($baseDir . "/lib/controller/FriendCnt.php");
$controller = new FriendCnt();
$controller->invoke();

Теперь о контроллере. У нас — это класс FriendCnt. Вы уже заметили, что экземпляр этого класса создаётся в index.php. Он имеет только один метод invoke(), который вызывается сразу после создания экземпляра. В конструкторе контроллера, создаётся объект на основе класса модели — FrendList (список друзей) для оперирования с данными.

В функции invoke(), на основе пришедшего HTTP-запроса, принимается решение: какие данные потребуются от модели. Затем происходит вызов метода извлекающего данные. Далее происходит подключение шаблонов для отображения, которым передаются данные из контроллера. Обратите внимание, что контроллер ничего не знает о базе данных или о том, как страница генерится.

Листинг №2 (файл контроллера FriendCnt.php):

require_once($baseDir . '/lib/model/FriendList.php');
class FriendCnt {
	public $oFriendList;
	public function __construct() {
		$this->oFriendList = new FriendList();
	}

	public function invoke() {
		global $baseDir;
		$oFriendList = $this->oFriendList;
		if(isset($_GET['key'])) {
			$oFriendList->setKey($_GET['key']);
			$oFriend = $oFriendList->fetch();
			include $baseDir . '/lib/view/friendone.php';
		}else {
			$aFriend = $oFriendList->fetch();
			include $baseDir . '/lib/view/friendlist.php';
		}
	}
}

Модель и сущности

Модель — это образ реальности, из которой взято только то, что нужно для решения задачи. Модель концентрируется на логике решения основной задачи.  Многие называют это бизнес-логикой, на ней лежит большая ответственность:

  • Сохранение, удаление, обновление данных приложения. Это реализуется через операции с базой данных или через вызов внешних веб-сервисов.
  • Инкапсуляция всей логики приложения. Абсолютно вся логика приложения без исключений должна быть сконцентрирована в модели. Не нужно какую-то часть бизнес-логики выносить в контроллер или представление.

У нас к модели относятся два скрипта, в каждом из которых определён свой класс. Центральный класс FriendList и класс-сущность Friend. В центральном классе, происходит манипуляция с данными: получение данных от контроллера и их обработка. Класс-сущность служит контейнером для переноса данных между моделью и представлением, а также определяет их формат. При хорошей реализации паттерна MVC, классы сущности не должны упоминаться в контроллере, и они не должны содержать какую-либо бизнес-логику. Их цель — только хранение данных.
В классе FriendList, работающем со списком друзей, мы создали функцию, которая моделирует взаимодействие этого класса с базой данных. Метод getFriendList() возвращает массив из объектов, созданных на основе класса Friend. Для обеспечения удобства работы с данными, также была создана функция, индексирующая массив объектов. Контроллеру оказались доступны только два метода: setKey() — устанавливает поле ключа, по которому возвращаются детальные данные о друге; fetch() — возвращает или конкретный объект или весь список друзей.

Листинг №3 (файл модели FriendList.php):

require_once($baseDir . '/lib/model/Friend.php');
class FriendList {
	private $oneKey;
	private function getFriendList() {
		return array(
			new Friend("Александр", "1985", "alex@mail.com"),
			new Friend("Юрий", "1987", "yury@gmail.com"),
			new Friend("Алексей", "1989", "alexey@yahoo.com"),
		);
	}

	private function getIndexedList() {
		$list = array();
		foreach($this->getFriendList() as $val) {
			$list[$val->getKey()] = $val;
		}
		return $list;
	}

	public function setKey($key) {
		$this->oneKey = $key;
	}

	public function fetch() {
		$aFriend = $this->getIndexedList();
		return ($this->oneKey) ? $aFriend[$this->oneKey] : $aFriend;
	}
}

В зависимости от реализации объектов Сущности, данные о ней, могут быть оформлены в виде XML-документа или JSON-объекта.

Листинг №4 (файл сущности Friend.php):


class Friend {
	private $key;
	private $name;
	private $yearOfBirth;
	private $email;
	public function __construct($name, $yearOfBirth, $email) {
		$this->key = md5($name . $yearOfBirth . $email);
		$this->name = $name;
		$this->yearOfBirth = $yearOfBirth;
		$this->email = $email;
	}
	public function getKey() {
		return $this->key;
	}
	public function getName() {
		return $this->name;
	}
	public function getYearOfBirth() {
		return $this->yearOfBirth;
	}
	public function getEmail() {
		return $this->email;
	}
}

Представление

Теперь нам нужно представить данные в наилучшем свете для пользователя.

Настал черёд поговорить о Представлении. В зависимости от задачи, данные могут быть переданы представлению в разных форматах: простые объекты, XML-документы, JSON-объекты и т.д. В нашем случае передаётся объект или массив объектов. При это мы не побеспокоились о выводе базового слоя — то, что относится к футеру и хедеру генерируемой страницы, этот код повторяется в обоих файлах представления. Но для нашего небольшого примера это не важно.

Главное здесь показать, что представление отделено от контроллера и модели. При этом контроллер занимается передачей данных от модели к представлению.

В нашем примере представление содержит только два файла: для отображения детальной информации о друге и для отображения списка друзей.

Листинг №5 (файл для вывода списка друзей friendlist.php):

<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>Мои друзья</title>
</head>
<body>
	<table>
		<tr>
			<th>Имя</th>
			<th>Год рождения</th>
		</tr>
		<?php foreach($aFriend as $oFriend) : ?>
		<tr>
			<td>
				<a href="index.php?key=<?php echo $oFriend->getKey() ?>">
					<?php echo $oFriend->getName() ?>
				</a>
			</td>
			<td><?php echo $oFriend->getYearOfBirth() ?></td>
		</tr>
		<?php endforeach ?>
	</table>
</body>
</html>

Листинг №6 (файл для вывода списка друзей friendone.php):

<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title><?php echo $oFriend->getName() ?> : Мой друг</title>
</head>
<body>
    <?php
        echo 'Имя: ' . $oFriend->getName() . '<br/>';
        echo 'Год рождения: ' . $oFriend->getYearOfBirth() . '<br/>';
        echo 'Email: ' . $oFriend->getEmail() . '<br/>';
    ?>
	<a href="/">Список</a>
</body>
</html>

Если вы перенесёте весь этот код на веб-сервер, то в результате вы получите микро-сайт не на две страницы (если судить по количеству файлов представления), а уже на четыре. На первой будет показан список друзей, а на остальных трёх — детальная информация по каждому другу.

Мы могли бы реализовать детальный просмотр с помощью AJAX, тогда бы у нас была всего одна страница, и мы формировали бы часть представления через JSON-объекты непосредственно на компьютерах клиентов. Существует куча вариантов на этот счёт.

Это упрощённый пример веб-приложения на основе паттерна MVC. Но уже на нём можно увидеть массу возможностей. К плюсам мы уже отнесли гибкость и масштабируемость. Дополнительными плюсами будут — возможности стандартизации кодирования, лёгкость обнаружения и исправления ошибок, быстрое вхождение в проект новых разработчиков. Кроме того, вы можете в своём приложении изменять способ хранения сущностей, используя для этого сторонние веб-сервисы и облачные базы данных. Из минусов можно привести только небольшое увеличение объёма скриптов. А так, сплошные плюсы. Так-что пользуетесь на здоровье.

Здесь лежат файлы проекта, качайте сравнивайте:

Ну как? Какие мысли? Комментируем, не стесняемся.

Понравилась статья? Посоветуйте другу

Количество коментариев: 12

  1. Лишней логики в отображениях, вроде бы, нет. И всё (что удивительно) работает. Но вот вопрос: как добавить в приложение, допустим, категорию «Враги»? Надо создать 6 новых классов?

  2. Дмитрий Артанов Дмитрий Артанов

    Сразу возникает вопрос, зачем на сайте друзей, враги?
    Ну а если всё-таки хочется их учитывать, то можно добавить к модели друга статус — «вышел из доверия».

  3. Ну, вы ведь понимаете, что речь не о конкретной сущности. Можно было добавить и «Кошки». Или «Увлечения». Но по существу вы дали ответ, которого я и ожидал: добавить метку, ещё один атрибут к сущности. А теперь основной вопрос (предыдущий был предварительный :-):

    Каким волшебным образом title в вашем View должен меняться с Друзья на Враги (Кошки, Собаки, Решения)? Или каждой новой сущности надо добавлять по два новых шаблона? Вы не планировали показать это на примере, как это могло бы выглядеть в коде? Я — планирую 🙂

    • Дмитрий Артанов Дмитрий Артанов

      В статье взят упрощённый пример, чтобы у начинающих глаза шибко не разбегались, а то потом не догонишь 🙂
      Вы хотите чтобы потенциально сущностей было не ограниченное количество?

  4. Для начала достаточно пяти (сущностей). По одному-два объекта в каждой для примера.

    Пример «фреймворка» у вас получился идеальный, мне он очень помог кое-что понять. Он идеальный ещё и в плане доработки — на простом коде очень хорошо видно, где что можно усовершенствовать. Вот и возникает идея перейти «на следующую ступень», сделать пример пусть посложнее для понимания, но более идеальный в плане автоматики (отсутствия «плохих» зависимостей). Я практически закончил свою версию этой «второй ступени»…

  5. Комментарии на моём сайте, конечно, не сахар, но зато нет капчи. И ни одного робота! Вывел всех простой генерацией одного поля на javascript.

  6. Спасибо за урок.
    Вопрос: Когда посетитель возвращается к списку (index.php), каждый раз снова создается новый объект класса FriendCnt ?

    $controller = new FriendCnt();

    • Дмитрий Артанов Дмитрий Артанов

      Точно, и даже больше. Он создаётся и на просмотр информации об одном друге.
      Это главный контроллер, через него всё идёт.

  7. Здравствуйте, а как сделать чтобы когда я зохожу по адресу в http://mvc/ — у меня не показывает индекс, а показывает просто две папка, а когда уже заходишь в /www, то работает,

    Заранее благодарю.

    • Дмитрий Артанов Дмитрий Артанов

      Если я правильно понял проблему, то она связана с веб сервером. Домен mvc привязался не к той директории.
      Нужно уточнение — вы пользуетесь denwer?

  8. Привет! А если все же усложнить пример:
    — добавить вывод базового слоя(футеру и хедеру), сделать ваш пример, как бы блоком в общем шаблоне из других блоков;
    — вместо генерируемого хэша «$this->key = md5($name . $yearOfBirth . $email);», попытаться сделать какой то ЧПУ по типу site.ru/dryzja/pasha-hrennikov/.
    — доработать класс по работе с бд;
    Вобщем, вот что мне хотелось бы сделать, вроде бы вижу полную картину как мне кажется, но как оно должно выглядеться на уровне ооп и парадигмы мвц, какие ещё паттерны туда добавить незнаю. Возможно вы сможете привести более детальный пример, и этим сильно помочь мне 🙄 Или же пнуть к мануалам, которые, как вы думаете, мне помогут.

    • Дмитрий Артанов Дмитрий Артанов

      Для пополнения картины, нужно посмотреть статьи про ORM (Object-relational mapping). В MVC эта часть относится к модели.

Добавить комментарий



[ Ctrl + Enter ]