MVC (Model-View-Controller) в PHP
05-04-2019Reading time ~ 8 min.PHP 14311
Пока я не стал баловаться программированием на Javа, особо не задумывался над использованием MVC в PHP. Обычно действуешь по своему опыту или как это принято в php-фреймворке (или CMS, не важно). Тем более почти все фреймворки декларируют свою преданность концепции MVC: дескать вот у нас всё сделано правильно, по теории.
После Java, где объектное программирование возведено почти в абсолют, MVC в PHP выглядит уже достаточно «бледно», пытаясь хоть как-то соответствовать этой концепции. Интересно ещё и то, что php-разработчики Model-View-Controller видят по разному и не всегда верно.
Базовая концепция Model-View-Controller
Основа MVC была заложена аж в 1978 году для Smalltalk. Она безумно простая и учитывала реалии того времени. Если чуть утрировать, то Контролёр — это устройство ввода — клавиатура и её обработчики (например драйвера). Как только в Контролёре что-то произошло, например пользователь нажал клавишу, это событие/данные передаются в Модель. Именно в Модели решается что именно нужно сделать с введенной информацией. Например, если нажата Enter, то происходит запуск введенной команды.
После того, как действие выполнено, необходимо как-то его отобразить. За это уже отвечает Вид (Представление) и в простом варианте это обычный монитор, принтер или файл.
Смысл был в том, чтобы выделить из общей кучи кода модули ввода/вывода, оставив основную функциональность программы в виде Модели.
Со временем MVC приняла более абстрактный вид, но общая концепция не изменилась.
- Контролёр указывает модели что нужно сделать.
- Модель выполняет работу.
- Вид отображает/отдаёт результат.
В классических приложениях Model-View-Controller реализуется довольно просто, поскольку состоят из статичного интерфейса. На этапе разработки программы, заранее создается форма, где располагаются кнопки, метки, меню и прочие элементы, которые не будут менять своего расположения, форму или дизайн во время выполнения программы.
Поэтому взаимодействие с пользователем здесь довольно просто реализуется через Контролёр, который понимает, что нажата какая-то кнопка и нужно выполнить определенное действие. Но с PHP всё сложней.
Работа web-приложения
Сайт может взаимодействовать с пользователем разными способами. Например нажатие кнопки, по которому появится всплывающий popup. То есть onClick кнопки должен отслеживаться Контролёром, хотя программно это HTML-код, который вроде как уже Представление.
Однако во всех сайтах есть одна общая черта — все они взаимодействуют через URL-адреса. Любая ссылка — это указание на действие. Например вывести главную страницу, или записи рубрики, или отправить форму по ajax-запросу (там тоже URL). То есть во всех случаях пользователь передаёт своё действие в виде ссылки (http-запроса).
Другой вариант — это действия, где происходит скрытая передача данных через POST (Ajax). Адрес страницы при этом не меняется, хотя обработчик уже должен как-то понимать, что если это post-запрос, то нужно сделать одно, а если get, то просто вывести типовую страницу.
Таким образом, в web-приложении Контролёр должен иметь функции роутинга. Реализация роутинга разнится, но во всех случаях представляет собой в первую очередь разбор URL на части, например с помощью parse_url() и определения метода передачи данных ($_POST и т.п.).
Роутинг в Контролёре определяет дальнейшее действие, которое сильно завязано на структуру приложения. Например можно вызвать функцию, подключить файл или выполнить метод php-класса. См. для примера PHP-роутинг (Routing) для новичков.
В любом случае Конролёру нужно точно знать что дальше делать и какой функциональный модуль будет выполнять эту работу. Также возможен вариант, что Контролёр только передает входящие данные в Модель, которая уже сама принимает решение какой модуль будет выполнять задачу. По сути это одно и тоже, разница лишь в том, где именно будет размещён это «action list».
После того, как управление переходит к Модели, происходит основная работа, то есть то, ради чего посетитель вообще зашёл на сайт. Предположим, что это примитивный вывод даты и времени. В Модели можно сразу же сформировать необходимый html-код для вывода, но это уже будет нарушать концепцию MVC, поскольку Модель должна отдавать только данные, а сама визуализация должна выполняться в Виде.
Вид/Представление по своей сути — это результирующий html-код, который помимо всего прочего, верстается под необходимый дизайн. Скажем, если нужно вывести дату другим цветом, всё что потребуется сделать, так это поменять css-стили в шаблоне. При этом ни Контролёр, ни Модель не меняются.
Проблемы View
Здесь мы сталкиваемся с проблемой: как именно можно передать данные в html-код? Ведь HTML статичен — это не язык программирования, а всего лишь вид разметки документа.
Даже в самой простой реализации, html-шаблон нужно будет сделать в виде php-файла, где придётся смешивать html и php-код между собой. В простых случаях это не проблема, но когда данных для вывода много, то такая смесь уже представляется серьёзной проблемой (низкая читабельность и сложная поддержка).
К сожалению решения этой проблемы не существует, хотя есть методы, которые позволяют упростить написание такого кода (об этом ниже).
Неверное понимание MVC
Очень часто встречаются искаженные толкования Model-View-Controller. Вопрос здесь скорее в практической реализации: PHP очень гибкий язык и позволяет использовать любой подход. Не думаю, что кто-то бросится переписывать свой проект под строгий MVC, но понимание этой концепции может пригодится при создании нового.
Часто под Моделью понимают только работу с базой данных. То есть контролёр вызывает некую функцию/метод Модели, которая возвращает сырые данные. Здесь проблема в том, что кто-то должен эти данные обработать. То есть Модель в этом случае не выполнила всю работу и данные должны быть обработаны либо уже в Представлении, либо в том же Контролёре. В обоих случаях это уже проблема.
Работа с БД — только часть работы Модели. Ни Контролёр, ни Вид не должны ничего знать ни про базу, ни про то, как получаются данные. Для них Модель — это черный ящик, куда можно передать входящие данные и получить готовые.
Другим искажением MVC будет считать Контролёр ответственным за всю логику и работу приложения. С одной стороны, он действительно отвечает за маршрутизацию, но в нём не должно быть логики выполнения. По хорошему, Контролёр даже не должен ничего знать и о Представлении. Когда Контролёру отводится другая роль, то это уже другая концепция: Model-View-ViewModel, Model-View-Presenter и т.п.
Модульность PHP
На практике принято разделять php-код на отдельные файлы. Именно такой подход позволяет избежать загромождения кода и разделить ответственность за каждое действие. Устоявшейся практикой являются не просто отдельные файлы, но и их группировка по отдельным каталогам.
Если проект основан на ООП, то обычно стоит задача организации многочисленных классов. Например в CodeIgniter 2/3 версий каждый класс представлял собой отдельный файл, но все файлы всех классов находятся в одном общем каталоге. Это накладывает некоторые ограничения, поскольку требует размещения всего кода относящегося к классу в один файл.
В CodeIgniter 4 уже используется модульный подход, где классы размещаются в отдельных каталогах и могут состоять из нескольких файлов.
MVC — лишь концепция, поэтому практическая реализация может быть разной. Обычно используют ООП, поскольку это решает проблему пространства имён в PHP, а модульность упрощает поддержку классов. Хотя использование классов и ООП не означает следованию MVC.
View не так прост
Ошибочно принято считать, что html-шаблон — это единственное, что отводится Виду. На самом деле это порождает проблему смешивания html и php-кода, о которой я написал выше.
В простом варианте это может выглядеть так:
model.php $title = 'Титул'; $descr = 'Описание'; $link = 'ссылка'; $link_url = 'адрес'; view.php <h1><?= $title ?></h1> <h4><?= $descr ?></h4> <div class="my-link"><a href="<?= $link_url ?>"><?= $link ?></a></div>
На помощь может прийти php-шаблонизатор, который позволит использовать более простой синтаксис, что улучшает читабельность:
view.php <h1>{{ $title }}</h1> <h4>{{ $descr }}</h4> <div class="my-link"><a href="{{ $link_url }}">{{ $link }}</a></div>
Если усложнить задачу, например добавить какое-то условие вывода, то код опять усложнится.
Другой вариант решения — это переместить html-код в сам php-шаблон. То есть это уже будет php-код, который выводит html. Понятно, что здесь уже можно произвольно усложнять логику вывода данных.
view.php echo '<h1>' . $title . '</h1>'; echo '<h4>' . $descr . '</h4>'; echo '<div class="my-link"><a href="' . $link_url .'">' . $link . '</a></div>';
Такой подход наиболее часто встречается в CMS-шаблонах. Проблема здесь в том, что вместо html-верстки мы вынуждены заниматься php-программированием.
Также View обычно разбивается на несколько частей/файлов. Например секция HEAD, после компоненты шапки, вывод основного контента, сайдбар и подвал. Здесь главное разделить блоки по функциональному признаку и так, чтобы между ними по возможности не было зависимостей.
Расскажу про ещё один вариант, который я придумал для своей MaxSite CMS. Используется специальный php-класс для форматированного вывода. Вначале в него загружаются данные записи, после этого задается формат вывода, где можно указать дополнительные параметры, а сам вывод осуществляется в виде метода, который делает замены «псевдокода» на основе реальных данных и заданного формата.
// формат $p->format('title', '<h1 class="t-gray800">', '</h1>', false); $p->format('cat', ', ', '<div class="t90">', '</div>'); $p->format('date', 'j F Y г.', '<time datetime="[page_date_publish_iso]">', '</time>'); // вывод $p->html('<header class="mar20-tb">'); $p->line('[title]'); $p->line('[cat][date]'); $p->html('</header>');
Таким образом, View не просто вывод шаблона — это обработчик шаблона, который подключает разные модули/блоки/файлы, использует шаблонизатор, а также может содержать сложную логику вывода данных.
Архитектура приложения
Можно ли реализовать строгую MVC в PHP? Если вопрос стоит принципиально, то PHP без проблем реализуется какой-то один из шаблонов проектирования, реализующий MVC: Наблюдатель, Стратегия и Компоновщик и т.д. Всё это взаимодействие между классами в рамках ООП, с которым у PHP нет проблем.
Но с моей точки зрения, более важным будет общая архитектура приложения и практическая реализация отдельных модулей. Например в том же CodeIgniter 2/3 конфигурация представляет собой обычный массив, а в 4-й версии — это уже PHP-класс, который работает с таким же массивом, но уже внутри себя. То есть это более строгое следование теории, но с практической точки зрения — бессмысленное раздувание кода. Или, скажем, во многих случаях удобней и проще воспользоваться обычной require_once()
вместо создания сложного загрузчика php-классов.
Поэтому, перед созданием веб-приложения, важно продумать такие нюансы и соблюсти баланс удобства использования и возможность дальнейшего развития своего проекта.
Когда-то давно изучал Java, еще во времена NetBeanse 6.7. Перешел на C#, только из-за того, что в инфраструктуре Windows легче строить обработки, если вычислительная инфраструктура организации также построена на ПО производства Microsoft.
Соглашусь с Вами, что Java по ощущению дает такую степень свободы, которую я не ощущал на C#. В то далекое время, тоже использовал модель MVC, но для себя её интерпретирую чуток по другому:
Модель (Model) - отвечает за сохранность информации приложения (запрошена ли информация из базы данных, введена пользователем или вычислена);
Вид (View) - работает только как интерфейс с пользователем;
Контроллер (Controller) - отвечает за все выполняемые действия программы, запрос информации из базы данных, обработку ввода пользователя или производит вычисления, т.е. строилась модель в следующем порядке: