Меню

Представления log in и log out. Паттерн Model-Update-View и зависимые типы Корыстолюбивый logout html view

Комплектующие

В основном для разработки пользовательских интерфейсов. Что бы им воспользоваться надо создать тип Model, представляющий полное состояние программы, тип Message, описывающий события внешней среды, на которые программа должна реагировать, меняя свое состояние, функцию updater, которая из старого состояния и сообщения создает новое состояние прораммы и функции view, которая вычисляет по состоянию программы требуемые воздействия на внешнюю среду, которые порождают события типа Message. Паттерн очень удобный, но у него есть маленький недостаток - он не позволяет описать какие события имеют смысл для конкретных состояний программы.

Схожая проблема возникает (и решается) и при использовании ОО-паттерна State.

Язык Elm простой, но очень строгий - он проверяет, что функция updater хоть как-то обрабатывает все возможные сочетания модели-состояние и сообщения-события. По этому приходится писать лишний, пусть и тривиальный - как правило оставляющий модель без изменений, код. Я хочу продемонстрировать, как этого можно избежать в более сложных языках - Idris, Scala, C++ и Haskell.

Весь приведенный здесь код доступен на GitHub для экспериментов. Рассмотрим наиболее интересные места.


Функция msg необычна - она возвращает не значение, а тип. Во время исполнения про типы значений ни чего не известно - компилятор выполняет стирание всей лишней информации. То есть такая функция может быть вызвана только на этапе компиляции.

MUV - это конструктор. Он принимает параметры: model - начальное состояние программы, updater - функция обновления состояния при внешнем событии, и view - функция создания внешнего представления. Заметьте что тип функций updater и view зависит от значения модели (с помощью функции msg из параметров типа).

Теперь посмотрим, как это приложение запустить

MuvRun: (Application modelType msgType IO) -> IO a muvRun (MUV model updater view) = do msg <- view model muvRun (MUV (updater model msg) updater view)
В качестве внешнего представления (view) мы выбрали операцию ввода/вывода (в Idris, как и в Haskell, операции ввода/вывода - first class values, что бы они выполнились надо предпринять дополнительные действия, обычно вернуть такую операцию из функции main).

Кратко об IO

При выполнении операции типа (IO a) происходит некоторое воздействие на внешний мир, возможно пустое, и в программу возвращается значение типа a, но функции стандартной библиотеки устроены так, что обработать его можно только порождая новое значение типа IO b. Таким образом чистые функции отделены от функций с побочными эффектами. Это непривычно многим программистам, но помогает писать более надежный код.


Так как функция muvRun порождает ввод/вывод, она должна вернуть IO, но так как она ни когда не завершиться, тип операции может быть любой - IO a.

Теперь опишем типы сущностей, с которыми мы собираемся работать

Data Model = Logouted | Logined String data MsgOuted = Login String data MsgIned = Logout | Greet total msgType: Model -> Type msgType Logouted = MsgOuted msgType (Logined _) = MsgIned
Здесь описан тип модели, отражающий наличие двух состояний интерфейса - пользователь не залогинен, и залогинен пользователь с именем типа String.

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

И наконец функция, задающая соответствие значения модели типу сообщения.

Функция объявлена тотальной - то есть она не должна упасть или зависнуть, компилятор постарается за этим проследить. msgType вызывается на этапе компиляции, а значит ее тотальность означает, что компиляция не зависнит из-за нашей ошибки, хотя и не может гарантировать, что выполнение этой функции приведет к исчерпанию ресурсов системы.
Так же гарантировано, что она не выполнит «rm -rf /», потому что в ее сигнатуре нет IO.

Опишем updater:

Total updater: (m:Model) -> (msgType m) -> Model updater Logouted (Login name) = Logined name updater (Logined name) Logout = Logouted updater (Logined name) Greet = Logined name
Думаю логика этой функции понятна. Хочу еще раз отметить тотальность - она означает что компилятор Idris проверит, что мы рассмотрели все разрешенные системой типов альтернативы. Elm тоже осуществляет такую проверку, но он не может знать, что мы не можем разлогиниться, если еще не залогинены, и потребует явную обработку условия

Updater Logouted Logout = ???
Idris же в лишней проверки найдет несоотвествие типов.

Теперь приступим к view - как обычно в UI это будет самой сложной частью кода.

