как выглядит структура mvc приложения
MVC для веб: проще некуда
В этой статье мы рассмотрим архитектурный паттерн MVC (Model, View, Controller) в применении к веб-разработке, «в чистом виде», без привлечения каких-то дополнительных, не относящихся к MVC структур и паттернов. Мы будем продвигаться от простого к сложному, поэтому пока не станем рассматривать, например, дальнейшее развитие MVC – паттерн HMVC (Hierarchical MVC). Хотя HMVC, несомненно, намного более интересен для разработки веб-приложений, но его применение не отменяет необходимости понимания «обычного» MVC.
Статья в Википедии (а именно туда, видимо, чаще всего попадают те, кто только начинает изучать MVC), изобилует неточностями и туманными формулировками, само определение, по сути, является неверным, а приведенная схема просто напросто не соответствует той, которая применяется в веб вообще и при разработке на PHP – в особенности.
Наиболее корректное определение паттерна MVC я обнаружил тут:
Шаблон проектирования MVC предполагает разделение данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: Модель, Представление и Контроллер – таким образом, что модификация каждого компонента может осуществляться независимо.
Уточним, что термин «компонент» в данном случае не имеет никакой связи с компонентами некоторых популярных CMS или фреймворков, а компоненты Битрикса, например вообще строятся из всех трёх составляющих MVC.
В приведённом определении под компонентом следует понимать некую отдельную часть кода, каждая из которых играет одну из ролей Контроллера, Модели или Представления, где Модель служит для извлечения и манипуляций данными приложения, Представление отвечает за видимое пользователю отображение этих данных (то есть, в применении к вебу, формирует отдаваемый сервером браузеру пользователя HTML/CSS), а Контроллер управляет всем этим оркестром.
Давайте рассмотрим классическую схему веб-приложения:
Рисунок 1
На этом и последующем рисунках пунктирными линиями показана управляющая информация (такая, например, как ID запрашиваемой записи блога или товара в магазине), а сплошными – собственно данные приложения (которые могут храниться в БД, или в виде файлов на диске, или даже, возможно, в оперативной памяти – этот вопрос лежит за пределами паттерна MVC). В применении к вебу запрос и ответ ходят по HTTP, поэтому можно условно считать, что на этом рисунке пунктиром обозначены заголовки HTTP-запроса и ответа, а сплошными линиями – их тела.
Получив Запрос 1, Контроллер его анализирует, и в зависимости от результатов обработки может выдать следующие варианты ответа (почему ответ имеет номер 4, станет понятно из следующих рисунков):
1. Сразу выдать ответ об ошибке (например, при запросе несуществующей страницы отдать только HTTP-заголовок «404 Not found»)
2. Если поступивший Запрос 1 признан корректным, то, в зависимости от того, является он запросом на просмотр или на модификацию данных, Контроллер вызывает соответствующий метод Модели, такой как Save или Load (Запрос 2 на Рис.2).
Рисунок 2
Важное замечание: концепция MVC не только не привязана к какому-то конкретному языку программирования, она также не привязана и к используемой парадигме программирования. То есть, вы вполне можете проектировать своё приложение по MVC, при этом не применяя ООП, и спроектировать Модель Товар для интернет-магазина таким образом:
Итак, в зависимости от полученного от Модели Ответа 2 Контроллер решает, какое из Представлений вызвать для формирования итогового ответа на изначальный Запрос 1:
3.1. В случае неудачи – Представление для сообщения об ошибке
3.2. В случае успеха – Представление для отображения запрашиваемых данных либо сообщения об их успешном сохранении (если Запрос 1 был на изменение данных).
Рисунок 3
Вопрос о том, кто должен проверять на валидность и права доступа входные данные (Контроллер или Модель), является предметом достаточно многочисленных споров, поскольку паттерн MVC не описывает таких деталей. Это значит, что в этом вопросе выбор за вами (или за вас его сделали авторы вашего любимого фрейvворка или CMS).
Мы в своей практике придерживаемся такого подхода:
Контроллер проверяет входные данные на предмет «общей» (т.е. независящей от конкретного запроса) корректности, соответствие требованиям Модели к валидности сохраняемых данных проверяет соответствующий метод Модели, а права доступа – метод Access отдельного класса User.
Для вызова Представления в PHP иногда проектируется специальный класс (а то и несколько классов), например, View (это часто встречается в описаниях MVC в реализации того или иного фреймворка), однако это не является требованием MVC.
Также файлы Представлений часто называют шаблонами, а при использовании так называемых шаблонизаторов роль Представления играет сам шаблонизатор, а шаблоны (т.е. файлы, содержащие непосредственно HTML-разметку) в некоторых фреймворках называют layouts.
Не вполне понятен предыдущий абзац? Забейте, поскольку у нас каждое Представление – это просто отдельный PHP-файл, а сам PHP устроен так, что в случае, если мы используем для выполнения Запроса 3 инструкцию include, Ответ 3 и Ответ 4 (помните, что это ответ на Запрос 1?) отдаются браузеру автоматически, средствами самого PHP.
Давайте рассмотрим пример.
У нас есть два варианта Представления (шаблоны), в которых
будет означать HTML-код, предворяющий в формируемом веб-документе основной контент (т.е. содержит тег doctype, контейнер head, код шапки страницы, и т.п.), а – примерно то же, только для подвала страницы.
Листинг 1. Шаблон product.tpl.php отображает данные о Товаре (которые к моменту его вызова уже содержит объект $product):
Листинг 2. Шаблон error.tpl.php отображает сообщение об ошибке (которое содержится в переменной $error):
Листинг 3. Контроллер product.php, служащий для отобоажения Товара, будет выглядеть примерно так:
Те, кто любит красивый и оптимизированный код могут заметить, что блоки HTML.header и HTML.footer дублируются в обоих шаблонах (они же Представления) error.tpl.php и product.tpl.php, и наверняка захотят вынести их в Контроллер product.save.php:
…. и нарушить таким образом основное правило MVC – разделяйте Контроллер, Модель и Представление.
Тем не менее, дублирующийся код – однозначное Зло. Что же делать?
Мы должны перейти от MVC к HMVC!
Но это – тема отдельной статьи.
Простые MVC-приложения
Теория
Если коротко, то MVC (model view controller) это такой способ написания программы, когда код отвечающий за вывод данных, пишется в одном месте, а код который эти данные формирует, пишется в другом месте. В итоге получается так, что если вам надо подправить вывод вы сразу знаете в каком месте искать. Сейчас все популярные фреймворки используют такую архитектуру.
Также стоит упомянуть тот факт, что существует два лагеря: один пишет логику в контроллерах, второй в моделях. В тех фреймворках, которые знаю я (yii, laravel) логику пишут в контроллерах, а модели являются просто экземплярами ORM. У yii кстати в мануале написано, что писать логику надо в моделях, а потом они сами в примерах пишут её в контроллерах, довольно забавно.
С бизнес-логикой определились, пишем в контроллерах. Также в методах контроллера происходит вызов моделей, которые у нас по сути экземпляры ORM, чтобы с их помощью получить данные из базы над которыми будут производить изменения. Конечный результат отправляется в виды. Виды cодержат HTML-разметку и небольшие вставки PHP-кода для обхода, форматирования и отображения данных.
Ещё можно упомянуть, что есть два вида MVC Page Controller и Front Controller. Page Controller’ом пользуются редко, его подход заключается в использовании нескольких точек входа (запросы к сайту осуществляются к нескольким файлам), и внутри каждой точки входа свой код отображения. Мы будем писать Front Controller с одной точкой входа.
Практика
Дальше в папке нашего проекта создаём папку, которую можно назвать App например. В ней будет следующее содержимое.
Наш Service Locator. Файл App.php
Сервис локатор нужен чтобы хранить в нём компоненты нашего приложения. Поскольку у нас простое mvc приложение, то мы не используем паттерн registry (как например в yii сделано). А просто сохраняем компоненты приложения в статические свойства, чтобы обращаться к ним было проще. Ещё App регистрирует автозагрузчик классов и обработчик исключений.
Роутер. Файл Router.php
Файл Db.php
Этот класс юзает файл конфига, который возврашает массив при подключении
Файл Config/Db.php
Наше ядро. Файл Kernel.php
Ядро обращается к роутеру, а потом запускает действия контроллера. Ещё ядро может кинуть исключение, если нет нужного контроллера или метода.
Файл Controller.php
Ещё нам нужно создать базовый класс для наших контроллеров, чтобы потом наследоваться от него. Наследовать методы нужно для того, чтобы вы могли рендерить (сформировать вывод) виды. Методы рендеринга поддерживают использование лэйаутов — шаблонов, которые содержат общие для всех видов компоненты, например футер и хэдер.
Файл index.php
Не забываем создать индексный файл в корне:
Создаём контроллеры и виды
Работа с нашим приложением (можно даже сказать минифреймворком) теперь сводится к созданию видов и контроллеров. Пример контроллера следующий (в папке Controllers):
Реализация MVC паттерна на примере создания сайта-визитки на PHP
Как вы уже догадались из названия статьи, сегодня речь пойдет о самом популярном, разве что после Singleton, шаблоне проектирования MVC, хотя такое сравнение не совсем уместно. Понимание концепции MVC может помочь вам в рефакторинге и разрешении неприятных ситуаций в которые, возможно попал ваш проект. Дабы восполнить пробел, мы реализуем шаблон MVC на примере простого сайта-визитки.
Оглавление
Введение
Многие начинают писать проект для работы с единственной задачей, не подразумевая, что это может вырасти в многопользовательскую систему управления, ну допустим, контентом или упаси бог, производством. И всё вроде здорово и классно, всё работает, пока не начинаешь понимать, что тот код, который написан — состоит целиком и полностью из костылей и хардкода. Код перемешанный с версткой, запросами и костылями, неподдающийся иногда даже прочтению. Возникает насущная проблема: при добавлении новых фич, приходится с этим кодом очень долго и долго возиться, вспоминая «а что же там такое написано то было?» и проклинать себя в прошлом.
Вы можеть быть даже слышали о шаблонах проектирования и даже листали эти прекрасные книги:
Представленная статья будет полезна в первую очередь новичкам. Во всяком случае, я надеюсь что за пару часов вы сможете получить представление о реализации MVC паттерна, который лежит в основе всех современных веб-фреймворков, а также получить «пищу» для дальнейших размышлений над тем — «как стоит делать». В конце статьи приводится подборка полезных ссылок, которые также помогут разобраться из чего состоят веб-фреймворки (помимо MVC) и как они работают.
Прожженные PHP-программисты вряд ли найдут в данной статье что-то новое для себя, но их замечания и комментарии к основному тексту были бы очень кстати! Т.к. без теории практика невозможна, а без практики теория бесполезна, то сначала будет чуть-чуть теории, а потом перейдем к практике. Если вы уже знакомы с концепцией MVC, можете пропустить раздел с теорией и сразу перейти к практике.
1. Теория
Шаблон MVC описывает простой способ построения структуры приложения, целью которого является отделение бизнес-логики от пользовательского интерфейса. В результате, приложение легче масштабируется, тестируется, сопровождается и конечно же реализуется.
Рассмотрим концептуальную схему шаблона MVC (на мой взгляд — это наиболее удачная схема из тех, что я видел):
В архитектуре MVC модель предоставляет данные и правила бизнес-логики, представление отвечает за пользовательский интерфейс, а контроллер обеспечивает взаимодействие между моделью и представлением.
Типичную последовательность работы MVC-приложения можно описать следующим образом:
Модель — содержит бизнес-логику приложения и включает методы выборки (это могут быть методы ORM), обработки (например, правила валидации) и предоставления конкретных данных, что зачастую делает ее очень толстой, что вполне нормально.
Модель не должна напрямую взаимодействовать с пользователем. Все переменные, относящиеся к запросу пользователя должны обрабатываться в контроллере.
Модель не должна генерировать HTML или другой код отображения, который может изменяться в зависимости от нужд пользователя. Такой код должен обрабатываться в видах.
Одна и та же модель, например: модель аутентификации пользователей может использоваться как в пользовательской, так и в административной части приложения. В таком случае можно вынести общий код в отдельный класс и наследоваться от него, определяя в наследниках специфичные для подприложений методы.
Вид — используется для задания внешнего отображения данных, полученных из контроллера и модели.
Виды cодержат HTML-разметку и небольшие вставки PHP-кода для обхода, форматирования и отображения данных.
Не должны напрямую обращаться к базе данных. Этим должны заниматься модели.
Не должны работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер.
Может напрямую обращаться к свойствам и методам контроллера или моделей, для получения готовых к выводу данных.
Виды обычно разделяют на общий шаблон, содержащий разметку, общую для всех страниц (например, шапку и подвал) и части шаблона, которые используют для отображения данных выводимых из модели или отображения форм ввода данных.
Контроллер — связующее звено, соединяющее модели, виды и другие компоненты в рабочее приложение. Контроллер отвечает за обработку запросов пользователя. Контроллер не должен содержать SQL-запросов. Их лучше держать в моделях. Контроллер не должен содержать HTML и другой разметки. Её стоит выносить в виды.
В хорошо спроектированном MVC-приложении контроллеры обычно очень тонкие и содержат только несколько десятков строк кода. Чего, не скажешь о Stupid Fat Controllers (SFC) в CMS Joomla. Логика контроллера довольно типична и большая ее часть выносится в базовые классы.
Модели, наоборот, очень толстые и содержат большую часть кода, связанную с обработкой данных, т.к. структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения.
1.1. Front Controller и Page Controller
В большинстве случае, взаимодействие пользователя с web-приложением проходит посредством переходов по ссылкам. Посмотрите сейчас на адресную строку браузера — по этой ссылке вы получили данный текст. По другим ссылкам, например, находящимся справа на этой странице, вы получите другое содержимое. Таким образом, ссылка представляет конкретную команду web-приложению.
Надеюсь, вы уже успели заметить, что у разных сайтов могут быть совершенные разные форматы построения адресной строки. Каждый формат может отображать архитектуру web-приложения. Хотя это и не всегда так, но в большинстве случаев это явный факт.
Рассмотрим два варианта адресной строки, по которым показывается какой-то текст и профиль пользователя.
Подход с множеством точек взаимодействия вы можете наблюдать на форумах с движком phpBB. Просмотр форума происходит через сценарий viewforum.php, просмотр топика через viewtopic.php и т.д. Второй подход, с доступом через один физический файл сценария, можно наблюдать в моей любимой CMS MODX, где все обращения проходят через index.php.
Эти два подхода совершенно различны. Первый — характерен для шаблона контроллер страниц (Page Controller), а второй подход реализуется паттерном контроллер запросов (Front Controller). Контроллер страниц хорошо применять для сайтов с достаточно простой логикой. В свою очередь, контроллер запросов объединяет все действия по обработке запросов в одном месте, что даёт ему дополнительные возможности, благодаря которым можно реализовать более трудные задачи, чем обычно решаются контроллером страниц. Я не буду вдаваться в подробности реализации контроллера страниц, а скажу лишь, что в практической части будет разработан именно контроллер запросов (некоторое подобие).
1.2. Маршрутизация URL
Маршрутизация URL позволяет настроить приложение на прием запросов с URL, которые не соответствуют реальным файлам приложения, а также использовать ЧПУ, которые семантически значимы для пользователей и предпочтительны для поисковой оптимизации.
К примеру, для обычной страницы, отображающей форму обратной связи, URL мог бы выглядеть так:
http://www.example.com/contacts.php?action=feedback
Приблизительный код обработки в таком случае:
Думаю, почти все так раньше делали.
С использованием движка маршрутизации URL вы сможете для отображения той же информации настроить приложение на прием таких запросов:
http://www.example.com/contacts/feedback
Здесь contacts представляет собой контроллер, а feedback — это метод контроллера contacts, отображающий форму обратной связи и т.д. Мы еще вернемся к этому вопросу в практической части.
Также стоит знать, что маршрутизаторы многих веб-фреймворков позволяют создавать произвольные маршруты URL (указать, что означает каждая часть URL) и правила их обработки.
Теперь мы обладаем достаточными теоретическими знаниями, чтобы перейти к практике.
2. Практика
Для начала создадим следующую структуру файлов и папок:
Забегая вперед, скажу, что в папке core будут храниться базовые классы Model, View и Controller.
Их потомки будут храниться в директориях controllers, models и views. Файл index.php это точка в хода в приложение. Файл bootstrap.php инициирует загрузку приложения, подключая все необходимые модули и пр.
Будем идти последовательно; откроем файл index.php и наполним его следующим кодом:
Тут вопросов возникнуть не должно.
Следом, сразу же перейдем к фалу bootstrap.php:
Первые три строки будут подключать пока что несуществующие файлы ядра. Последние строки подключают файл с классом маршрутизатора и запускают его на выполнение вызовом статического метода start.
2.1. Реализация маршрутизатора URL
Пока что отклонимся от реализации паттерна MVC и займемся мрашрутизацией. Первый шаг, который нам нужно сделать, записать следующий код в .htaccess:
Этот код перенаправит обработку всех страниц на index.php, что нам и нужно. Помните в первой части мы говорили о Front Controller?!
Маршрутизацию мы поместим в отдельный файл route.php в директорию core. В этом файле опишем класс Route, который будет запускать методы контроллеров, которые в свою очередь будут генерировать вид страниц.
Замечу, что в классе реализована очень упрощенная логика (несмотря на объемный код) и возможно даже имеет проблемы безопасности. Это было сделано намерено, т.к. написание полноценного класса маршрутизации заслуживает как минимум отдельной статьи. Рассмотрим основные моменты…
С помощью функции explode производится разделение адреса на составлющие. В результате мы получаем имя контроллера, для приведенного примера, это контроллер contacts и имя действия, в нашем случае — feedback.
Далее подключается файл модели (модель может отсутствовать) и файл контроллера, если таковые имеются и наконец, создается экземпляр контроллера и вызывается действие, опять же, если оно было описано в классе контроллера.
Таким образом, при переходе, к примеру, по адресу:
example.com/portfolio
или
example.com/portfolio/index
роутер выполнит следующие действия:
2.2. Возвращаемся к реализации MVC
Перейдем в папку core и добавим к файлу route.php еще три файла: model.php, view.php и controller.php
Напомню, что они будут содержать базовые классы, к написанию которых мы сейчас и приступим.
Содержимое файла model.php
Класс модели содержит единственный пустой метод выборки данных, который будет перекрываться в классах потомках. Когда мы будем создавать классы потомки все станет понятней.
Содержимое файла view.php
В нашем случае общий шаблон будет содержать header, menu, sidebar и footer, а контент страниц будет содержаться в отдельном виде. Опять же это сделано для упрощения.
Содержимое файла controller.php
Метод action_index — это действие, вызываемое по умолчанию, его мы перекроем при реализации классов потомков.
2.3. Реализация классов потомков Model и Controller, создание View’s
Теперь начинается самое интересное! Наш сайт-визитка будет состоять из следущих страниц:
На предыдущем рисунке отдельно выделен файл template_view.php — это шаблон, содержащий общую для всех страниц разметку. В простейшем случае он мог бы выглядеть так:
Для придания сайту презентабельного вида сверстаем CSS шаблон и интегририруем его в наш сайт путем изменения структуры HTML-разметки и подключения CSS и JavaScript файлов:
В конце статьи, в разделе «Результат», приводится ссылка на GitHub-репозиторий с проектом, в котором проделаны действия по интеграции простенького шаблона.
2.3.1. Создаем главную страницу
Начнем с контроллера controller_main.php, вот его код:
В метод generate экземпляра класса View передаются имена файлов общего шаблона и вида c контентом страницы.
Помимо индексного действия в контроллере конечно же могут содержаться и другие действия.
Файл с общим видом мы рассмотрели ранее. Рассмотрим файл контента main_view.php:
Здесь содержиться простая разметка без каких либо PHP-вызовов.
Для отображения главной странички можно воспользоваться одним из следующих адресов:
Пример с использованием вида, отображающего данные полученные из модели мы рассмотрим далее.
2.3.2. Создадаем страницу «Портфолио»
В нашем случае, страница «Портфолио» — это единственная страница использующая модель.
Модель обычно включает методы выборки данных, например:
Класс контроллера модели содержится в файле controller_portfolio.php, вот его код:
В переменную data записывается массив, возвращаемый методом get_data, который мы рассматривали ранее.
Далее эта переменная передается в качестве параметра метода generate, в который также передаются: имя файла с общим шаблон и имя файла, содержащего вид c контентом страницы.
Вид содержащий контент страницы находится в файле portfolio_view.php.
Здесь все просто, вид отображает данные полученные из модели.
2.3.3. Создаем остальные страницы
Остальные страницы создаются аналогично. Их код досутпен в репозитории на GitHub, ссылка на который приводится в конце статьи, в разделе «Результат».
3. Результат
А вот что получилось в итоге:
А вот в этой версии я набросал следующие классы (и соответствующие им виды):
4. Заключение
Шаблон MVC используется в качестве архитектурной основы во многих фреймворках и CMS, которые создавались для того, чтобы иметь возможность разрабатывать качественно более сложные решения за более короткий срок. Это стало возможным благодаря повышению уровня абстракции, поскольку есть предел сложности конструкций, которыми может оперировать человеческий мозг.
Но, использование веб-фреймворков, типа Yii или Kohana, состоящих из нескольких сотен файлов, при разработке простых веб-приложений (например, сайтов-визиткок) не всегда целесообразно. Теперь мы умеем создавать красивую MVC модель, чтобы не перемешивать Php, Html, CSS и JavaScript код в одном файле.
Данная статья является скорее отправной точкой для изучения CMF, чем примером чего-то истинно правильного, что можно взять за основу своего веб-приложения. Возможно она даже вдохновила Вас и вы уже подумываете написать свой микрофреймворк или CMS, основанные на MVC. Но, прежде чем изобретать очередной велосипед с «блекджеком и шлюхами», еще раз подумайте, может ваши усилия разумнее направить на развитие и в помощь сообществу уже существующего проекта?!
P.S.: Статья была переписана с учетом некоторых замечаний, оставленных в комментариях. Критика оказалась очень полезной. Судя по отклику: комментариям, обращениям в личку и количеству юзеров добавивших пост в избранное затея написать этот пост оказалось не такой уж плохой. К сожалению, не возможно учесть все пожелания и написать больше и подробнее по причине нехватки времени… но возможно это сделают те таинственные личности, кто минусовал первоначальный вариант. Удачи в проектах!
5. Подборка полезных ссылок по сабжу
В статье очень часто затрагивается тема веб-фреймворков — это очень обширная тема, потому что даже микрофреймворки состоят из многих компонентов хитро увязанных между собой и потребовалась бы не одна статья, чтобы рассказать об этих компонентах. Тем не менее, я решил привести здесь небольшую подборку ссылок (по которым я ходил при написаниие этой статьи), которые так или иначе касаются темы фреймворков.
5.1. MVC и другие паттерны
5.2. Шаблонизация
PHP сам по себе является шаблонизатором, но все же…