Включение и применение маршрутизации с помощью атрибутов. Реализация классов потомков Model и Controller, создание View"s

12.04.2019

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

В предыдущих главах при обращении к некоторому действию контроллера мы набирали в браузере адрес наподобие следующего http://localhost/Home/Index , где Home являлся именем контроллера без префикса Controller , а Index - именем действия этого контроллера. Если метод Index принимал какой-нибудь параметр, например, типа int: public ActionResult Index(int Id) , то мы могли обратиться к этому методу и передать значение в его параметр с помощью следующей строки: http://localhost/Home/Index/5 . Но мы не говорили еще о том, почему мы должны прописывать маршрут именно так, и как мы собственно можем управлять маршрутами.

Посмотрим, как определен маршрут. Если в MVC 3 для определения маршрута по умолчанию в файле Global.asax.cs создавался специальный метод RegisterRoutes , который определял маршрут по умолчанию и потом вызывался в методе Application_Start:

Using System.Web.Routing; using System.Data.Entity; namespace MvcEmptyApp { public class MvcApplication: System.Web.HttpApplication { protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ............................. } public static void RegisterRoutes(RouteCollection routes) { //Здесь определение маршрутов................................ } } }

То в MVC 4 все начальные настройки конфигурации для файла Global.asax.cs вынесены в классы, расположенные в папке App_Start . И затем эти классы вызываются в файле Global.asax.cs:

Using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; namespace MvcEmptyApp { public class MvcApplication: System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); } } }

Откроем файл RouteConfig.cs , расположенный в папке App_Start, в котором и находится определение маршрута по умолчанию:

Using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; namespace MvcEmptyApp { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapHttpRoute(name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional }); routes.MapRoute(name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }); } } }

Цель первой строки routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); отключить обработку запросов для некоторых файлов, например с расширением *.axd (WebResource.axd). Следующие два вызова - routes.MapHttpRoute и routes.MapRoute как раз и задают определение маршрута. Главное отличие состоит в том, что вызов routes.MapHttpRoute устанавливает сопоставления запроса некоторому маршруту для ресурса Web API, а routes.MapRoute устанавливает маршрут для обычного контроллера, которые мы использовали в предыдущих главах.

Поскольку тему Web API мы рассмотрим чуть позднее, поэтому пока поговорим об определении маршрута на примере обычного контроллера, к тому же в обоих определениях маршрутов много похожего.

Итак, метод MapRoute выполняет сопоставление маршрута запросу. Он имеет ряд перегруженных версий, которые помогают указать параметры сопоставления.

Здесь мы сначала задаем имя маршрута с помощью свойства name (в данном случае имя Default ). С помощью параметра url мы задаем шаблон Url , с которым будет сопоставляться данный маршрут.

Шаблон URL состоит из нескольких сегментов, заключенных в фигурные скобки (сегмент - это часть запроса, находящаяся между слешами, но не включающая эти слеши). Каждый сегмент шаблона содержит параметр. Эти параметры называются параметрами URL.

При этом именовать параметры можно как угодно, используя любые алфавитно-цифровые символы. При получении запроса механизм маршрутизации парсит строку URL и помещает значения маршрута в словарь - в объект RouteValueDictionary , доступный через контекст приложения RequestContext . В качестве ключей используются имена параметров URL, а соответствующие сегменты URL в качестве значений. То есть, если строка запроса URL выглядит следующим образом: http://localhost/Home/Index/5 , то у нас образуются следующие пары ключей и значений в словаре RouteValueDictionary:

Параметр

Значение

Следующий параметр - defaults определяет значения по умолчанию для маршрута. И если вдруг в строке запроса мы не указали все параметры и попытались обратиться по адресу http://localhost/ , то система маршрутизации вызовет метод Index контроллера Home, как указано в параметре defaults . Также, если мы не укажем метод контроллера, например, http://localhost/Home/ , также будет вызван метод Index контроллера Home.

Поэтому если мы захотим, к примеру, чтобы у нас по умолчанию клиент обращался не к методу Index контроллера HomeController, а, например, к методу Show контроллера BookController, то мы можем соответственно изменить значения данного параметра:

Defaults: new { controller = "Book", action = "Show", id = UrlParameter.Optional }

Последний параметр объявлен как необязательный id = UrlParameter.Optional , поэтому, если он не указан в строке запроса, он не будет учитываться и передаваться в словарь параметров RouteValueDictionary . Например, запрос http://localhost/Home/Create/3 вызовет метод Create контроллера Home, передав в этот метод в качестве параметра число 3. В то же время запрос http://localhost/Home/Create/ также вызовет метод Create контроллера Home, хотя последний параметр в нем не указан.