Total loginPage: IO MsgOuted loginPage = do putStr "Login: " map Login getLine total genMsg: String -> MsgIned genMsg "" = Logout genMsg _ = Greet total workPage: String -> IO MsgIned workPage name = do putStr ("Hello, " ++ name ++ "\n") putStr "Input empty string for logout or nonempty for greeting\n" map genMsg getLine total view: (m: Model) -> IO (msgType m) view Logouted = loginPage view (Logined name) = workPage name
view должна создавать операцию ввода/вывода, которая возвращает сообщения, тип которого снова зависит от значения модели. У нас есть два варианта: loginPage, который выводит сообщение «Login:», читает строку с клавиатуры и заворачивает ее в сообщение Login и workPage с параметром именем пользователя, который выводит приветсвие и возвращает различные сообщения (но одинакового типа - MsgIned) в зависимости от того, введет пользоваль пустую или не пустую строку. view возвращает одну из этих операций в зависимости от значения модели, и компилятор проверяет их тип, несмотря на то, что он разный.

Теперь мы можем создать и запустить наше приложение

App: Application Model Main.msgType IO app = MUV Logouted updater view main: IO () main = muvRun app
Здесь надо отметить тонкий момент - функция muvRun возврящает IO a , где a не было специфицировано, а значение main имеет тип IO () , где () - это имя типа, обычно называемого Unit , у которого есть единственное значение, тоже записываемое как пустой тупл () . Но компилятор с этим легко справляется. подставив вместо a ().

Scala и зависимые от пути типы

В Scala нет полноценной поддержки зависимых типов, но есть типы, зависимые от экземпляра объекта, через который на него ссылаются (path dependent types). В теории зависимых типов их можно описать как вариант сигма-типа. Зависимые от пути типы позволяют запретить складывать вектора из разных векторных пространств, или описать кому с кем можно целоваться . Но мы их применим для более простых задач.

Sealed abstract class MsgLogouted case class Login(name: String) extends MsgLogouted sealed abstract class MsgLogined case class Logout() extends MsgLogined case class Greet() extends MsgLogined abstract class View { def run() : Msg } sealed abstract class Model { type Message def view() : View } case class Logouted() extends Model { type Message = MsgLogouted override def view() : View .... } case class Logined(name: String) extends Model { type Message = MsgLogined override def view() : View .... }
Алгебраические типы в Scala моделируются через наследование. Типу соотвествует некоторый sealed abstract class , а каждому конструктору унаследованный от него case class . Мы будем стараться их использовать именно как алгебраические типы, описывая все переменные как принадлежащие к родительскому sealed abstract class .

Классы MsgLogined и MsgLogouted в рамках нашей программы не имеют общего предка. Функцию view пришлось размазать по разным классам модели, что бы иметь доступ к конкретному типу сообщений. В этом есть свои плюсы, которые оценят сторонники ОО - код получается сгруппирован в соотвествии с бизнес-логикой, все что связано с одним use case оказывается рядом. Но мне бы больше понравилось выделить view в отдельную функцию, разработку которой можно было бы передать другому человеку.

Теперь реализуем updater

Object Updater { def update(model: Model)(msg: model.Message) : Model = { model match { case Logouted() => msg match { case Login(name) => Logined(name) } case Logined(name) => msg match { case Logout() => Logouted() case Greet() => model } } } }
Здесь мы, используя зависимые от пути типы, описываем тип второго аргумента от значения первого. Что бы Scala воспринимала подобные зависимости, функции приходится описывать в карррированном виде, то есть в виде функции от первого аргумента, которая возвращает функцию от второго аргумента. К сожалению, Scala в этом месте не осуществляет многих проверок типов, для которых у компилятора достаточно информации.

Теперь дадим полную реализацию модели и view

Case class Logouted() extends Model { type Message = MsgLogouted override def view() : View = new View { override def run() = { println("Enter name ") val name = scala.io.StdIn.readLine() Login(name) } } } case class Logined(name: String) extends Model { type Message = MsgLogined override def view() : View = new View { override def run() = { println(s"Hello, $name") println("Empty string for logout, nonempy for greeting.") scala.io.StdIn.readLine() match { case "" => Logout() case _ => Greet() } } } } abstract class View { def run() : Msg } object Viewer { def view(model: Model): View = { model.view() } }
Тип возвращаемого функцией view зависит от экземпляра ее аргумента. Но за реализацией она обращается в модель.

Запускается созданное так приложение так

