Возвращаемся к реализации MVC

16.11.2023 Windows XP

Многие начинают писать проект для работы с единственной задачей, не подразумевая, что это может вырасти в многопользовательскую систему управления, ну допустим, контентом или упаси бог, производством. И всё вроде здорово и классно, всё работает, пока не начинаешь понимать, что тот код, который написан - состоит целиком и полностью из костылей и хардкода. Код перемешанный с версткой, запросами и костылями, неподдающийся иногда даже прочтению. Возникает насущная проблема: при добавлении новых фич, приходится с этим кодом очень долго и долго возиться, вспоминая «а что же там такое написано то было?» и проклинать себя в прошлом.

Вы можеть быть даже слышали о шаблонах проектирования и даже листали эти прекрасные книги:

  • Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидесс «Приемы объектно ориентированного проектирования. Паттерны проектирования»;
  • М. Фаулер «Архитектура корпоративных программных приложений».
А многие, не испугавшись огромных руководств и документаций, пытались изучить какой-либо из современных фреймворков и столкнувшись со сложностью понимания (в силу наличия множества архитектруных концепций хитро увязанных между собой) отложили изучение и применение современных интсрументов в «долгий ящик».

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

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

1. Теория Шаблон MVC описывает простой способ построения структуры приложения, целью которого является отделение бизнес-логики от пользовательского интерфейса. В результате, приложение легче масштабируется, тестируется, сопровождается и конечно же реализуется.

Рассмотрим концептуальную схему шаблона MVC (на мой взгляд - это наиболее удачная схема из тех, что я видел):

В архитектуре MVC модель предоставляет данные и правила бизнес-логики, представление отвечает за пользовательский интерфейс, а контроллер обеспечивает взаимодействие между моделью и представлением.

