MVC и его реализация на PHP
MVC - это архитектурная модель программного обеспечения, то есть схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер. Разделение производится таким образом, что модификация каждого компонента может осуществляться независимо.
Если вы хотите ознакомиться с материалом в видео формате, вы можете найти запись в лектории или просто перейти по ссылке. Также вы сможете получить методичку и перезентацию на данную тему под видео - лекцией.
Концепция MVC впервые была описана аж в 1978 году, но ее окончательная версия была опубликована только через 10 лет, в 1988. Важно понимать, что MVC применяется исключительно в объектно ориентированных языках программирования и основной целью данной концепции является разделение бизнес логики от визуального представления данных.
Бизнес логика в самом первом приближении - это некоторые формулы, расчеты, ветвления в программе и тому подобное. Всю эту бизнес логику было принято определить в так называемые модели (Model). Весь контент, который должен быть виден пользователю определили в представления (Views). Прием данных от пользователя, связь модели и контроллера и отправку результата обратно пользователю взял на себя ответственность контроллер (Controller). По этим трем важным компонентам данной концепции и возникла аббревиатура MVC - Model View Controller (Модель Представление Контроллер).
Алгоритм работы MVC на примере новостного портала
- Пользователь отправляет запрос на получение всех новостей
- Контроллер принимает запрос и при помощи модели получает весь список новостей и направляет их в представление
- Представление отображает новости в определенном виде
Ближе к делу. Давайте рассмотрим пример простейшей реализации данной концепции на языке PHP. В рамках данной лекции будет предложен к рассмотрению микро фреймворк основанный на концепции MVC. На данном микро фреймворке будет реализован простейший новостной портал. Сразу хотелось бы заметить, что реализация MVC будет предложена к показу в облегченном варианте с целью снижения порога вхождения.
Структура проекта в файловой системе выглядит следующим образом:
Существует 4 директории:
- Controllers - директория в которой хранится весь список контроллеров. В рамках данного проекта будет использоваться единственный контроллер “home”. Принято в рамках хорошего тона в качестве суффикса в наименовании контроллера указывать текст “Controller”
- Models - директория для хранения моделей. Пока здесь хранится всего одна модель “News”, которая содержит всю бизнес логику для работы с новостями приложения.
- Views - содержит список всех возможных представлений. Представления в данном контексте - это обычные файлы формата “php”, в которых содержится HTML код с PHP вставками. В рамках проекта имеется всего два представления: “index.php” - главная страница портала и “news.php” - новости портала
- System - директория в которой хранятся все системный классы и файлы, а именно:
- App - главный класс приложения, реализующий в себе функционал простейшего роутинга
- View - класс отвечающий за рендер представлений
- autoload - файл в котором реализована автозагрузка классов по стандарту PSR-0
В проекте также существует файл “index.php” выполняющий роль единой точки входа в приложение. Точкой входа в приложение называют файл с которого начинается выполнение программы. В первую очередь нам необходимо настроить веб сервер таким образом, чтобы он перенаправлял все запросы в единую точку входа - index.php.
Конфигурация виртуального хоста для веб-сервера nginx должна выглядеть следующим образом:
location / {
# Redirect everything that isn't a real file to index.php
try_files $uri $uri/ /index.php$is_args$args;
}
Директива try_files проверяет существование файлов в заданном порядке и использует для обработки запроса первый найденный файл. С помощью слэша в конце имени можно проверить существование каталога. В случае, если ни один файл не найден, то делается внутреннее перенаправление на uri, заданный последним параметром. То есть сначала производится попытка найти запрошенный файл, если файл не был найден, то проверяется существование директории. Но если запрошенная директория не существует, то выборка выпадает на последнюю запись, т.е. на нашу единую точку входа “index.php”
В Apache для того, чтобы добиться перенаправления всех запросов в единую точку входа, используется модуль “mod_rewrite”. В случае веб-сервера Apache можно не править напрямую виртуальный хост относящийся к проекту, а воспользоваться файлом “.htaccess”, который будет выглядеть приблизительно следующим образом
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php
При помощи директивы “RewriteEngine” и ее опции “On” производится включение модуля перезаписи URL. Используя директиву “RewriteCond” мы задаем некоторые условия, которые проверяют на истинность URL, и если условия истинны, то применяется директива перезаписи “RewriteRule”. В данном случае перезаписи будут подвергаться все URL, которые не ведут на реальные файлы или каталоги. Перезапись осуществляется на единую точку входа “index.php”.
Теперь можно приступить к разбору единой точки входа в приложение
1 <?php
2 // Включаем режим строгой типизации
3 declare(strict_types=1);
4 // Подключаем файл реализующий автозагрузку
5 require_once __DIR__ . '/System/autoload.php';
6 // Запускаем приложение
7 System\App::run();
В третьей строчке мы используем конструкцию “declare” которая устанавливает поддержку строгой типизации в рамках всего проекта. В шестой строчке подключается файл, в котором описана логика автозагрузки по стандарту PSR-0, выглядит это следующим образом
1 <?php
2 /**
3 * Example Implementation of PSR-0
4 *
5 * @param $className
6 */
7 function autoload($className)
8 {
9 $className = ltrim($className, '\\');
10 $fileName = '';
11 $namespace = '';
12 if ($lastNsPos = strrpos($className, '\\')) {
13 $namespace = substr($className, 0, $lastNsPos);
14 $className = substr($className, $lastNsPos + 1);
15 $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) .
16 DIRECTORY_SEPARATOR;
17 }
18 $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
19 require $fileName;
20 }
21 spl_autoload_register('autoload');
Подробнее о автозагрузке по стандарту PSR-0 читайте статью, представленную в дополнительных материалах к конспекту.
На 9 строке точке входа index.php производится непосредственно запуск самого приложения. Давайте посмотрим, что делает статический метод “run” класса приложения “App”
1 <?php
2
3 namespace System;
4
5 /**
6 * Главный класс приложения
7 *
8 * @author farza
9 * @return void
10 */
11 class App
12 {
13 /**
14 * Запуск приложения
15 *
16 * @author farZa
17 * @throws \ErrorException
18 */
19 public static function run()
20 {
21 // Получаем URL запроса
22 $path = $_SERVER['REQUEST_URI'];
23 // Разбиваем URL на части
24 $pathParts = explode('/', $path);
25 // Получаем имя контроллера
26 $controller = $pathParts[1];
27 // Получаем имя действия
28 $action = $pathParts[2];
29 // Формируем пространство имен для контроллера
30 $controller = 'Controllers\\' . $controller . 'Controller';
31 // Формируем наименование действия
32 $action = 'action' . ucfirst($action);
33
34 // Если класса не существует, выбрасывем исключение
35 if (!class_exists($controller)) {
36 throw new \ErrorException('Controller does not exist');
37 }
38
39 // Создаем экземпляр класса контроллера
40 $objController = new $controller;
41
42 // Если действия у контроллера не существует, выбрасываем исключение
43 if (!method_exists($objController, $action)) {
44 throw new \ErrorException('action does not exist');
45 }
46
47 // Вызываем действие контроллера
48 $objController->$action();
49 }
50 }
Вся реализация маршрутизации приложения реализована в методе run.
Маршрутизатор приложения - это компонент который распознает входящие запросы. Маршрутизатор анализирует HTTP методы и URL полученного запроса, и
сопоставляет их с подходящим методом контроллера для передачи ему управления. Если компонент не сможет найти запрошенное пользователем действие контроллера,
приложение выдаст ошибку.
В нашем микро фреймворке реализован простейший маршрутизатор, который компактно умещается в методе run. Давайте разберемся как он работает на простом примере. Предположим пользователь запрашивает адрес вида “/home/main”. По этому адресу он предполагает увидеть главную страницу портала
http://example.com/home/main
Наш мини - компонент роутинга способен разобрать данный url. Он настроен таким образом, что в качестве контроллера (класс) будет понимать первую часть url запроса, т.е. “home”, а вторую часть url запроса, т.е. “main” будет понимать как действие (метод) контроллера.
Еще раз, маршрутизатор данного приложения настроен так, что в качестве контроллера он понимает первую часть url, а в качестве действия контроллера понимает вторую часть этого же url.
Таким образом будет создан экземпляр класса контроллера “home” и будет вызвано его действие “main”.
Теперь давайте разберем код маршрутизатора, приведенный выше.
- На 22 строке мы получаем url запроса, то есть “/home/main”
- На 24 строке разбиваем url на части
- С 26-28 строках создаем переменные и помещаем туда наименование контроллера и наименование действия
- В рамках нашего приложения мы для себя приняли некоторую спецификацию повествующую о том, что все контроллеры должны находиться в пространстве имен “Control ers”, поэтому на 30 строке мы конкатенируем префикс к названию контроллера, создавая тем самым наименование класса контроллера. Правилом хорошего тона является добавление суффикса к наименованию класса контроллера. Чтобы удовлетворить данное правило, в 30 строке производится конкатенация суффикса к наименованию контроллера.
- Действием контроллера называют метод класса контроллера. Как правило, наименования действий начинается с ключевого слова “action”. Конечно же можно не соблюдать данное правило, но в данном конкретном примере, на 32 строке кода, конкатенация префикса “action” с наименованием действия была произведена.
- На 35-37 строках кода производится проверка существования класса контроллера. Если класс не был обнаружен, то выбрасывается исключение
- На 40 строке создается экземпляр класса контроллера, а затем на 43-45 строках производится проверка наличия запрашиваемого действия у созданного экземпляра класса контроллера. В случае отсутствия действия, выбрасывается исключение.
- На последней 48 строке производится вызов действия контроллера.
Вкратце подытожим. Пользователь отправляет запрос, который направляется веб-сервером в единую точку входа index.php. Там подключаются нужные зависимости и производится запуск приложения. В методе запуска встроен маршрутизатор, который разбирает запрос пользователя и понимает какой контроллер нужно создать и какое действие у него вызвать.
Предположим что пользователь отправил запрос вида “/home/main”. Маршрутизатор сразу понимает, что необходимо создать экземпляр класса “homeController”, находящийся в пространстве имен “Controllers” и вызвать у него действие “actionMain”. В данном примере такой контроллер уже был создан и действие “main” тоже уже существует
1 <?php
2
3 namespace Controllers;
4
5 use System\View;
6 use Models\News;
7
8 /**
9 * Главный контроллер приложения
10 *
11 * @author farza
12 */
13 class homeController
14 {
15 /**
16 * Действие отвечающее за отображение главной
17 * страницы портала
18 *
19 * @author farZa
20 */
21 public function actionMain()
22 {
23 // Рендер главной страницы портала
24 View::render('index');
25 }
26 }
Единственное, что делает действие “main” контроллера “home” - отображает представление. В данном проекте было принято, что все представления будут храниться в директории “Views” и представлять из себя обыкновенные файлы формата “.php”, в которых будет присутствовать HTML разметка с возможными вставками PHP кода. Никаких шаблонизаторов в рамках системы не предусмотрено.
В двадцать четвертой строке мы говорим, что необходимо отобразить представление с наименованием “index”. За отображение представлений отвечает метод “render” класса “View”. Давайте рассмотрим его логическую структуру
1 <?php
2
3 namespace System;
4
5 /**
6 * Главный класс реализующий функционал отображения
7 * представлений
8 *
9 * @author farza
10 */
11 class View
12 {
13 /**
14 * @author farZa
15 * @param string $path
16 * @param array $data
17 * @throws \ErrorException
18 */
19 public static function render(string $path, array $data = [])
20 {
21 // Получаем путь, где лежат все представления
22 $fullPath = __DIR__ . '/../Views/' . $path . '.php';
23
24 // Если представление не было найдено, выбрасываем исключение
25 if (!file_exists($fullPath)) {
26 throw new \ErrorException('view cannot be found');
27 }
28
29 // Если данные были переданы, то из элементов массива
30 // создаются переменные, которые будут доступны в представлении
31 if (!empty($data)) {
32 foreach ($data as $key => $value) {
33 $$key = $value;
34 }
35 }
36
37 // Отображаем представление
38 include($fullPath);
39 }
40 }
Метод “render” принимает 2 параметра:
- path - наименование представления
- data - необязательный параметр, представляющий из себя массив данных, элементы которого станут доступны в отображаемом представлении
Разбор
- В 22 строчке кода сохраняется путь до представления, а затем в 25-27 строчках производится проверка наличия представления в директории “Views”. В случае отсутствия представления, выбрасывается исключение.
- Если в представление данные были переданы, то из элементов массива создаются переменные, которые станут доступны в подключаемом представлении, вся эта логика описана в 31-35 строчках
- В 38 строке производится непосредственно подключение файла представления.
В рамках проекта представление уже было создано и находится в директории “Views”:
1 <h2>Главная страница</h2>
2
3 <div class="main-page">
4 <p>Добро пожаловать на главную страницу нашего первого
5 <strong>MVC</strong> фреймворка</p>
6
7 <a href="/home/news">Новости портала</a>
8 </div>
Данное представление содержит примитивную HTML разметку без каких либо вставок PHP кода.
Если мы перейдем по адресу “/home/main”, то увидим главную страницу приложения
Единственное, что осталось за бортом данной лекции, и что просто необходимо рассмотреть в рамках обсуждаемой концепции MVC и нашего микро-фреймворка - это “Модель”.
Давайте создадим страницу новостей. В качестве задачи поставим следующее условие: Необходимо получить список новостей из базы данных и отобразить их на странице.
В условии был упомянут факт о работе с базой данных, а значит будет некоторая бизнес логика, отвечающая за данную работу, а, если быть точнее, за получение списка новостей из базы данных. Эту бизнес логику в рамках концепции MVC мы обязаны положить в модель. В рамках данного проекта уже создана таблица “news” в базе данных и модель “News” расположенная в пространстве имен “Models”.
1 <?php
2
3 namespace Models;
4
5 /**
6 * Модель "Новости" содержащая бизнес логику
7 * относящуюся к сущности "Новости"
8 *
9 * @author farza
10 */
11 class News
12 {
13 /**
14 * Метод, отвечающий за получение всех данных
15 * о новостях портала
16 *
17 * @author farZa
18 * @return array
19 */
20 public function displayAll()
21 {
22 // Строка соединения с базой данных
23 $dsn = 'mysql:host=127.0.0.1;dbname=db_name;';
24 // Создаем экземпляр класса для работы с БД
25 $pdo = new \PDO($dsn, 'admin', 'admin');
26
27 // SQL запрос на получение всех новостей
28 $sql = 'SELECT * FROM news';
29
30 // Возвращаем полученные из БД данные
31 return $pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
32 }
33 }
Модель “News” содержит метод “displayAll”, который инкапсулирует бизнес логику получения всех новостей в одном месте
- В 23 строке мы готовим так называемый “connection string"
- В 25 строке создаем экземпляр класса PDO для работы с базой данныx
- В 28 строке готовим SQL запрос на получение всех новостей
- В 31 строке возвращаем список всех полученных новостей в виде ассоциативного массива
Итак, бизнес логика готова, осталось лишь создать действие контроллера, которое воспользуется методом модели с целью получения всех новостей и направит их в представление, которое отобразит новости в подобающем виде
1 <?php
2
3 namespace Controllers;
4
5 use System\View;
6 use Models\News;
7
8 /**
9 * Главный контроллер приложения
10 *
11 * @author farza
12 */
13 class homeController
14 {
15 /**
16 * Действие отвечающее за отображение главной
17 * страницы портала
18 *
19 * @author farZa
20 */
21 public function actionMain()
22 {
23 // Рендер главной страницы портала
24 View::render('index');
25 }
26
27 /**
28 * Действие отвечающее за отображение всех
29 * новостей
30 *
31 * @author farZa
32 */
33 public function actionNews()
34 {
35 // Создаем модель
36 $model = new News();
37
38 // Получаем данные используя модель
39 $data = $model->displayAll();
40
41 // Передаем данные представлению для их отображения
42 View::render('news', [
43 'data' => $data,
44 ]);
45 }
46 }
- В 33 строке было создано действие с наименованием “news"
- В 36 строке создается экземпляр класса модели
- В 39 строке производится получение всех новостей
- В 42-44 строках данные о новостях передаются в представление “news"
Представление “news” выглядит следующим образом
1 <?php
2 /**
3 * @var array $data - массив новостей
4 */
5 ?>
6
7 <h2>Новости</h2>
8
9 <div class="news-block">
10 <?php foreach ($data as $item): ?>
11 <div class="news-item" style="border: 1px solid black">
12 <div class="title">
13 <span><?= $item['title']; ?></span>
14 </div>
15
16 <div class="description">
17 <span><?= $item['description']; ?></span>
18 </div>
19 </div>
20
21 <br/>
22 <?php endforeach; ?>
23 </div>
Если перейти на страницу “home/news” то можно наблюдать следующий результат
Распространенные ошибки
Так как MVC не имеет строгой реализации, то реализован он может быть по-разному. Нет общепринятого определения, где должна располагаться бизнес-логика. Она может находиться как в контроллере, так и в модели. От этого сообщество разделилось на два лагеря. Первый поддерживает принцип “Fat control er & Slim model”, а второй “Slim control er & Fat model”. Верно будет сказать, что эта тема является самой горячей дискуссией, хотя по определению контроллер должен лишь принимать и анализировать запросы, а также делать выбор следующего действия системы, (например, передача запроса другим элементам системы).
Также важно сказать, что от недостатка опыта начинающие программисты очень часто трактуют архитектурную модель MVC как пассивную модель. Пассивная модель выполняет функцию для доступа и работы с данными, а контроллер содержит всю бизнес логику. В результате - код моделей по факту является средством получения данных из СУБД, а контроллер - обыкновенный класс, наполненным бизнес-логикой. В рамках концепции MVC это является самой распространенной ошибкой, которую
следует избегать. В объектно-ориентированном программировании используется активная модель MVC, где модель - это не только совокупность кода доступа к данным и СУБД, но и вся бизнес-логика.
Применение
Применение данной концепции нашли множество фреймворков реализованных при помощи языка PHP. Такие популярные фреймворки как Zend Framework, Symfony, Laravel, Yi и множество других полностью построены на данной моделе.
Дополнительные задания
Реализованный проект в рамках даннойго конспекта естественно нуждается в улучшении. В качестве факультативного задания можно реализовать дополнительные возможности,
а также произвести небольшой рефакторинг
- В данный момент все представления хранятся в директории “Views”. Было бы неплохо, если бы в данной директории находились поддиректории, наименование которых бы соответствовало наименованию контроллеров, а в этих директориях хранились представления относящиеся исключительно к указанному контроллеру. Например есть контроллер “home”, а в директории “Views” должна быть под директория “home”, в которой бы хранились все представления относящиеся к вышеупомянутому контроллеру. В действиях контроллера при вызове метода “render”, система должна понимать в какой директории находится подключаемое представление.
- Целесообразно рассмотреть вариант создания класса, который бы содержал логику относящуюся к маршрутизации
- В данный момент представления не содержат основную часть HTML разметки, такую как “DOCTYPE”, “<html>”, “<body>” и т.д., но в каждом представлении повторять обязательный каркас HTML страницы не является целесообразным. Было бы очень здорово, если бы был отдельный файл, называемый шаблоном (layout), который подключал в изменяемой части шаблона само представление
- В моделях используется логика для работы с базой данных на довольно низком уровне, средствами PDO. Напишите класс, который бы инкапсулировал некоторую логическую часть работы с базой данных и предоставлял интерфейс для простого использования и манипулрования данными. Под интерфейсом можно понимать публичные методы типа “findAl ()”, поиск всех записей, “findOne” - поиск одной записи