Object Main { import scala.annotation.tailrec @tailrec def process(m: Model) { val msg = Viewer.view(m).run() process(Updater.update(m)(msg)) } def main(args: Array) = { process(Logouted()) } }
Код runtime-системы, таким образом, ни чего не знает о внутреннем устройстве моделий и типах сообщений, но компилятор может проверить что сообщение подходит к текущей модели.

Здесь нам понадобились не все возможности, продоставляемые зависимыми от пути типами. Интересные свойства проявятся, если мы будем параллельно работать с неколькими экземплярами систем Model-Updater-View, например при симуляции многоагентного мира (view тогда бы представлял из себя воздействие агента на мир и получение обратной связи). В этом случае компилятор проверял, что сообщение обрабатывается именно тем агентом, для которого преднозначено, несмотря на то, что все агенты имеют одинаковый тип.

С++

С++ до сих пор чувствителен к порядку определений, даже если они все сделаны в одном файле. Это создает некоторые неудобства. Я буду приводить код в удобной для демонстрации идей последовательнсоти. Упорядоченную для компилируемости версию можно посмотреть на GitHub .

Алгебраические типы могут быть реализованы так же, как в Scala - абстрактный класс соответствует типу, а конкретные наследники - конструкторам (назовем их «классами-конструкторами», что бы не путать с обычными конструкторами C++) алгебраического типа.

В C++ есть поддержка зависимых от пути типов, но компилятор не может использовать этот тип абстрактно, не зная реального типа, с которым он связан. По этому реализовать Model-Updater-View с их помощью не получается.

Но C++ располагает мощной системой шаблонов. Зависимость типа от значения модели можно спрятав его в шаблонный параметр специализированной версии исполнительной системы.

Struct Processor { virtual const Processor *next() const = 0; }; template struct ProcessorImpl: public Processor { const CurModel * model; ProcessorImpl(const CurModel* m) : model(m) { }; const Processor *next() const { const View * view = model->view(); const typename CurModel::Message * msg = view->run(); delete view; const Model * newModel = msg->process(model); delete msg; return newModel->processor(); } };
Мы описываем абстрактную исполнительную систему, с единственным методом - выполнить все, что требуется, и вернуть новую исполнительную систему, подходящую для следующей итерации. Конкреная версия имеет шаблонный параметр и будет специализирована для каждого «класса-конструктора» модели. Здесь важно, что все свойства типа CurModel будут проверены во время специализации шаблона конкретным параметром-типом, а на момент компиляции самого шаблона их описывать не требуется (хотя и возможно с помощью концептов или других способов реализации классов типов). Scala тоже имеет достаточно мощную систему параметризованных типов, но проверки свойств типов-параметров она осуществляет во время компиляции параметризованного типа. Там реализация такого паттерна затруднена, но возможна, благодоря поддержке классов типов.

Опишем модель.

Struct Model { virtual ~Model() {}; virtual const Processor *processor() const = 0; }; struct Logined: public Model { struct Message { const virtual Model * process(const Logined * m) const = 0; virtual ~Message() {}; }; struct Logout: public Message { const Model * process(const Logined * m) const; }; struct Greet: public Message { const Model * process(const Logined * m) const; }; const std::string name; Logined(std::string lname) : name(lname) { }; struct LoginedView: public View { ... }; const View * view() const { return new LoginedView(name); }; const Processor *processor() const { return new ProcessorImpl(this); }; }; struct Logouted: public Model { struct Message { const virtual Model * process(const Logouted * m) const = 0; virtual ~Message() {}; }; struct Login: public Message { const std::string name; Login(std::string lname) : name(lname) { }; const Model * process(const Logouted * m) const; }; struct LogoutedView: public View { ... }; const View * view() const { return new LogoutedView(); }; const Processor *processor() const { return new ProcessorImpl(this); }; };
«Классы-конструкторы» модели «все свое носят с собой» - то есть содержат спициализированные для них классы сообщений и view, а так же умеют создавать исполнительную систему под себя. Собственные типы View имеют общего для всех моделей предка, что может оказаться полезно при разработке более сложных исполнительных систем. Принципиально что типы сообщений полностью изолированы и не имеют общего предка.

Реализация updater отделена от модели, поскольку требует что бы тип модели был уже полностью описан.

Const Model * Logouted::Login::process(const Logouted * m) const { delete m; return new Logined(name); }; const Model * Logined::Logout::process(const Logined * m) const { delete m; return new Logouted(); }; const Model * Logined::Greet::process(const Logined * m) const { return m; };
Теперь соберем вместе все, что относится к view, включая внутренние сущности моделей