Таким образом, настройки по умолчанию позволяют нам не указывать в строке запроса полностью название контроллера и его метода. Но в случае если такие настройки не заданы, мы должны определять в строке запроса контроллер и его метод. Например, изменим установку маршрутов в файле RouteConfig.cs следующим образом:

Public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapHttpRoute(name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional }); routes.MapRoute(name: "Default", url: "{controller}/{action}"); } }

При запуске из Visual Studio или при запуске в браузере по адресу http://mysyte.com/ мы получим информацию об ошибке. Ошибка будет состоять в том, что теперь нам полностью надо набирать в строке запроса адрес ресурса. Поэтому следующий адрес http://mysyte.com/Home/Index будет нормально работать (если у вас, конечно, определен контроллер Home с методом Index и соответствующим ему представлением).

И если мы теперь перейдем по адресу http://localhost/Home/ , как мы это делали выше, то получим ошибку, так как у нас указан только одни сегмент. А в определении маршрута у нас указано два сегмента - {controller}/{action} . Если для параметров не определены значения по умолчанию, то строка запроса должна иметь такое же число сегментов, для которых не определены значения по умолчанию.

В то же время если запрос будет состоять из трех сегментов, например, http://localhost/Home/Index/1 , то мы также получим ошибку, потому что число сегментов в запросе больше числа, определенного в шаблоне URL данного маршрута.

Маршрутизация с помощью атрибутов

Маршруты во всех примерах, приведенных в предыдущих статьях, были определены посредством методики, которая называется маршрутизацией на основе соглашений . В версии MVC 5 появилась поддержка новой методики, известной как маршрутизация с помощью атрибутов , при которой маршруты определяются атрибутами C#, примененными непосредственно к классам контроллеров. В последующих разделах будет показано то, как создавать и конфигурировать маршруты с использованием атрибутов, которые могут свободно смешиваться со стандартными маршрутами на основе соглашений.

Сравнение маршрутизации на основе соглашений и маршрутизации с помощью атрибутов

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

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

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

Включение и применение маршрутизации с помощью атрибутов

Маршрутизация с помощью атрибутов по умолчанию отключена. Включается она посредством расширяющего метода MapMvcAttributeRoutes() , который вызывается на объекте RouteCollection, передаваемом в качестве аргумента статическому методу RegisterRoutes(). В примере ниже приведено содержимое файла RouteConfig.cs, в который был добавлен вызов метода MapMvcAttributeRoutes(), а также упрощены маршруты в приложении, чтобы можно было сосредоточить внимание на использовании атрибутов:

Using System.Web.Mvc; using System.Web.Routing; using System.Web.Mvc.Routing.Constraints; using UrlsAndRoutes.Infrastructure; namespace UrlsAndRoutes { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.MapMvcAttributeRoutes(); routes.MapRoute(name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new { "UrlsAndRoutes.Controllers" }); } } }

Вызов метода MapMvcAttributeRoutes() заставляет систему маршрутизации проинспектировать классы контроллеров в приложении в поисках атрибутов, конфигурирующих маршруты. Наиболее важный атрибут называется Route , и в примере ниже применяется к методу действия Index():

