По всему интернет-миру разбросаны миллионы веб-приложений. Есть совсем простые, есть такие, что сам «архитектор матрицы ногу сломит». Но их объединяет одно — 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-системы по отдельным папкам и, таким образом, получим простую структуру каталогов, в которой легко найти то, что нам нужно. Кроме того, эти три папки поместим в каталог 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. Но уже на нём можно увидеть массу возможностей. К плюсам мы уже отнесли гибкость и масштабируемость. Дополнительными плюсами будут — возможности стандартизации кодирования, лёгкость обнаружения и исправления ошибок, быстрое вхождение в проект новых разработчиков. Кроме того, вы можете в своём приложении изменять способ хранения сущностей, используя для этого сторонние веб-сервисы и облачные базы данных. Из минусов можно привести только небольшое увеличение объёма скриптов. А так, сплошные плюсы. Так-что пользуетесь на здоровье.
Здесь лежат файлы проекта, качайте сравнивайте:
Ну как? Какие мысли? Комментируем, не стесняемся.
Лишней логики в отображениях, вроде бы, нет. И всё (что удивительно) работает. Но вот вопрос: как добавить в приложение, допустим, категорию «Враги»? Надо создать 6 новых классов?
Сразу возникает вопрос, зачем на сайте друзей, враги?
Ну а если всё-таки хочется их учитывать, то можно добавить к модели друга статус — «вышел из доверия».
Ну, вы ведь понимаете, что речь не о конкретной сущности. Можно было добавить и «Кошки». Или «Увлечения». Но по существу вы дали ответ, которого я и ожидал: добавить метку, ещё один атрибут к сущности. А теперь основной вопрос (предыдущий был предварительный :-):
Каким волшебным образом title в вашем View должен меняться с Друзья на Враги (Кошки, Собаки, Решения)? Или каждой новой сущности надо добавлять по два новых шаблона? Вы не планировали показать это на примере, как это могло бы выглядеть в коде? Я — планирую 🙂
В статье взят упрощённый пример, чтобы у начинающих глаза шибко не разбегались, а то потом не догонишь 🙂
Вы хотите чтобы потенциально сущностей было не ограниченное количество?
Для начала достаточно пяти (сущностей). По одному-два объекта в каждой для примера.
Пример «фреймворка» у вас получился идеальный, мне он очень помог кое-что понять. Он идеальный ещё и в плане доработки — на простом коде очень хорошо видно, где что можно усовершенствовать. Вот и возникает идея перейти «на следующую ступень», сделать пример пусть посложнее для понимания, но более идеальный в плане автоматики (отсутствия «плохих» зависимостей). Я практически закончил свою версию этой «второй ступени»…
Комментарии на моём сайте, конечно, не сахар, но зато нет капчи. И ни одного робота! Вывел всех простой генерацией одного поля на javascript.
Спасибо за урок.
Вопрос: Когда посетитель возвращается к списку (index.php), каждый раз снова создается новый объект класса FriendCnt ?
$controller = new FriendCnt();
Точно, и даже больше. Он создаётся и на просмотр информации об одном друге.
Это главный контроллер, через него всё идёт.
Здравствуйте, а как сделать чтобы когда я зохожу по адресу в http://mvc/ — у меня не показывает индекс, а показывает просто две папка, а когда уже заходишь в /www, то работает,
Заранее благодарю.
Если я правильно понял проблему, то она связана с веб сервером. Домен mvc привязался не к той директории.
Нужно уточнение — вы пользуетесь denwer?
Привет! А если все же усложнить пример:
— добавить вывод базового слоя(футеру и хедеру), сделать ваш пример, как бы блоком в общем шаблоне из других блоков;
— вместо генерируемого хэша «$this->key = md5($name . $yearOfBirth . $email);», попытаться сделать какой то ЧПУ по типу site.ru/dryzja/pasha-hrennikov/.
— доработать класс по работе с бд;
Вобщем, вот что мне хотелось бы сделать, вроде бы вижу полную картину как мне кажется, но как оно должно выглядеться на уровне ооп и парадигмы мвц, какие ещё паттерны туда добавить незнаю. Возможно вы сможете привести более детальный пример, и этим сильно помочь мне 🙄 Или же пнуть к мануалам, которые, как вы думаете, мне помогут.
Для пополнения картины, нужно посмотреть статьи про ORM (Object-relational mapping). В MVC эта часть относится к модели.