Template struct View { virtual const Message * run() const = 0; virtual ~View() {}; }; struct Logined: public Model { struct LoginedView: public View { const std::string name; LoginedView(std::string lname) : name(lname) {}; virtual const Message * run() const { char buf; printf("Hello %s", name.c_str()); fgets(buf, 15, stdin); return (*buf == 0 || *buf == "\n" || *buf == "\r") ? static_cast(new Logout()) : static_cast(new Greet); }; }; const View * view() const { return new LoginedView(name); }; }; struct Logouted: public Model { struct LogoutedView: public View { virtual const Message * run() const { char buf; printf("Login: "); fgets(buf, 15, stdin); return new Login(buf); }; }; const View * view() const { return new LogoutedView(); }; };
И, наконец, напишем main

Int main(int argc, char ** argv) { const Processor * p = new ProcessorImpl(new Logouted()); while(true) { const Processor * pnew = p->next(); delete p; p = pnew; } return 0; }

И снова Scala, уже с классами типов

По структуре эта реализация почти полностью повторяет версию на C++.

Аналогичная часть кода

abstract class View { def run(): Message } abstract class Processor { def next(): Processor; } sealed abstract class Model { def processor(): Processor } sealed abstract class LoginedMessage case class Logout() extends LoginedMessage case class Greet() extends LoginedMessage case class Logined(val name: String) extends Model { override def processor(): Processor = new ProcessorImpl(this) } sealed abstract class LogoutedMessage case class Login(name: String) extends LogoutedMessage case class Logouted() extends Model { override def processor(): Processor = new ProcessorImpl(this) } object Main { import scala.annotation.tailrec @tailrec def process(p: Processor) { process(p.next()) } def main(args: Array) = { process(new ProcessorImpl(Logouted())) } }


А вот в реализации среды исполнения возникают тонкости.

Class ProcessorImpl(model: M)(implicit updater: (M, Message) => Model, view: M => View) extends Processor { def next(): Processor = { val v = view(model) val msg = v.run() val newModel = updater(model,msg) newModel.processor() } }
Здесь мы видим новые таинственные параметры (implicit updater: (M, Message) => Model, view: M => View) . Ключевое слово implicit означает что компилятор при вызове этой функции (точнее конструктора класса) будет искать в контексте помечанные как implicit объекты подходящих типов и передавать их в качестве соответствующих параметров. Это достаточно сложная концепция, одно их применений которой - реализация классов типов. Здесь они обещают компилятору, что для конкретных реализаций модели и сообщения все необходимые функции нами будут предоставлены. Теперь выполним это обещание.

Object updaters { implicit def logoutedUpdater(model: Logouted, msg: LogoutedMessage): Model = { (model, msg) match { case (Logouted(), Login(name)) => Logined(name) } } implicit def viewLogouted(model: Logouted) = new View { override def run() : LogoutedMessage = { println("Enter name ") val name = scala.io.StdIn.readLine() Login(name) } } implicit def loginedUpdater(model: Logined, msg: LoginedMessage): Model = { (model, msg) match { case (Logined(name), Logout()) => Logouted() case (Logined(name), Greet()) => model } } implicit def viewLogined(model: Logined) = new View { val name = model.name override def run() : LoginedMessage = { println(s"Hello, $name") println("Empty string for logout, nonempy for greeting.") scala.io.StdIn.readLine() match { case "" => Logout() case _ => Greet() } } } } import updaters._

Haskell

В мейнстримовом Haskell нет зависимых типов. В нем так же отсутствиет наследование, которое мы существенно применяли при реализации паттерна в Scala и C++. Но одноуровневое наследование (с элементами зависимых типов) может быть смоделировано с помощью более-менее стандандартных расширений языка -TypeFamilies и ExistentialQuantification. Для общего интерфейса дочерних ООП-классов заводится класс типов, в котором присутствует зависимый «семейный» тип, сами дочерние классы представляются отдельным типом, а потом заворачиваются в «экзистенциональный» тип с единственным конструктором.

Data Model = forall m. (Updatable m, Viewable m) => Model m class Updatable m where data Message m:: * update:: m -> (Message m) -> Model class (Updatable m) => Viewable m where view:: m -> (View (Message m)) data Logouted = Logouted data Logined = Logined String
Я попытался разнести updater и view как можно дальше, по этому создал два разных класса типов, но пока это плохо получилось.