Using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class CustomerController: Controller { // ... public ActionResult Index() { ViewBag.Controller = "Customer"; ViewBag.Action = "Index"; return View("ActionName"); } } }

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

Свойство Name

Назначает маршруту имя, используемое для генерации исходящих URL из специфичного маршрута.

Свойство Template

Определяет шаблон, который будет использоваться для сопоставления с URL, нацеленными на данный метод действия.

Если во время применения атрибута Route предоставляется только одно значение (как делалось в примере выше), то это значение считается шаблоном, который будет использоваться при сопоставлении с маршрутами. Шаблоны для атрибута Route следуют той же самой структуре, как и при маршрутизации на основе соглашений, хотя есть ряд отличий, когда дело доходит до ограничений сопоставления маршрутов.

В рассматриваемом примере атрибут Route применяется для указания на то, что действие Index контроллера Customer может быть доступно через URL вида /Test. Результат можно видеть на рисунке ниже:

Когда метод действия декорирован атрибутом Route, он больше не может быть доступен через маршруты на основе соглашений, определенные в файле RouteConfig.cs. В данном примере это означает, что действие Index контроллера Customer не будет доступно через URL вида /Customer/Index. Подобный подход к системе маршрутизации можно увидеть на популярном конструкторе логотипов http://logoservis.ru/ .

Атрибут Route препятствует нацеливанию маршрутов, основанных на соглашениях, на метод действия, даже если маршрутизация с помощью атрибутов отключена. Позаботьтесь о вызове метода MapMvcAttributeRoutes() в файле RouteConfig.cs, иначе вы создадите недостижимые методы действий.

Атрибут Route влияет только на методы, к которым он применен, а это значит, что хотя метод действия Index() в контроллере Customer достижим через URL вида /Test, к методу действия List() по-прежнему придется обращаться с использованием URL в форме/Customer/List. Атрибут Route можно применять к одному и тому же методу действия несколько раз, при этом каждый случай использования приводит к созданию нового маршрута.

Создание маршрутов с переменными сегментов

Средство маршрутизации с помощью атрибутов поддерживает все возможности маршрутизации на основе соглашений, хотя и выраженные через атрибуты. Это включает создание маршрутов, которые содержат переменные сегментов; пример такого маршрута приведен в примере ниже:

Using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class CustomerController: Controller { // ... public string Create(string user, int id) { return string.Format("Пользователь: {0}, Id: {1}", user, id); } } }

Здесь был добавлен метод действия по имени Create(), который принимает аргументы string и int. Для простоты из метода возвращается результат типа string, поэтому создавать представление не придется. Маршрут, определенный атрибутом Route, смешивает статический префикс (Users/Add) с переменными сегментов user и id, которые соответствуют аргументам метода.

Инфраструктура MVC Framework применяет средство привязки моделей, чтобы преобразовать значения переменных сегментов в подходящие типы для вызова метода Create(). Результат перехода на URL вида /Users/Add/Alex/120 показан на рисунке ниже:

Обратите внимание на то, что каждый экземпляр атрибута Route функционирует независимо, а это означает возможность создания совершенно разных маршрутов для нацеливания на методы действий в контроллере, как показано в таблице ниже:

Применение ограничений маршрутов в атрибутах

Маршруты, определенные с использованием атрибутов, могут быть ограничены подобно маршрутам, которые определяются в файле RouteConfig.cs, хотя применяемый прием имеет более прямой характер. Чтобы продемонстрировать это в работе, в контроллер Customer добавлен новый метод действия, как показано в примере ниже:

Using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class CustomerController: Controller { // ... public string Create(string user, int id) { return string.Format("Пользователь: {0}, Id: {1}", user, id); } public string ChangePass(string user, string password) { return string.Format("Метод действия ChangePass() - Пользователь: {0}, Пароль: {1}", user, password); } } }

Новый метод действия по имени ChangePass() принимает два аргумента типа string. С помощью атрибута Route действие ассоциируется с тем же самым шаблоном, что и метод Create(): статическим префиксом /Users/Add, за которым следуют две переменные сегментов. Чтобы различать эти действия, к атрибуту Route для метода Create() применяется ограничение:

Здесь указано имя переменной сегмента (id), двоеточие и затем int. Это сообщает системе маршрутизации о том, что метод действия Create() должен быть ориентирован на запросы, в которых значение, предоставляемое для сегмента id, является допустимым значением типа int. Ограничение int соответствует классу ограничения IntRouteConstraint, и в таблице из предыдущей статьи был приведен набор атрибутов, которые можно использовать для доступа к встроенным ограничениям на основе типов и значений.

Чтобы увидеть эффект от примененного ограничения, необходимо запустить приложение и запросить URL двух видов - /Users/Add/Alex/120 и /Users/Add/Alex/Пароль12345. Последний сегмент в первом URL представляет собой допустимое значение int и направляется методу Create(). Последний сегмент второго URL не является значением int, поэтому он направляется методу ChangePass(), как показано на рисунке ниже:

Комбинирование ограничений

К переменной сегмента можно применять несколько ограничений с целью дальнейшего сужения диапазона значений, которые будут соответствовать маршруту. В следующем примере демонстрируется комбинирование ограничений alpha и length в маршруте для метода ChangePass():

// ... public string ChangePass(string user, string password) { return string.Format("Метод действия ChangePass() - Пользователь: {0}, Пароль: {1}", user, password); } // ...

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

Применяя ограничения, соблюдайте осторожность. Маршруты, определяемые с помощью атрибута Route, работают точно таким же образом, как маршруты, определяемые в файле RouteConfig.cs, и для URL, которые не соответствуют ни одному из методов действий, браузеру будет отправляться результат 404 - Not Found (не найдено). Всегда предусматривайте запасной маршрут, который будет давать соответствие независимо от значений, содержащихся в URL.

Использование префикса маршрута

С помощью атрибута RoutePrefix можно определить общий префикс, который будет применяться ко всем маршрутам, заданным в контроллере. Это может оказаться удобным при наличии множества методов действий, направление на которые должно осуществляться с использованием одного и того же корня URL. В примере ниже демонстрируется применение атрибута RoutePrefix к классу CustomerController:

Using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class CustomerController: Controller { public ActionResult List() { // ... } public ActionResult Index() { // ... } public string Create(string user, int id) { // ... } public string ChangePass(string user, string password) { // ... } } }

Атрибут RoutePrefix используется для указания на то, что маршруты, связанные с методом действия, должны снабжаться префиксом Users. Определив такой общий префикс, можно удалить его из атрибутов Route для методов действий Create() и ChangePass(). Инфраструктура MVC Framework будет комбинировать общий префикс с шаблоном URL автоматически при создании маршрутов.

Обратите внимание на изменение также и шаблона URL для атрибута Route, применяемого к методу действия Index(), следующим образом:

За счет предварения URL префиксом "~/" мы указываем инфраструктуре MVC Framework на то, что атрибут RoutePrefix не должен применяться к методу действия Index(), поэтому он по-прежнему будет доступен через URL вида /Test.

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

И так начнем, как я уже писал в предыдущей статье, паттерн MVC подразумевает одну точку входа – index.php, через это скрипт будут проходить все запросы, через него будет работать вся логика проекта. Для того чтобы реализовать такой подход необходимо настроить сервер, подразумевается, что сайт работает на сервере apache, поэтому нам достаточно создать файл.htaccess, в котором мы укажем правила маршрутизации URL. Помимо определения точки входа, маршрутизация позволяет создавать ЧПУ(человеко-понятные урлы). То есть после правильной настройки, адреса страниц буду выглядеть вот так site.ru/article/new.
Для начала, давайте составим.htaccess, который перенаправит обработку всех страниц на скрипт index.php. Код выглядит вот так:

RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?route=$1

Файл.htaccess должен лежать в корневой папке сайта, тут же необходимо создать скрипт index.php, который является точкой входа. Давайте запишем в index.php одну строку, для проверки работы перенаправления:

Echo "test";

Теперь можно проверять работу перенаправления, введите любой адрес и посмотрите, что получиться: test-mvc.web/sdf/sdf/ или test-mvc.web/sdf/sdf/2342/не важно, на экране в любом случае, должно появиться «Test». Если Вы увидели эту надпись, значит, у нас все получилось.
Продолжим, давайте для удобства создадим в корне сайта файл config.php, в котором будем задавать различные константы, облегчающие своим существование настройку сайта. Это могут быть различные пути к скриптам, подступы к базе данных и так далее. Сейчас в конфиге давайте зададим следующее:

// Задаем константы: define ("DS", DIRECTORY_SEPARATOR); // разделитель для путей к файлам $sitePath = realpath(dirname(__FILE__) . DS); define ("SITE_PATH", $sitePath); // путь к корневой папке сайта // для подключения к бд define("DB_USER", "root"); define("DB_PASS", ""); define("DB_HOST", "localhost"); define("DB_NAME", "blog_mvc");

Для того, чтобы константы и другие данные конфига мы могли использовать во всем проекте, в файле index.php необходимо подключить скрипт config.php.
Помимо подключения файла с настройками, в index.php нужно создать подключение к базе данных, подключить скрипт с ядром сайта и запустить роутер, в котором будет происходить маршрутизация.
Теперь по порядку, создание соединения с базой данных будет находиться в index.php для того, чтобы соединение открывалось только один раз. Единожды открыв соединение, мы сможем использовать его во всех контроллерах и моделях, но об этом чуть позже. Сейчас просто создадим соединение с базой. Для работы с бд я решил использовать PDO. Подробнее почитать про PDO можно .
Ядро сайта расположим в папке core и назовем скрипт core.php, тут мы напишем функцию, которая будет сама подключать, необходимы для работы классы. Такая функция очень облегчит и упростит нам работу с контролерами, моделями и тд. Поскольку, забегая вперед скажу, что каждый контролер и каждая модель будут представлять собой отдельный класс.
Помимо авто подключения классов, добавим в ядро создания хранилища (реестра), в котором будем хранить все необходимые объекты и переменные, которые могут пригодиться в любом месте проекта.
Роутер тоже подключим в индексном файле, он будет анализировать URL и подключать необходимый контроллер и экшен. Что такое контролер я писал в предыдущей статье, а информацию про экшен я пропустил умышленно, не став нагружать лишней информацией. Так что же такое экшен?
Контролер это класс, в котором заключены различные методы, при MVC подходе каждый метод будет являться экшеном. То есть экшен(action) – это метод класса, который будет обрабатывать данные и передавать их в представление (в шаблон). Может быть, пока не совсем понятно, но после примера все станет на свои места.
На данном этапе теории достаточно, давайте перейдем к практике. Приведу код файлов, работу которых, я описывал выше.
Код скрипта index.php:

// включим отображение всех ошибок error_reporting (E_ALL); // подключаем конфиг include ("/config.php"); // Соединяемся с БД $dbObject = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASS); // подключаем ядро сайта include (SITE_PATH . DS . "core" . DS . "core.php"); // Загружаем router $router = new Router($registry); // записываем данные в реестр $registry->set ("router", $router); // задаем путь до папки контроллеров. $router->setPath (SITE_PATH . "controllers"); // запускаем маршрутизатор $router->start();

Скрипт core.php:

// Загрузка классов "на лету" function __autoload($className) { $filename = strtolower($className) . ".php"; // определяем класс и находим для него путь $expArr = explode("_", $className); if(empty($expArr) OR $expArr == "Base"){ $folder = "classes"; }else{ switch(strtolower($expArr)){ case "controller": $folder = "controllers"; break; case "model": $folder = "models"; break; default: $folder = "classes"; break; } } // путь до класса $file = SITE_PATH . $folder . DS . $filename; // проверяем наличие файла if (file_exists($file) == false) { return false; } // подключаем файл с классом include ($file); } // запускаем реестр (хранилище) $registry = new Registry;

Класс хранилища Registry.php, будет находиться в папке /classes/

// Класс хранилища Class Registry { private $vars = array(); // запись данных function set($key, $var) { if (isset($this->vars[$key]) == true) { throw new Exception("Unable to set var `" . $key . "`. Already set."); } $this->vars[$key] = $var; return true; } // получение данных function get($key) { if (isset($this->vars[$key]) == false) { return null; } return $this->vars[$key]; } // удаление данных function remove($var) { unset($this->vars[$key]); } }

Код файла router.php, который находиться в папке /classes/

// класс роутера Class Router { private $registry; private $path; private $args = array(); // получаем хранилище function __construct($registry) { $this->registry = $registry; } // задаем путь до папки с контроллерами function setPath($path) { $path = trim($path, "/\\"); $path .= DS; // если путь не существует, сигнализируем об этом if (is_dir($path) == false) { throw new Exception ("Invalid controller path: `" . $path . "`"); } $this->path = $path; } // определение контроллера и экшена из урла private function getController(&$file, &$controller, &$action, &$args) { $route = (empty($_GET["route"])) ? "" : $_GET["route"]; unset($_GET["route"]); if (empty($route)) { $route = "index"; } // Получаем части урла $route = trim($route, "/\\"); $parts = explode("/", $route); // Находим контроллер $cmd_path = $this->path; foreach ($parts as $part) { $fullpath = $cmd_path . $part; // Проверка существования папки if (is_dir($fullpath)) { $cmd_path .= $part . DS; array_shift($parts); continue; } // Находим файл if (is_file($fullpath . ".php")) { $controller = $part; array_shift($parts); break; } } // если урле не указан контролер, то испольлзуем поумолчанию index if (empty($controller)) { $controller = "index"; } // Получаем экшен $action = array_shift($parts); if (empty($action)) { $action = "index"; } $file = $cmd_path . $controller . ".php"; $args = $parts; } function start() { // Анализируем путь $this->getController($file, $controller, $action, $args); // Проверка существования файла, иначе 404 if (is_readable($file) == false) { die ("404 Not Found"); } // Подключаем файл include ($file); // Создаём экземпляр контроллера $class = "Controller_" . $controller; $controller = new $class($this->registry); // Если экшен не существует - 404 if (is_callable(array($controller, $action)) == false) { die ("404 Not Found"); } // Выполняем экшен $controller->$action(); } }

Теперь необходимо создать папки для хранения контроллеров, шаблонов и моделей – в корне создадим три папки controllers, views и models. И создадим несколько тестовых файлов /controllers/index.php, /views/index/index.php и /models/model_users.php, а теперь заполним файлы:
Для контроллера:

// контролер Class Controller_Index Extends Controller_Base { // шаблон public $layouts = "first_layouts"; // экшен function index() { $model = new Model_Users(); $userInfo = $model->getUser(); $this->template->vars("userInfo", $userInfo); $this->template->view("index"); } }

Для отображения(/views/index/index.php)

Test view
id:
name:

И модель:

// модель Class Model_Users{ public function getUser(){ return array("id"=>1, "name"=>"test_name"); } }

Как вы могли заметить, класс контролера наследуется от родительского класса Controller_Base. Это сделано, для того, чтобы упростить класс контролера. Поскольку нам еще необходимо подключать класс для работы с шаблонами, его подключение вынесено в Controller_Base.
Приведу его код, он расположен в папке /classes/ и называется controller_base.php:

// абстрактый класс контроллера Abstract Class Controller_Base { protected $registry; protected $template; protected $layouts; // шаблон public $vars = array(); // в конструкторе подключаем шаблоны function __construct($registry) { $this->registry = $registry; // шаблоны $this->template = new Template($this->layouts, get_class($this)); } abstract function index(); }

Теперь осталось только разобраться с шаблонами. В абстрактном классе Controller_Base мы вызываем класс Template и передаем ему имя шаблона и имя контроллера.
Код класса Template, который лежит тут /classes/ и называется template.php

// класс для подключения шаблонов и передачи данных в отображение Class Template { private $template; private $controller; private $layouts; private $vars = array(); function __construct($layouts, $controllerName) { $this->layouts = $layouts; $arr = explode("_", $controllerName); $this->controller = strtolower($arr); } // установка переменных, для отображения function vars($varname, $value) { if (isset($this->vars[$varname]) == true) { trigger_error ("Unable to set var `" . $varname . "`. Already set, and overwrite not allowed.", E_USER_NOTICE); return false; } $this->vars[$varname] = $value; return true; } // отображение function view($name) { $pathLayout = SITE_PATH . "views" . DS . "layouts" . DS . $this->layouts . ".php"; $contentPage = SITE_PATH . "views" . DS . $this->controller . DS . $name . ".php"; if (file_exists($pathLayout) == false) { trigger_error ("Layout `" . $this->layouts . "` does not exist.", E_USER_NOTICE); return false; } if (file_exists($contentPage) == false) { trigger_error ("Template `" . $name . "` does not exist.", E_USER_NOTICE); return false; } foreach ($this->vars as $key => $value) { $$key = $value; } include ($pathLayout); } }

Если вы внимательно прочитали код, то наверняка поняли, что для отображения на страницах у нас используется шаблон first_layouts и вьюха(отображение) index.php – ее код я приводил чуть выше. Все что нам осталось, это создать файл шаблона first_layouts. Расположим его в папке /views/layouts/first_layouts.php
Шаблон будет содержать вот такой код:

header

footer

Вот и все, на этом создание «каркаса» закончено. Сейчас у нас получилась самая простая структура, использующая в своей основе паттерн MVC. В этой статье я не стал затрагивать работу с базой данных, только вскользь упомянул ее, поскольку статья и так получилась большая. Непосредственно работу с базой данных я опишу в следующей статье.
На этом статья закончена, скачать исходники можно архивом .

Давайте поговорим о интересных полезностях, которые рано или поздно пригодятся любому web-developer`у.

Начнем с Роутинга (eng. “Routing” – маршрутизация). Сразу же появляется вопрос, зачем же нужна на сайте маршрутизация, если и раньше все хорошо работало и устраивало? Все просто. Если вы хотите получить более гибкую систему и уменьшить время на настройку/перенастройку сайта, то маршрутизация вам необходима. К тому же централизованное управление сайтом упростит работу с кодом.

Когда то я сам пользовался обычной switch-case маршрутизацией и думал, что это удобно (на самом деле даже такая маршрутизация куда лучше, чем ее отсутствие).

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

  • 1) маршрутизацию без кода поместить в отдельный файл (это позволит свободно редактировать файл прямо через админку)
  • 2) поместить логику отдельно в класс, который бы занимался генерацией контента для страниц

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

Определились «зачем?», перейдем к «как?».

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

  • RewriteEngine On
  • RewriteCond %{REQUEST_FILENAME} !-f
  • RewriteCond %{REQUEST_FILENAME} !-d
  • RewriteRule . * index. php [L]

Этот код перенаправит обработку всех страниц на index. php, что даст нам возможность упростить управление контентом. Для увеличения понимания советую обратить внимание на статью DarkKemper`a

Теперь, когда у нас есть перенаправление, перейдем к созданию файла-маршрутизатора. Я пользуюсь xml-форматом, но это не особо важно, главное, что бы вам было удобно.

  • packageName1. ClassName/MethodName
  • packageName2. ClassName/MethodName
  • packageName3. ClassName/MethodName
  • Все довольно просто: в файле-маршрутизаторе вы можете указывать настройки как для отдельных URL, так и для каких-либо статичных частей страниц. В примере я указал модули для шапки сайта, так как она у меня не меняется. Замечу, что кроме модулей, вы можете указывать настройки, какие-либо передаваемые параметры, правила… В общем все, что угодно.

    Последний и самый важный шаг – пишем класс Router, который и будет заведовать всей маршрутизацией.

    Для начала нам нужно разобрать xml-файл:

    • private static function parse($configPath) {
    • return simplexml_load_file($configPath);

    Метод parse() получает путь к вашему конфигу (можно использовать не только для маршрутизации) и возвращает SimpleXML object.

    Теперь вы можете обращаться к каким либо настройкам следующим образом: $xml->header->modules …

    • public static function GetContent($configPath) {
    • $content = "";
    • $router = self::parse($configPath);
    • foreach($router as $page) {
    • if(preg_match(
    • "#^" . $page->uri . "$#"
    • , str_replace($_SERVER["QUERY_STRING"], "", $_SERVER["REQUEST_URI"])
    • , $uriParts
    • if(! empty($page->modules->module)) {
    • foreach($page->modules->module as $modul) {
    • $pageModules = $modul;
    • //Modul::Load() занимается загрузкой пакетов из ...
    • $content . = Modul::Load($modul, $uriParts?: $uriParts);
    • //в своем конфиге я создал группу "page404", которая обрабатывается, если для данной страницы не заданы модули
    • if(empty($pageModules)) {
    • foreach($router->page404->modules->module as $modul) {
    • $pageModules = $modul;
    • $content . = Modul::Load($modul);
    • return $content;

    Итак, для начала мы парсим xml-файл, после чего обрабатываем регулярным выражением строку из .

    Если мы находим соответствие с URL текущей страницы, то подключаем модули, указанные в файле-маршрутизаторе. Если не находим – то подключаются модули 404 страницы.

    Ну, вот и все. Ничего сложного тут нет.

    Выводы можно сделать следующие:

    • + Система очень простая,
    • + Легко и быстро можно изменить страницы и разделы до неузнаваемости,
    • + Благодаря регулярным выражениям можно легко связать модули (мы можем передавать в них параметры).
    • - Если ваш файл-маршрутизатор большой, то без кэширования лучше не работать.

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

    Маршрутизация и URL-адреса

    Реализация MVC с помощью PHP в интернете может оказаться немного сложнее. Первая проблема связана с URL -маршрутизацией. URL -маршрутизация - это один из аспектов, которые не учитывались, когда разрабатывался MVC .

    Так какие возможности у нас есть для решения проблемы URL -маршрутизации? Одна из них заключается в предопределении всех URL -адресов, которые нужны веб-приложению. А затем в их сохранении в хранилище вместе с соответствующими данными, которые шаблоны «Модели », «Представления » и «Контроллера » загружают для каждой страницы или раздела.

    Затем система получает URL -адрес, запрошенный пользователем, и загружает компоненты, назначенные для этой страницы. Это вполне осуществимое решение, если вы создаете сайт-визитку или статический сайт, который не работает с динамическими URL . Например:

    array("model" => "AboutModel", "view" => "AboutView", "controller" => "AboutController"), "portfolio" => array("model" => "PortfolioModel", "view" => "PortfolioView", "controller" => "PortfolioController")); foreach($data as $key => $components){ if ($page == $key) { $model = $components["model"]; $view = $components["view"]; $controller = $components["controller"]; break; } } if (isset($model)) { $m = new $model(); $c = new $controller($model); $v = new $view($model); echo $v->output(); } }

    URL -адреса будут выглядеть следующим образом:

    example.com/index.php?page=about

    example.com/index.php?page=portfolio

    MVC PHP пример загружает конкретный набор «Модели », «Представления » и «Контроллера » для запрашиваемой страницы. Если параметр URL -страницы - это “about ”, то будет отображаться страница About . Если “portfolio ”, то будет отображаться страница Portfolio .

    Этот пример статической маршрутизации даже при таких простых настройках имеет несколько недостатков. Одним из них является тот факт, что масштабируемость проекта затруднена, поскольку объем сайта ограничен заданным массивом страниц.

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

    Мы помещаем ключ для каждого связанного элемента в массив с переменной URL -адреса, но взаимодействия с соответствующими классами уже предопределены; мы не могли сравнивать значения каждого ключа со статической маршрутизацией. Но почему бы нам не попробовать сделать это? Ну, для начала, нам не стоит жестко закреплять в коде каждый раздел системы.

    Можно создать разделы или страницы, создавая взаимосвязи между «Моделью », «Представлением » и «Контроллером ». Например:

    output(); }

    Наш новый URL -адрес будет выглядеть так:

    example.com/index.php?controller=controllername;model=modelname&view=viewname&action=actionname

    Текущая переменная URL -адреса сообщает системе, какую функцию нужно вызвать в «Контроллере ». Когда эта функция передает данные в «Модель », она пересылает и часть данных, которые указывают, какое именно «Представление » и «Контроллер » нужно загрузить.

    Это может быть переменная URL -адреса события, отдельная переменная или данные, собранные контроллером. Не забывайте, «Контроллер » никогда не должен загружать данные или непосредственно передавать их в «Представление »; он должен взаимодействовать только с «Моделью » и вводимыми пользователем данными.

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

    Однако при динамической маршрутизации «Контроллер » может получить больше функций, чем при статической. Динамическую маршрутизацию можно рассматривать как модификацию традиционной MVC архитектуры. Тем не менее, если она реализована правильно и эффективно, «Контроллер » может стать более важным элементом, чем при статической маршрутизации.

    Добавление Front Controller позволит вашей системе динамически загружать разделы, в зависимости от того, что вам нужно.

    DRY (Don’t Repeat Yourself) и шаблоны

    Для меня одним из главных аргументов в пользу использования MVC является возможность сделать всю систему как можно более организованной. Любой разработчик согласится, что самое худшее для любого приложения – это повторение одного и того же кода. Принцип поддержания простоты кода и многократного использования компонентов получила название философия DRY - Don’t Repeat Yourself (не повторяйтесь ).

    Принципы DRY гласят: "Каждый фрагмент информации должен быть представлен в системе единожды, однозначно и понятно ". Цель DRY - расширить и исследовать все возможные способы, доступные разработчикам, чтобы сделать систему динамичной и оптимизированной насколько это возможно. DRY подразумевает, что, если вам нужно написать один и тот же фрагмент кода во многих местах, то вместо повторения этого кода создайте отдельный метод и используйте его, где это необходимо.

    Такой подход делает систему более оптимизированной и предусматривает возможность кэширования для сокращения общего времени обработки.

    Корректная реализация DRY - это когда при изменении одного элемента системы несвязанные с ним элементы не меняются. Поэтому DRY так важен при разработке с использованием MVC паттерна.

    Шаблоны

    Слово "шаблон " может вызвать некоторые вопросы у тех, кто работал ранее с MVC веб-фреймворками, так как большинство людей отождествляют шаблон с «Представлением ». Как мы уже обсуждали ранее, это некорректно с точки зрения традиционной архитектуры MVC .

    В идеале, «Представление » должно обрабатывать данные после их получения от «Модели ». Поэтому имеет смысл организовать все так, чтобы компонент «Представление » только выбирал шаблон и передавал данные в этот шаблон.

    Таким образом, данные будут готовы для отображения с помощью структуры блочного кода, или с помощью echo , print . Главное помнить, что ваши данные должны быть готовы для вывода через шаблон. Если у вас в шаблоне данные обрабатываются по-другому, скорее всего, у вас неверно задана архитектура MVC .

    Вот простой пример того, как представление загружает шаблон и передает в него данные:

    tstring = "The string has been loaded through the template."; $this->template = "tpl/template.php"; } } controller = $controller; $this->model = $model; } public function output(){ $data = "

    " . $this->model->tstring ."

    "; require_once($this->model->template); } } The Template name

    Шаблон PHP MVC передается через «Модель », которая может назначать шаблон в зависимости от того, для чего предназначено каждое конкретное «Представление ». Этот метод шаблонов позволяет создавать эффективные и расширяемые системы, предоставляя возможность разделения back-end и front-end разработки. В этом и заключается основная цель MVC .

    Заключение

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

    Перевод статьи «The MVC Pattern and PHP. Part 2 » был подготовлен дружной командой проекта Сайтостроение от А до Я.