Типичную последовательность работы MVC-приложения можно описать следующим образом:

  • При заходе пользователя на веб-ресурс, скрипт инициализации создает экземпляр приложения и запускает его на выполнение.
    При этом отображается вид, скажем главной страницы сайта.
  • Приложение получает запрос от пользователя и определяет запрошенные контроллер и действие. В случае главной страницы, выполняется действие по умолчанию (index ).
  • Приложение создает экземпляр контроллера и запускает метод действия,
    в котором, к примеру, содержаться вызовы модели, считывающие информацию из базы данных.
  • После этого, действие формирует представление с данными, полученными из модели и выводит результат пользователю.
  • Модель - содержит бизнес-логику приложения и включает методы выборки (это могут быть методы ORM), обработки (например, правила валидации) и предоставления конкретных данных, что зачастую делает ее очень толстой, что вполне нормально.
    Модель не должна напрямую взаимодействовать с пользователем. Все переменные, относящиеся к запросу пользователя должны обрабатываться в контроллере.
    Модель не должна генерировать HTML или другой код отображения, который может изменяться в зависимости от нужд пользователя. Такой код должен обрабатываться в видах.
    Одна и та же модель, например: модель аутентификации пользователей может использоваться как в пользовательской, так и в административной части приложения. В таком случае можно вынести общий код в отдельный класс и наследоваться от него, определяя в наследниках специфичные для подприложений методы.

    Вид - используется для задания внешнего отображения данных, полученных из контроллера и модели.
    Виды cодержат HTML-разметку и небольшие вставки PHP-кода для обхода, форматирования и отображения данных.
    Не должны напрямую обращаться к базе данных. Этим должны заниматься модели.
    Не должны работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер.
    Может напрямую обращаться к свойствам и методам контроллера или моделей, для получения готовых к выводу данных.
    Виды обычно разделяют на общий шаблон, содержащий разметку, общую для всех страниц (например, шапку и подвал) и части шаблона, которые используют для отображения данных выводимых из модели или отображения форм ввода данных.

    Контроллер - связующее звено, соединяющее модели, виды и другие компоненты в рабочее приложение. Контроллер отвечает за обработку запросов пользователя. Контроллер не должен содержать SQL-запросов. Их лучше держать в моделях. Контроллер не должен содержать HTML и другой разметки. Её стоит выносить в виды.
    В хорошо спроектированном MVC-приложении контроллеры обычно очень тонкие и содержат только несколько десятков строк кода. Чего, не скажешь о Stupid Fat Controllers (SFC) в CMS Joomla. Логика контроллера довольно типична и большая ее часть выносится в базовые классы.
    Модели, наоборот, очень толстые и содержат большую часть кода, связанную с обработкой данных, т.к. структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения.

    1.1. Front Controller и Page Controller В большинстве случае, взаимодействие пользователя с web-приложением проходит посредством переходов по ссылкам. Посмотрите сейчас на адресную строку браузера - по этой ссылке вы получили данный текст. По другим ссылкам, например, находящимся справа на этой странице, вы получите другое содержимое. Таким образом, ссылка представляет конкретную команду web-приложению.

    Надеюсь, вы уже успели заметить, что у разных сайтов могут быть совершенные разные форматы построения адресной строки. Каждый формат может отображать архитектуру web-приложения. Хотя это и не всегда так, но в большинстве случаев это явный факт.

    Рассмотрим два варианта адресной строки, по которым показывается какой-то текст и профиль пользователя.

    Приблизительный код обработки в таком случае:
    switch($_GET["action"]) { case "about" : require_once("about.php"); // страница "О Нас" break; case "contacts" : require_once("contacts.php"); // страница "Контакты" break; case "feedback" : require_once("feedback.php"); // страница "Обратная связь" break; default: require_once("page404.php"); // страница "404" break; }
    Думаю, почти все так раньше делали.

    С использованием движка маршрутизации 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 и наполним его следующим кодом:
    ini_set("display_errors", 1); require_once "application/bootstrap.php";
    Тут вопросов возникнуть не должно.

    Следом, сразу же перейдем к фалу bootstrap.php :
    require_once "core/model.php"; require_once "core/view.php"; require_once "core/controller.php"; require_once "core/route.php"; Route::start(); // запускаем маршрутизатор
    Первые три строки будут подключать пока что несуществующие файлы ядра. Последние строки подключают файл с классом маршрутизатора и запускают его на выполнение вызовом статического метода start.

    2.1. Реализация маршрутизатора URL Пока что отклонимся от реализации паттерна MVC и займемся мрашрутизацией. Первый шаг, который нам нужно сделать, записать следующий код в .htaccess :
    RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule .* index.php [L]
    Этот код перенаправит обработку всех страниц на index.php , что нам и нужно. Помните в первой части мы говорили о Front Controller?!

    Маршрутизацию мы поместим в отдельный файл route.php в директорию core. В этом файле опишем класс Route, который будет запускать методы контроллеров, которые в свою очередь будут генерировать вид страниц.

    Содержимое файла route.php

    class Route { static function start() { // контроллер и действие по умолчанию $controller_name = "Main"; $action_name = "index"; $routes = explode("/", $_SERVER["REQUEST_URI"]); // получаем имя контроллера if (!empty($routes)) { $controller_name = $routes; } // получаем имя экшена if (!empty($routes)) { $action_name = $routes; } // добавляем префиксы $model_name = "Model_".$controller_name; $controller_name = "Controller_".$controller_name; $action_name = "action_".$action_name; // подцепляем файл с классом модели (файла модели может и не быть) $model_file = strtolower($model_name).".php"; $model_path = "application/models/".$model_file; if(file_exists($model_path)) { include "application/models/".$model_file; } // подцепляем файл с классом контроллера $controller_file = strtolower($controller_name).".php"; $controller_path = "application/controllers/".$controller_file; if(file_exists($controller_path)) { include "application/controllers/".$controller_file; } else { /* правильно было бы кинуть здесь исключение, но для упрощения сразу сделаем редирект на страницу 404 */ Route::ErrorPage404(); } // создаем контроллер $controller = new $controller_name; $action = $action_name; if(method_exists($controller, $action)) { // вызываем действие контроллера $controller->$action(); } else { // здесь также разумнее было бы кинуть исключение Route::ErrorPage404(); } } function ErrorPage404() { $host = "http://".$_SERVER["HTTP_HOST"]."/"; header("HTTP/1.1 404 Not Found"); header("Status: 404 Not Found"); header("Location:".$host."404"); } }


    Замечу, что в классе реализована очень упрощенная логика (несмотря на объемный код) и возможно даже имеет проблемы безопасности. Это было сделано намерено, т.к. написание полноценного класса маршрутизации заслуживает как минимум отдельной статьи. Рассмотрим основные моменты…

    В элементе глобального массива $_SERVER["REQUEST_URI"] содержится полный адрес по которому обратился пользователь.
    Например: example.ru/contacts/feedback

    С помощью функции explode производится разделение адреса на составлющие. В результате мы получаем имя контроллера, для приведенного примера, это контроллер contacts и имя действия, в нашем случае - feedback .

    Далее подключается файл модели (модель может отсутствовать) и файл контроллера, если таковые имеются и наконец, создается экземпляр контроллера и вызывается действие, опять же, если оно было описано в классе контроллера.

    Таким образом, при переходе, к примеру, по адресу:
    example.com/portfolio
    или
    example.com/portfolio/index
    роутер выполнит следующие действия:

  • подключит файл model_portfolio.php из папки models, содержащий класс Model_Portfolio;
  • подключит файл controller_portfolio.php из папки controllers, содержащий класс Controller_Portfolio;
  • создаст экземпляр класса Controller_Portfolio и вызовет действие по умолчанию - action_index, описанное в нем.
  • Если пользователь попытается обратиться по адресу несуществующего контроллера, к примеру:
    example.com/ufo
    то его перебросит на страницу «404»:
    example.com/404
    То же самое произойдет если пользователь обратится к действию, которое не описано в контроллере.2.2. Возвращаемся к реализации MVC Перейдем в папку core и добавим к файлу route.php еще три файла: model.php, view.php и controller.php


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

    Содержимое файла model.php
    class Model { public function get_data() { } }
    Класс модели содержит единственный пустой метод выборки данных, который будет перекрываться в классах потомках. Когда мы будем создавать классы потомки все станет понятней.

    Содержимое файла view.php
    class View { //public $template_view; // здесь можно указать общий вид по умолчанию. function generate($content_view, $template_view, $data = null) { /* if(is_array($data)) { // преобразуем элементы массива в переменные extract($data); } */ include "application/views/".$template_view; } }
    Не трудно догадаться, что метод generate предназначен для формирования вида. В него передаются следующие параметры:

  • $content_file - виды отображающие контент страниц;
  • $template_file - общий для всех страниц шаблон;
  • $data - массив, содержащий элементы контента страницы. Обычно заполняется в модели.
  • Функцией include динамически подключается общий шаблон (вид), внутри которого будет встраиваться вид
    для отображения контента конкретной страницы.

    В нашем случае общий шаблон будет содержать header, menu, sidebar и footer, а контент страниц будет содержаться в отдельном виде. Опять же это сделано для упрощения.

    Содержимое файла controller.php
    class Controller { public $model; public $view; function __construct() { $this->view = new View(); } function action_index() { } }
    Метод action_index - это действие, вызываемое по умолчанию, его мы перекроем при реализации классов потомков.

    2.3. Реализация классов потомков Model и Controller, создание View"s Теперь начинается самое интересное! Наш сайт-визитка будет состоять из следущих страниц:
  • Главная
  • Услуги
  • Портфолио
  • Контакты
  • А также - страница «404»
  • Для каждой из страниц имеется свой контроллер из папки controllers и вид из папки views. Некоторые страницы могут использовать модель или модели из папки models.


    На предыдущем рисунке отдельно выделен файл template_view.php - это шаблон, содержащий общую для всех страниц разметку. В простейшем случае он мог бы выглядеть так:
    Главная
    Для придания сайту презентабельного вида сверстаем CSS шаблон и интегририруем его в наш сайт путем изменения структуры HTML-разметки и подключения CSS и JavaScript файлов:

    В конце статьи, в разделе «Результат», приводится ссылка на GitHub-репозиторий с проектом, в котором проделаны действия по интеграции простенького шаблона.

    2.3.1. Создадаем главную страницу Начнем с контроллера controller_main.php , вот его код:
    class Controller_Main extends Controller { function action_index() { $this->view->generate("main_view.php", "template_view.php"); } }
    В метод generate экземпляра класса View передаются имена файлов общего шаблона и вида c контентом страницы.
    Помимо индексного действия в контроллере конечно же могут содержаться и другие действия.

    Файл с общим видом мы рассмотрели ранее. Рассмотрим файл контента main_view.php :
    Добро пожаловать!

    ОЛОЛОША TEAM - команда первоклассных специалистов в области разработки веб-сайтов с многолетним опытом коллекционирования мексиканских масок, бронзовых и каменных статуй из Индии и Цейлона, барельефов и изваяний, созданных мастерами Экваториальной Африки пять-шесть веков назад...


    Здесь содержиться простая разметка без каких либо PHP-вызовов.
    Для отображения главной странички можно воспользоваться одним из следующих адресов:

    Пример с использованием вида, отображающего данные полученные из модели мы рассмотрим далее.

    2.3.2. Создадаем страницу «Портфолио» В нашем случае, страница «Портфолио» - это единственная страница использующая модель.
    Модель обычно включает методы выборки данных, например:
  • методы нативных библиотек pgsql или mysql;
  • методы библиотек, реализующих абстракицю данных. Например, методы библиотеки PEAR MDB2;
  • методы ORM;
  • методы для работы с NoSQL;
  • и др.
  • Для простоты, здесь мы не будем использовать SQL-запросы или ORM-операторы. Вместо этого мы сэмулируем реальные данные и сразу возвратим массив результатов.
    Файл модели model_portfolio.php поместим в папку models. Вот его содержимое:
    class Model_Portfolio extends Model { public function get_data() { return array(array("Year" => "2012", "Site" => "http://DunkelBeer.ru", "Description" => "Промо-сайт темного пива Dunkel от немецкого производителя Löwenbraü выпускаемого в России пивоваренной компанией "CАН ИнБев"."), array("Year" => "2012", "Site" => "http://ZopoMobile.ru", "Description" => "Русскоязычный каталог китайских телефонов компании Zopo на базе Android OS и аксессуаров к ним."), // todo); } }

    Класс контроллера модели содержится в файле controller_portfolio.php , вот его код:
    class Controller_Portfolio extends Controller { function __construct() { $this->model = new Model_Portfolio(); $this->view = new View(); } function action_index() { $data = $this->model->get_data(); $this->view->generate("portfolio_view.php", "template_view.php", $data); } }
    В переменную data записывается массив, возвращаемый методом get_data , который мы рассматривали ранее.
    Далее эта переменная передается в качестве параметра метода generate , в который также передаются: имя файла с общим шаблон и имя файла, содержащего вид c контентом страницы.

    Вид содержащий контент страницы находится в файле portfolio_view.php .
    Портфолио

    Все проекты в следующей таблице являются вымышленными, поэтому даже не пытайтесь перейти по приведенным ссылкам.
    ГодПроектОписание


    Здесь все просто, вид отображает данные полученные из модели.

    2.3.3. Создаем остальные страницы Остальные страницы создаются аналогично. Их код досутпен в репозитории на GitHub, ссылка на который приводится в конце статьи, в разделе «Результат».3. Результат А вот что получилось в итоге:

    Скриншот получившегося сайта-визитки



    Ссылка на GitHub: https://github.com/vitalyswipe/tinymvc/zipball/v0.1

    А вот в этой версии я набросал следующие классы (и соответствующие им виды):

    • Controller_Login в котором генерируется вид с формой для ввода логина и пароля, после заполнения которой производится процедура аутентификации и в случае успеха пользователь перенаправляется в админку.
    • Contorller_Admin с индексным действием, в котором проверяется был ли пользователь ранее авторизован на сайте как администратор (если был, то отображается вид админки) и действием logout для разлогинивания.
    Аутентификация и авторизация - это другая тема, поэтому здесь она не рассматривается, а лишь приводится ссылка указанная выше, чтобы было от чего оттолкнуться.4. Заключение Шаблон MVC используется в качестве архитектурной основы во многих фреймворках и CMS, которые создавались для того, чтобы иметь возможность разрабатывать качественно более сложные решения за более короткий срок. Это стало возможным благодаря повышению уровня абстракции, поскольку есть предел сложности конструкций, которыми может оперировать человеческий мозг.

    Но, использование веб-фреймворков, типа Yii или Kohana, состоящих из нескольких сотен файлов, при разработке простых веб-приложений (например, сайтов-визиткок) не всегда целесообразно. Теперь мы умеем создавать красивую MVC модель, чтобы не перемешивать Php, Html, CSS и JavaScript код в одном файле.

    Данная статья является скорее отправной точкой для изучения CMF, чем примером чего-то истинно правильного, что можно взять за основу своего веб-приложения. Возможно она даже вдохновила Вас и вы уже подумываете написать свой микрофреймворк или CMS, основанные на MVC. Но, прежде чем изобретать очередной велосипед с «блекджеком и шлюхами», еще раз подумайте, может ваши усилия разумнее направить на развитие и в помощь сообществу уже существующего проекта?!

    P.S.: Статья была переписана с учетом некоторых замечаний, оставленных в комментариях. Критика оказалась очень полезной. Судя по отклику: комментариям, обращениям в личку и количеству юзеров добавивших пост в избранное затея написать этот пост оказалось не такой уж плохой. К сожалению, не возможно учесть все пожелания и написать больше и подробнее по причине нехватки времени… но возможно это сделают те таинственные личности, кто минусовал первоначальный вариант. Удачи в проектах!

    5. Подборка полезных ссылок по сабжу В статье очень часто затрагивается тема веб-фреймворков - это очень обширная тема, потому что даже микрофреймворки состоят из многих компонентов хитро увязанных между собой и потребовалась бы не одна статья, чтобы рассказать об этих компонентах. Тем не менее, я решил привести здесь небольшую подборку ссылок (по которым я ходил при написаниие этой статьи), которые так или иначе касаются темы фреймворков.

    Теги: Добавить метки

    Первая статья о разработке собственного фреймворка будет посвящена обзору основных намеченных возможностей и архитектурных особенностей нового фреймворка. Здесь я опишу модули и компоненты, разработка которых будет описана подробно в следующих статьях. Итак, определимся, как мы организуем код фреймворка и приложений, которые его будут использовать, и далее накидаем roadmap разработки основных компонентов.

    Для начала я выбрал простую и лаконичную структуру директорий проекта:

      /
    • app – директория webroot сервера, сюда кладем index.php
      • public – здесь будем хранить статичные файлы приложения
    • src – директория с исходным кодом самого приложения
      • Blog – каждое приложение находится с своей директории
    • lib – директория для внешних библиотек. Тут же будет располагаться и код фреймворка
    • var – директория для файлов приложения (кэш, хранение файлов, сессий и т.п.)

    С директориями определились. Сейчас мы можем удобно разделять отдельный компоненты приложений по разным директориям, использовать PSR0 для автозагрузки классов и т.п.
    Далее нужно выбрать имя фреймворка, чтобы элементарно создать по него директорию внутри lib. Недолго думая, я выбрал имя Bun – короткое, запоминающееся и вроде ни с чем не конфликтующее.

    Внутри lib создаем директорию bun (неймспейс вендора), в ней еще одну директорию bun (неймспейс библиотеки), а внутри нее src – тут и разместим код нашего фреймворка. Такая замудреная структура директорий позволит нам в дальнейшем подключать фреймворк через http://packagist.org без лишних проблем.

    Далее выделяем namespace для базовых классов фреймворка – я выбрал Core. Чтобы организовать в дальнейшем подключение компонетов вне директории Core потребуется организации модульной структуры фреймворка. Поэтому первым компонентом системы у меня появляется модуль (Bun\Core\Module) .

    Bun\Core\Module

    Модуль должен предоставлять базовую информацию независимом компоненте фреймворка. Для начала - используемые модулем конфигурации и зависимости, версия, описание модуля и т.п. Каждая директория внутри lib/bun/bun/src – будет являться отдельным модулем и должна внутри себя содержать одноименный класс (например, Bun\Core\Core.php) – который представляет собой реализацию модуля.

    Bun\Core\Application

    Приложение представляет собой реализацию паттерна Front Controller – единая точка входа в приложение для всех запросов. В архитектуре Bun будет предусмотрено, что все запросы, включая php cli или запросы статичных файлов должны обрабатываться внутри Application. Экземпляр приложения будет создаваться в index.php (стартовом файле), при инициализации приложения будет указываться его среда исполнения (production, development, testing, etc). Уже приложение дальше будет выполнять работу по загрузке конфигурации, инициализации контейнера сервисов, роутингу запросов через вспомогательные компоненты, вызов контроллеров, выдачу ответа и завершение приложения.

    Первое, что делаем приложение при старте, как-правило -– загружает файлы конфигурации, поэтому следующим базовым компонентом будет Config

    Bun\Core\Config

    Для конфигурации приложения и компонентов фреймворка выделен отдельный набор классов – Bun\Core\Config\ApplicationConfig – непосредственно сервис для управления конфигами и класс Bun\Core\Config\AbstractConfig – абстрактный класс, являющийся базом классом для элементов конфигурации. В качестве формата конфигурации выбран PHP-класс. Это удобно с точки зрения кэширования, т.е. выгодней хранить конфиги непосредственно в коде приложения, чем использовать отдельные файлы форматов xml, json, ini и прочее.

    Использование классов в качестве конфигов также удобно и для разделение конфигураций отдельных компонентов и частей приложения. А также удобно для переопределения настроек фреймворка по-умолчанию внутри приложения, или настроек приложения внутри конкретной среды исполнения. По задумке каждый модуль или приложение содержит внутри себя пространство имен Config – в котором и складываются классы конфигураций. Каждый класс имеет свое пространство имен, а параметры конфигурации хранит в виде массива в защищенном свойстве.
    Доступ к конфигам предполагается через dot-нотацию $config->get("name1.name2.param1") .

    После того, как мы инициализировали конфигурацию приложения, можно приступать к обработки запроса. Для работы с запросами и ответами веб-приложений выделен отдельный набор компонетов Http



    Bun\Core\Http

    Набор компонетов Http будет отвечать за абстрагирование от работы с суперглобавльными переменными $_SERVER, $_GET, $_POST и пр. За это будет отвечать сервис Bun\Core\Http\Request. Кроме того, в Http войдет класс Response - который будет стандартом результата работы приложения, т.е. запуск контроллера должен завешать получение объекта Bun\Core\Http\Response. Данный класс абстрагирует нас от работы с http-заголовками и пр. Кроме того, удобно использовать производные классы, типа AjaxResponse, DownloadResponse, ConsoleResponse и т.п.

    Когда мы готовы получить информацию о запросе, можем переходить к роутингу: следующий компонент – Router

    Bun\Core\Router

    Router - стандартный компонент современных веб-приложений на php. В Bun Framework роутере не предвидится ничего экстра ординарного, простой конфиг в виде массива шаблонов url-запросов, который маппится на классы контроллеров и их экшенов. Я планирую реализовать возможность разбирать параметры из url вида /page/view/:page_id – которые будут передаваться в экшен контроллера в виде аргументов. Также планирую сделать разделение запросов по методу (удобно, когда некоторые методы можно вызывать только через POST - не нужно делать лишних проверок в коде бизнес-логики)

    От одного стандартного компонента php приложений переходим к другому – роутинг запросов тесно связан с реализацией паттерна MVC (Model View Controller). Здесь предполагает разделение логики приложения, данных и отображения.

    Bun Framework MVC

    В реализации MVC трудно особо отличиться – поэтому тут тоже все довольно прозаично. Внутри Core я выделаю пространство имен Controller – здесь создаю базовый класс контроллера, который имеет доступ ко всем копонентам системы, хранит в себе объект запустившего его приложения, сервис конфигов. При инизациализации контроллер получает параметры запуска: имя метода (экшена) и его аргументы.

    Код отображения я выделяю в отдельную директорию View внутри Core. Bun Framework не предусматривает каких-то определенных компонентов типа View – подразумевается, что вы просто дергаете нужный шаблон внутри контроллера, передавая туда данные. По-умочанию я предполагаю запить в фреймворк поддержку шаблонизатора Twig и поддержку нативных шаблонов *.phtml

    Последний компонент – Модель. Для нее также я выделяю отдельный namespace внутри Core: Model. Здесь в дело будет вступать еще один базоый компонент фреймворка – ObjectMapper. Сами по себе модели – просто классы, т.е. не являются ActiveRecord, но реализуют при это определенный Bun\Core\Model\ModelInterface. Именно с такими классами умеет работать ObjectMapper и сохранять их в какое-нибудь хранилище.

    Теперь придется рассказать и про маппер объектов и про хранилище, начнем с первого.

    Bun\Core\ObjectMapper

    Маппер объектов – пожалуй, самая сложная часть модуля Core фреймворка. Он будет представлять собой сервис, который умеет превращать объект модели в записи в какой-нибудь базе данных, а также делать обратное действие – брать запись из хранилища данных и маппить ее в объект модели. По-умолчанию в модуль Core войдет сервис, реализующий файловое хранилище объектов.

    Bun\Core\Storage

    Набор компонентов Storage (Хранилище) представляет собой абстракцию и интерфейсы, которым дожно следовать реализации любых хранилищ в приложении. Первым таким хранилищем будет Bun\Core\Storage\FileStorage . Для своей работы файловое хранилище будет использовать набор вспомогательных классов для работы с файлами, а также для построение запросов на поиск записей в хранилище файлов.

    Описанный выше маппер объектов сможет работать с любой реализацией Storage, чтобы сохранять там свои объекты. Здесь стоит выделить еще один важный компонент модуля Core – Repository.

    Bun\Core\Repository

    Репозитории – слой доступа к объектом. Чтобы не перегружать маппер объектов функционалом поиска и различных выборок объектов из хранилища - данная работа передана в репозитории. Репозиторий может работать напрямую с хранилищем, выбирать от туда данные, а затем через ObjectMapper превращать их в объекты и передавать приложению.

    Сразу умпомяну связанный с вышеописанными компонентами – кэш

    Bun\Core\Cache

    Cache – будет содержать набор абстракций для реализации взаимодействия с различными кэш-хранилищями, типа Redis, Memacached и т.п. Кроме того, в модуль Core будет включен компонент FileCacheDriver, который реализует кэшированние данных в файлах.

    Тут переходим к важному и, как я считаю, определяющему архитектуру, компоненту фреймворка. Гибкость и заменяемость компонентов достигается, когда ваш код не привязан жестко к опереденным сервисам, а может быстро между ними переключаться. Помимо того, следующий компонент делает огромную работу по грамотной организации кода фреймворка и приложения.

    Bun\Core\Container

    Container – реализует один из моих любимых паттернов в программировании – Dependency Injection. Внедрение зависимостей хорошо во всем – части приложения являются слабо зависимыми от конкретных компонентов, отдельные классы легко тестировать, подменяя их зависимости. Удобно реализовывать альтернативные варианты реализации в нескольких сервисах, а потом легко переключаться между ними даже без изменений в коде приложения. Плюс ко всему ваши классы, использующие внедрение зависимостей наглядно показывают свои зависимости – вы даже можете построить граф зависимостей компонентов вашего приложения.

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

    На этом базовые компоненты фреймворка в модуле Core пока закончены. В ходе реализации в Core модуль возможно будут включены реализации управления событиями, FormBuilder. Среди вспомогательных комопнентов стоит отметить типизацию исключений. Базовый класс исключений Bun\Core\Exception\Exception - предусмотрен для того, чтобы все остальные типизированны исключения внутри приложения и фреймворка наследовались от него. Это обеспечивает централизованный перехват исключений на уровне приложения и препятствует возникновению не перехваченных исключений и авварийному падению приложения.

    В следующих частях рассказа о разработке Bun Framework я начну рассказывать о компоненте Application, а также опишу сути автозагузки файлов по стандарту PSR0. После чего, перейду к описанию сервиса конфигурации приложения и набору компонентов Http

    Из паттернов меня устраивало mvc, registry. Для запросов я написал небольшой слой абстракции, для роутинга — свою функцию парсинга запроса.
    Структура веб-приложения будет такой

    Папка application

    Входной файл index.php подключает bootstrap.php. Тот в свою очередь подключает ядро, конфиг-файл, некоторые библиотеки и запускает маршрутизатор.

    Use Core\Route; require_once "lib/registry.php"; require_once "config.php"; require_once "lib/datebase.php"; require_once "core/model.php"; require_once "core/view.php"; require_once "core/controller.php"; require_once "core/route.php"; $router = new Route(); $router->start(); // запускаем маршрутизатор

    Реестр устроен просто:

    Namespace Lib; class Lib_Registry { static private $data = array(); static public function set($key, $value) { self::$data[$key] = $value; } static public function get($key) { return isset(self::$data[$key]) ? self::$data[$key] : null; } static public function remove($key) { if (isset(self::$data[$key])) { unset(self::$data[$key]); } } }

    Здесь геттеры и сеттеры для сохранения глобальных значений.

    Use Lib\Lib_Registry; define("PATH_SITE", $_SERVER["DOCUMENT_ROOT"]); define("HOST", "localhost"); define("USER", "root"); define("PASSWORD", "mypass"); define("NAME_BD", "articles"); define ("DS", DIRECTORY_SEPARATOR); $mysqli = new mysqli(HOST, USER, PASSWORD,NAME_BD)or die("Невозможно установить соединение c базой данных".$mysqli->connect_errno()); Lib_Registry::set("mysqli",$mysqli); $mysqli->query("SET names "utf8""); //база устанавливаем кодировку данных в базе

    Запрос вида http://domen.ru/articles/index преобразуется в Контролллер — Экшн. Название контролллера и экшна задается в стиле Zend framework, camel case — Controller_Name , function action_name (). Файл контролллера, модели, въюхи должен совпадать с названием контроллера в нижнем регистре без префикса Controller_ или Model_

    Класс модели задается так же — Model_Name , файл вьюхи мы уже выяснили — по имени экшна или явно в методе generate(name )

    Так как в перспективе у нас создание админки — создадим папки client и admin . Кстати наш маршрутизатор будет учитывать вложенные папки, т.е. можно будет создать подпапки в контроллерах (напр. /about/contacts/contacts.php ) и обратиться к нему по его пути /about/contacts/
    Итак, мы запустили маршрутизатор

    /** * */ public function start() { // catch AJAX request if ($this->getIsAjaxRequest()) { } session_start(); $this->dispatch(); } /** * */ public function dispatch(){ // диспетчер получает файл совпадающий с названием контроллера, экшн и аргументы $this->getDirections($file, $controller, $action, $args); /* ************* include Controller - Model */ if (is_readable($file) == false) { die ("File $file 404 Not Found"); } // подключили контроллер include ($file); $model = str_replace("controller", "model", $file); // Model additional if(is_readable($model)){ // подключаем модель include($model); } /* ****** получаем класс ** */ $controller = ucfirst($controller); $class = ucfirst($this->namespace)."\Controller_" . $controller; // создаем экземпляр $controller = new $class($this->controller_path_folder); if (is_callable(array($controller, $action)) == false) { die ("Action $action 404 Not Found"); } // вызываем экшн $controller->$action($args); }

    Диспетчер вызывает метод getDirections(), т.е. получить директивы запроса. По умолчанию дефолтный контроллер — articles, экшн — index.

    /** * @param $file * @param $controller * @param $action * @param $args */ private function getDirections(&$file, &$controller, &$action, &$args) { $route = (empty($_SERVER["REQUEST_URI"])) ? "" : $_SERVER["REQUEST_URI"]; unset($_SERVER["REQUEST_URI"]); $route = trim($route, "/\\"); $controller_path = $this->path; if (empty($route)) { /* ******************* Default directions ******** */ $controller = "articles"; $action = "action_index"; $controller_path = $this->controller_path_folder = "application/controllers/$this->namespace/"; $file = $controller_path.$controller.".php"; } else { $parts = explode("/", $route); /* ************** namespace ********** */ if($parts == "admin") { $this->namespace = "admin"; array_shift($parts); } /* ***************** folders & subfolders ******* */ $fullpath = $this->controller_path_folder = $controller_path . $this->namespace; foreach ($parts as $part) { $fullpath .= DS . $part; if (is_dir($fullpath)) { array_shift($parts); continue; } if (is_file($fullpath . ".php")) { array_shift($parts); $file = "$fullpath.php"; break; } } /* *************** Controller, Action, Params ******** */ if(!isset($part)) $part = "articles"; $controller = $part; if(!$file) $file = $fullpath."/$part.php"; $action = array_shift($parts); if(!$action) $action = "action_index"; else $action = "action_$action"; $args = $parts; } }

    В следующем уроке рассмотрим создание базовых контроллера, моделей и вьюх, напишем запросы.

    Файлы 1-го урока здесь

    https://github.com/vaajnur/create_php_application
    Ветка мастер будет 1-м уроков, далее для каждого урока одноименная ветка — lesson1, lesson2, 3..

    Последнее обновление: 29.11.2017

    В Angular есть своя система маршрутизации, которая позволяет сопоставлять маршруты с компонентами. Но в ASP.NET Core также есть своя система маршрутизации, которая применяется для обработки запросов. С какими проблемами мы можем столкнуться? К примеру, возьмем проект из прошлой темы, где уже использовалась маршрутизация. Если внутри приложения Angular мы будем переходить по ссылкам, то у нас будет все нормально.

    Однако если же мы напрямую в адресной строке браузера введем нужный нам адрес и отправим запрос, то такой запрос не будет должным образом обработан:

    Несмотря на то, что идет обращение по тому же адресу, но во втором случае мы получим ошибку 404.

    Когда мы переходим п ссылке или программным образом внутри приложения Angular, то применяется соответствующий API HTML5 (Pushstate API), с помощью которого меняется URL в браузере без отправки HTTP-запроса. Но когда мы вручную вводим адрес ресурса в адресной строке браузера, то браузер отправляет новый запрос к приложению ASP.NET Core.

    В примере выше отправлялся запрос "product/1". Но у нас нет действия контроллера, который бы сопоставлялся бы с подобным запросом. Поэтому естественно мы получим ошибку. И нам необходимо добавить дополнительный код на стороне сервера, чтобы подобные запросы также бы обрабатывались приложением Angular.

    Что в данном случае мы можем сделать? Все зависит от того, как формируется веб-страница. Либо это непосредственно статическая веб-страница, которая напрямую отправляется пользователю. Либо же это представление (файл с расширением cshtml), на котором определен код загрузки приложения Angulr.

    Запросы к статическим файлам

    В прошлой теме приложение Angular загружалось на статическую веб-страницу index.html, которая располагалась в проекте в папке wwwroot:

    Angular in ASP.NET Core

    И в этом случае нам надо, чтобы, если запрос не предназначался к статическому файлу, не предназначался ни к одному из действий контроллера, то такой в ответ на такой запрос посылался файл index.html. И в данном случае на стороне сервера мы можем использовать несколько подходов.

    Метод Run

    Самый простой способ - добавить в конец конвейера обработки запроса middleware, который бы отправлял файл index.html. Для этого изменим метод Configure в классе Startup:

    Public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true }); } app.UseDefaultFiles(); app.UseStaticFiles(); app.UseMvc(); // обработка маршрутов, которые не сопоставлены с ресурсам ранее app.Run(async (context) => { context.Response.ContentType = "text/html"; await context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "index.html")); }); }

    Таким образом, для всех запросов, которые не сопоставлены с ресурсами в приложении, будет отправляться файл wwwroot/index.html.

    Минусом такого подхода является то, что нам сложно будет задействовать на стороне сервера обработку ошибок 404. Так как если у нс нет контролеров и действий, соответствующих запросу, то запрос все равно будет обрабатываться, правда, на стороне клиента в Angular.

    Отправка файла методом контроллера

    Похожий подход предоставляет отправка файла методом контроллера. Например, допустим, в проекте в папке Controllers есть следующий контроллер HomeController:

    Using Microsoft.AspNetCore.Mvc; using System.IO; using Microsoft.AspNetCore.Hosting; namespace HelloAngularApp.Controllers { public class HomeController: Controller { IHostingEnvironment env; public HomeController(IHostingEnvironment env) { this.env = env; } public IActionResult Index() { return new PhysicalFileResult(Path.Combine(env.WebRootPath, "index.html"), "text/html"); } } }

    В данном случае метод Index отправляет файл index.html.

    В этом случае изменим метод Configure класса Startup следующим образом:

    Public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true }); } ` app.UseDefaultFiles(); app.UseStaticFiles(); app.UseMvc(routes =>

    В данном случае для инфраструктуры MVC определено два маршрута. Первый маршрут сопоставляется с обычными контроллерами и их действиями. Второй маршрут определен с помощью метода MapSpaFallbackRoute() и сопоставляется с действием Index контроллера Home, который в данном случае отправляет в ответ пользователю файл index.html.

    Запросы к действиям контроллера

    Мы необязательно можем определять загрузку приложения Angular на статической html-странице. Это может быть и представление. Например, определим в проекте каталог Views/Home и в него поместим новый файл Index.cshtml :

    Angular in ASP.NET Core

    В данном случае представление содержит только код html, хотя при необходимости также можно определить в нем инструкции Razor, создать для него мастер страницу и т.д.

    А метод Index контролера HomeController будет использовать это представление:

    Using Microsoft.AspNetCore.Mvc; namespace HelloAngularApp.Controllers { public class HomeController: Controller { public IActionResult Index() { return View(); } } }

    В методе Configure класса Startup определим следующий код:

    Public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true }); } //app.UseDefaultFiles(); - этот метод теперь не нужен app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("angular-fallback", new { controller = "Home", action = "Index" }); }); }

    Здесь опять же с помощью метода routes.MapSpaFallbackRoute все остальные запросы, которые не сопоставлены с другими ресурсами, будут обрабатываться методом Index контроллера HomeController.