Реализация updater проста

Instance Updatable Logouted where data Message Logouted = Login String update Logouted (Login name) = Model (Logined name) instance Updatable Logined where data Message Logined = Logout | Greeting update m Logout = Model Logouted update m Greeting = Model m
В качестве View пришлось зафиксировать IO. Попытки сделать его более абстрактным сильно все усложняли и повышали связанность кода - тип Model должен знать, какой именно View мы собираемся использовать.

Import System.IO type View a = IO a instance Viewable Logouted where view Logouted = do putStr "Login: " hFlush stdout fmap Login getLine instance Viewable Logined where view (Logined name) = do putStr $ "Hello " ++ name ++ "!\n" hFlush stdout l <- getLine pure $ if l == "" then Logout else Greeting
Ну и исполняемая среда мало отличается от аналогичной в Idris

RunMUV:: Model -> IO a runMUV (Model m) = do msg <- view m runMUV $ update m msg main:: IO () main = runMUV (Model Logouted)

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

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

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

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

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

1. Теория

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

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

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

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

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

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

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

1.1. Front Controller и Page Controller

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

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

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

Первый вариант:

  1. www.example.com/article.php?id=3
  2. www.example.com/user.php?id=4
Здесь каждый сценарий отвечает за выполнение определённой команды.

Второй вариант:

  1. www.example.com/index.php?article=3
  2. www.example.com/index.php?user=4
А здесь все обращения происходят в одном сценарии index.php .

Подход с множеством точек взаимодействия вы можете наблюдать на форумах с движком 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

Приблизительный код обработки в таком случае:
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
роутер выполнит следующие действия:

  1. подключит файл model_portfolio.php из папки models, содержащий класс Model_Portfolio;
  2. подключит файл controller_portfolio.php из папки controllers, содержащий класс Controller_Portfolio;
  3. создаст экземпляр класса 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 предназначен для формирования вида. В него передаются следующие параметры:

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

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

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

2.3. Реализация классов потомков Model и Controller, создание View"s

Теперь начинается самое интересное! Наш сайт-визитка будет состоять из следущих страниц:
  1. Главная
  2. Услуги
  3. Портфолио
  4. Контакты
  5. А также — страница «404»
Для каждой из страниц имеется свой контроллер из папки controllers и вид из папки views. Некоторые страницы могут использовать модель или модели из папки models.

На предыдущем рисунке отдельно выделен файл template_view.php — это шаблон, содержащий общую для всех страниц разметку. В простейшем случае он мог бы выглядеть так:
<html lang ="ru" > <head > <meta charset ="utf- 8 "> <title > Главнаяtitle > head > <body > $content_view ; ?> body > html >
Для придания сайту презентабельного вида сверстаем CSS шаблон и интегририруем его в наш сайт путем изменения структуры HTML-разметки и подключения CSS и JavaScript файлов:
<link rel ="stylesheet" type ="text/css" href ="/css/style.css" /> <script src ="/js/jquery-1.6.2.js" type ="text/javascript" > script >

В конце статьи, в разделе «Результат», приводится ссылка на 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 :
<h1 > Добро пожаловать!h1 > <p > <img src ="/images/office-small.jpg" align ="left" > <a href ="/" > ОЛОЛОША TEAMa > - команда первоклассных специалистов в области разработки веб-сайтов с многолетним опытом коллекционирования мексиканских масок, бронзовых и каменных статуй из Индии и Цейлона, барельефов и изваяний, созданных мастерами Экваториальной Африки пять-шесть веков назад... p >
Здесь содержиться простая разметка без каких либо PHP-вызовов.
Для отображения главной странички можно воспользоваться одним из следующих адресов:

  • методы библиотек, реализующих абстракицю данных. Например, методы библиотеки 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 .

    Портфолио

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


    Ссылка на 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. Подборка полезных ссылок по сабжу

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

    Django comes with a lot of built-in resources for the most common use cases of a Web application. The registration app is a very good example and a good thing about it is that the features can be used out-of-the-box.

    With the Django registration app you can take advantages of the following features:

    • Login
    • Logout
    • Sign up
    • Password reset

    In this tutorial we will focus in the Login and Logout features. For sign up and password reset, check the tutorials below:

    Getting started

    Before we start, make sure you have django.contrib.auth in your INSTALLED_APPS and the authentication middleware properly configured in the MIDDLEWARE_CLASSES settings.

    Both come already configured when you start a new Django project using the command startproject . So if you did not remove the initial configurations you should be all set up.

    In case you are starting a new project just to follow this tutorial, create a user using the command line just so we can test the login and logout pages.

    $ python manage.py createsuperuser

    In the end of this article I will provide the source code of the example with the minimal configuration.

    Configure the URL routes

    First import the django.contrib.auth.views module and add a URL route for the login and logout views:

    from django.conf.urls import url from django.contrib import admin from django.contrib.auth import views as auth_views urlpatterns = [ url (r"^login/$" , auth_views . login , name = "login" ), url (r"^logout/$" , auth_views . logout , name = "logout" ), url (r"^admin/" , admin . site . urls ), ]

    Create a login template

    By default, the django.contrib.auth.views.login view will try to render the registration/login.html template. So the basic configuration would be creating a folder named registration and place a login.html template inside.

    Following a minimal login template:

    {% extends "base.html" %} {% block title %}Login{% endblock %} {% block content %}

    Login

    {% csrf_token %} {{ form.as_p }} {% endblock %}

    This simple example already validates username and password and authenticate correctly the user.

    Customizing the login view

    There are a few parameters you can pass to the login view to make it fit your project. For example, if you want to store your login template somewhere else than registration/login.html you can pass the template name as a parameter:

    url (r"^login/$" , auth_views . login , { "template_name" : "core/login.html" }, name = "login" ),

    You can also pass a custom authentication form using the parameter authentication_form , incase you have implemented a custom user model.

    Now, a very important configuration is done in the settings.py file, which is the URL Django will redirect the user after a successful authentication.

    Inside the settings.py file add:

    LOGIN_REDIRECT_URL = "home"

    The value can be a hardcoded URL or a URL name. The default value for LOGIN_REDIRECT_URL is /accounts/profile/ .

    It is also important to note that Django will try to redirect the user to the next GET param.

    Setting up logout view

    After acessing the django.contrib.auth.views.logout view, Django will render the registration/logged_out.html template. In a similar way as we did in the login view, you can pass a different template like so:

    url (r"^logout/$" , auth_views . logout , { "template_name" : "logged_out.html" }, name = "logout" ),

    Usually I prefer to use the next_page parameter and redirect either to the homepage of my project or to the login page when it makes sense.

    This example demonstrates how to automatically logout with default Spring security configuration.

    To logout, we just need to access URL "/logout" with POST request.

    In the POST /logout form, we also need to include the CSRF token, which is a protection against CSRF attack .

    Let"s see the example how to do that.

    Java Config class

    @Configuration @EnableWebSecurity @EnableWebMvc @ComponentScan public class AppConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin(); } @Override public void configure(AuthenticationManagerBuilder builder) throws Exception { builder.inMemoryAuthentication() .withUser("joe") .password("123") .roles("ADMIN"); } @Bean public ViewResolver viewResolver() { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/views/"); viewResolver.setSuffix(".jsp"); return viewResolver; } }

    Note that, in above configuration, we are also overriding configure(HttpSecurity http) to omit the default Basic Authentication (see the original method in WebSecurityConfigurerAdapter source code) and use form based Authentication. We are doing so because browsers cache the Basic Authentication information aggressively (after the first successful login) and there is no way to logout the user in the current session. In most of the examples, we will not be using Basic Authentication mechanism.

    A controller

    @Controller public class ExampleController { @RequestMapping("/") public String handleRequest2(ModelMap map) { map.addAttribute("time", LocalDateTime.now().toString()); return "my-page"; } }

    The JSP page

    src/main/webapp/WEB-INF/views/my-page.jsp

    Spring Security Example

    Time: ${time}

    To try examples, run embedded tomcat (configured in pom.xml of example project below):

    Mvn tomcat7:run-war

    Output

    Initial access to URI "/" will redirect to "/login":

    After submitting user name and password as we setup in our AppConfig class:

    Clicking on "Logout" button:


    Example Project

    Dependencies and Technologies Used:

    • spring-security-web 4.2.3.RELEASE: spring-security-web.
    • spring-security-config 4.2.3.RELEASE: spring-security-config.
    • spring-webmvc 4.3.9.RELEASE: Spring Web MVC.
    • javax.servlet-api 3.1.0 Java Servlet API
    • JDK 1.8
    • Maven 3.3.9
    ГодПроектОписание
    " .$row ["Year" ]."" .$row ["Site" ]."" .$row ["Description" ]."