О системе DoIt CMS

О системе и преимущества

TODO:

  • before_filter гибкий
  • простота
  • переопределения
  • модульность, расширяемость
  • шаблоны с PHP
  • Active Record
  • Встроенная админка

Общие принципы

Принцип упрощения работы приложения

Основной принцип, используемый в системе, это принцип сохранения простоты работы.

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

Если существуют две различные сущности, например фильтры в шаблонизаторе ({page.title|uppercase}) и функции-помощники ({{input_tag 'title'}}), то они должны работать схожим образом, объявляться одинаково и использовать одинаковые возможности. Таким образом, человек, привыкший к работе с фильтрами, не будет иметь проблем с помощниками. Создавать для этого два различных класса и разбираться в работе каждого из противоречит этому принципу. Поэтому в системе нет папки classes, в которой расположены некие файлы Helpers.class.php и Filters.class.php.

При разработке сделана попытка объединить все возможные сущности в несколько общих. Это привело к тому, что в шаблонизаторе используется крайне малое количество тегов, а именно три: вызов функции, вывод переменной, цикл foreach. И в случае встречи с неким тегом {{edit}} пользователь не будет вспоминать значение этого тега, зная, что это вызов функции edit(). Поэтому эту функцию можно использовать как фильтр, как помощник, как вставляемую область, как шаблон страницы, как виджет, как контроллер страницы по определённому url, как правило валидатора и так далее.

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

MVC

Используются основные принципы MVC (разделение логики, данных, и функций, их соединяющих). Если точнее, то используется некое подобие паттерна HMVC. Реализация отличается от классического подхода.

Модель – это класс, который необязательно объявлять, но его экземпляры можно использовать. Он позволяет обращаться к базе данных как к объектам.

Контроллер – это множество функций, переопределяемых при помощи правил роутера. Также это помощники (helpers , фрагменты разметки, содержащих код), обработчики для форм (action('client#update')), альтернативные валидаторы, функции для отправки электронной почты и т.д.

Вид – это множество функций, объявляемых автоматически на основе файлов с разметкой, переопределяемых при помощи правил роутера. Может содержать PHP-код, например, для сложных проверок. Каждая такая функция возвращает готовый HTML-код. Например print d()->about(); выведет шаблон about.html.

Также есть несколько дополнительных сущностей:

Опции – .ini файлы, которые инициализируют переменные (свойства) основного объекта. Они же определяют правила роутера, списки полей для системы администрирования, опции сайта в целом, логин и пароль администратора, правила для валидации форм. При помощи опций можно переопределить любую функцию или шаблон, или заставить выполниться дополнительную функцию или дополнительный шаблон, объявить переменную или массив.

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

Помощники (Helpers) – функции, результат работы которых используется для генерации HTML кода (формы, поля редактирования, ссылки, метатеги и так далее). Часть контроллера, в большинстве своём независимые и не привязаны к какому-то определённому контроллеру. При этом они остаются обычными функциями.

Отличия от большинства MVC фреймворков

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

В отличие от большинства MVC фреймворков, модель не содержит общего кода (например, отправка электронной почты), и вся логика содержится в контроллере (антипаттерн fat stupid ugly controllers). Похожий подход используется в CodeIgniter. Более того, во многих проектах объявление модели может вовсе отсутствовать.

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

<ul>
    <foreach User->all as user>
        <li>{user.login}</li>
    </foreach>
</ul>

Здесь выводятся все записи из таблицы users. Примечание: данный пример усложняет в дальнейшем работу с кодом (например, добавление фильтраций или сортировки, переопределение и т.д.). Поэтому всё-таки лучше выводить подобный код в контроллер.

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

В отличие от большинства MVC фреймворков, допустимо использование методов контроллера напрямую в Виде (при помощи спец. тега, например, некая функция, получающая количество {{users#get_count_of_all}}), и вызов шаблонов Вида из Контроллера для дальнейшей обработки (например, замене спец. символов).

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

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

В отличие от большинства MVC фреймворков, реализация модели близка к классическим CMS, использует только MySQL базу данных и не имеет абстракций для работы с другими СУБД или иными источниками данных.

В отличие от большинства MVC фреймворков, роутер способен влиять на Вид, а также переопределять дополнительные функции-помощники (например, {{form}} или {{input}})

В отличие от большинства MVC фреймворков, каждый URL не имеет привязанного роутером контроллера. На одной странице может существовать несколько сущностей, гибко контролируемым независимо по правилам URL и разными контроллерами. Т.н. классические «виджеты» (например, форма отправки почтового сообщения или форма авторизации) работают по тем же принципам, что и основной контент, вызываются одинаково, также могут быть переопределены по правилам роутера и представляют собой одно и то же, в соответствии с принципом упрощения схемы работы приложения. Тоже самое касается фрагментов HTML-вёрстки (например, {{footer}}). Каждая из триад модель-вид-контроллер может вызывать другие триады, при этом роутер может влиять на этот выбор. Мы можем реализовать маленький виджет, состоящий из контроллера и вида и заставить роутеру управлять его внешним видом в зависимости от URL страницы. Сам виджет может быть запущен как из контроллеров, так из из шаблона. При этом принцип его работы не будет отличаться от реализации, допустим, контроллера содержимого страницы или контроллера сайта и его разметки («лайоута»).

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

Например, если существует функция

function summa() 
{
    return 2+2;
}

то следующее правило в роутере:

/mypage main summa

заставит по адресу http://сайт/mypage вывести число 4 вместо HTML кода всей разметки.

Переменные и функции

Две основные сущности это переменные и функции (а также методы классов-контроллеров).

Переменные

Переменная - свойство основного объекта. Используется для хранения глобальных объектов, свойств, выводятся в представлении.

Например:

d()->title = "Заголовок";
d()->text = "Полный текст";

Для того, чтобы вывести переменные, используется простая конструкция:

print d()->title;
print d()->text;

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

Например, их можно вывести в представлении:

<h1>{title}</h1>
<p>{text}</p>

Запись {var_name} в представлении преобразуется в следующий php код:

<?php print d()->var_name; ?>

Альтернативно можно использовать следующую конструкцию (как правило, бессмысленную):

print d('title');   

В такую переменную можно записать экземпляр объекта, например, указав в контроллере следующий код:

d()->user = d()->User->find(1);

После этого разрешено использовать следующую запись:

print d()->user->title;

Что в шаблонизаторе будет иметь следующий аналог:

Имя пользователя: <strong>{user.title}</strong>

Кроме объектов, в переменных можно хранить массивы:

d()->user=array('title'=>'vasya');
print d()->user['title'];

Что в шаблонизаторе будет иметь следующий аналог (аналогичный с предыдущим подходом):

Имя пользователя: <strong>{user.title}</strong>

Переменные можно объявлять и в ini-файлах. Например, можно создать файл options.init.ini и записать в нём следующие строки:

[user]
login=petya
password=12345

Обращаться к таким конструкциям можно как к массивам:

print d()->user['login'];

Что в шаблонизаторе будет иметь следующий аналог (аналогичный с предудыщим подходом):

Логин и пароль пользователя: <strong>{user.login} : {user.password}</strong>

Таким образом, один из подходов в шаблонизации следующий:

  1. Описывается и вызывается функция, которая подготавливает объекты и строки и помещает их в переменые.
  2. Дополнительно вызываются функции, которые дорабатывают эти переменные.
  3. Вызывается функция представления, которая выводит переменные в HTML-код.

При выводе переменных можно использовать фильтры, например:

Безопасный вывод: {user.name|h} обрезка тегов при помощи htmlspechialchars.
Вывод в верхнем регистре: {user.name|strtoupper} 

О фильтрах рассказано подробнее в соответствующем месте.

Функции

Функции - вторая основополагающая сущность. Все функции являются методами основного объекта (исключение - методы контроллеров). Все шаблоны, все функции контроллера, выполненные не в классах, все сниппеты, фильтры, помощники являются функциями и вызываются одинаково:

print d()->function_name();

Например, в файле index.php вызывается шаблон main.html следующим образом:

print d()->main();

Внутри шаблона main есть конструкция {{content}}, которая преобразуется в следующий PHP-код:

print d()->content();

Если нужная функция является представлением (html-файлом), то её можно вызвать так:

print d()->view('content');

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

class users_controller
{
    function show()
    {
        //Получение массива с данными от модели
        d()->users = d()->User->find(url(3));

        //Вывод шаблона
        print d()->view('users#show'); // Вызов d()->users_show_tpl();
    }
}

Для удобства программиста, функция d()->view() автоматически умеет определять имя представления. То есть предыдущий пример будет работать и так:

class users_controller
{
    function show()
    {
        d()->users = d()->User->find(url(3));
        print d()->view();
    }
}

В этом случае имя шаблона определяется именем контроллера (функции, шаблона, функции, или метода класса). В качестве параметра передаётся имя текущей выполняемой функции.

Это позволяет выдерживать общий стиль именования пар контроллерв и представлений в триадах и склеивает вид и контрллер триады в единое целое.

Альтернативный способ записи следующий:

<?php print d()->call('function_name' [, $массив_с_параметрами_функции]); ?>

Таким образом, запись {{function_name}} в шаблоне выведет в этом месте результат работы функции d()->function_name();

Чтобы объявить такую функцию, есть два способа:

Первый: объявить её в любом из подключаемых файлов. Например, создать файл my_functions.func.php и записать в нём:

function my_func()
{
    return "Привет, мир!";
}

После этого можно использовать функцию следующим образом:

Контроллер:

d()->title = d()->my_func();

Шаблон:

Приложение сообщает: <b>{title}</b>

Либо без контроллера:

Приложение сообщает: <b>{{my_func}}</b>

Ни одна из функций не производит вывод напрямую, даже если внутри их кода есть вызовы print или echo. Каждая функция возвращает результат, а программист по своему желанию либо выводит его, либо использует как параметр для другой функции или записывает в переменную.

Второй: создать файл с расширением .php, например, если создать файл с именем counter.php, со следующим содержимым:

d()->users_count = 300 * 2;
print d()->counter_tpl;

то конструкция {{counter}} выполнит php-код и, при необходимости, вызовет соотвествующий шаблон.

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

Этот подход удобен для «грязных» быстрых хаков, когда мы можем вставить в шаблон что то вроде {{sape}}, создать файл sape.php и вписать в него нужный нам код.

Разумеется, для запроса функции мы можем воспользоваться не только записью {{counter}}, но и использовать следующий код:

print d()->counter();

Третий: создать файл с расширением .html, например, если создать файл с именем banner.html, со следующим содержимым:

<div class="banner">Баннер!</div>   

то можно будет использовать следующую конструкцию:

print d()->banner();

Или выводить её в представлении:

<p>А дальше реклама:</p>
{{banner}}
<p>Реклама закончилась</p>

В .html файлах, как правило, хранятся шаблоны вида. В принципе, можно обойтись только ими, пренебрегая контроллером и используя внутри них прямые вставки php-кода, но это является плохим тоном и усложняет дальнейшую поддержку. Однако никто не запрещает пользоваться логикой представления, используя конструкции if(){}.

Так как такие функции остаются функциями, внутри них можно использовать return.

Например, если у нас есть файл get_summ.html со следующим содержимым:

<h2>Получение суммы</h2>
Дальше вычисляется сумма<br>
<?php
    return d()->first + d()->second;
?>
Конец<br>

то следующая конструкция:

d()->first=3;
d()->second=2;
print d()->get_summ();

выведет не весь шаблон, а только строку '5'. Это можно использовать, например, для прекращения вывода текущего шаблона (файл adminka.html):

<h2>Панель администрирования</h2>
<?php 
    if(!d()->is_admin()) {
        return " Вы являетесь неавторизованным пользователем";
    }
?>
<button>Кнопка удаления аккаунта</button>   

При попытке не авторизованного запуска фрагмента {{adminka}} не будет выведен заголовок в теге <h2> и кнопка, зато будет выведена запрещающая строка.

Также можно перенаправлять вывод другим функциям:

if(!d()->is_admin()) {
    return d()->registration_form();
}

Если описываемый файл расположен, например, в папке mod_adv, то скорее всего файл назван adv_banner.html и вызывается конструкцией {{adv_banner}}. В этом случае имя файла можно начать со знака подчёркивания, опустив префикс: _banner.html.

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

Например, в папке mod_clients могут быть расположены следующие шаблоны:

_show.html
_edit.html
_list.html
_new.html
_search.html

Вызываются они следующим образом:

print d()->clients_show()
print d()->clients_edit()
print d()->clients_list()
print d()->clients_new()
print d()->clients_search()

Или, в шаблонах:

{{clients_show}}
{{clients_edit}}
{{clients_list}}
{{clients_new}}
{{clients_search}}

Разумеется, если объявлена функция my_function(), то можно вызывать её в коде напрямую, опуская префикс d()->:

print my_function();

Но в таком случае перестаёт работать система переопределения функций на основе URL (роутинг).

Вызов функций

Во всех этих случаях сказано, что мы можем вызывать функции как таким способом:

<h1>{{my_function}}</h1>

так и таким:

print d()->my_function();

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

<h1>{page.title|my_function}</h1>

В качестве места для рендера:

<h1>{{{my_function}}}</h1>

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

В качестве основы для списка:

<foreach my_function() as element>
    <li>{element.title}</li>
</foreach>

Если в последнем случае «внезапно» предложить функцию на основе файла my_function.html (шаблона), то конструкция выведет отрендеренный шаблон, проигнорируя своё содержимое.

TODO: описать следующее

  • d()->user_controller->func()
  • {{admin#script}}
  • {{helper "mail"}}
  • {{helper "style"=>"color:red;"}}
  • main, content
  • Принцип переопределения
  • Схема работы приложения

Фрагменты

Объявление фрагмента

Для объявления фрагмента с именем fragmentname необходимо создать файл fragmentname.html в директории app.

Также можно объявить функцию (например, в файле filename.func.php в директории app), которая будет возвращать вывод фрагмента.

Поиск фрагмента

Механизм поиска нужного фрагмента для запуска довольно нетривиален.

  1. При попытке запуска функции ищется сама функция с этим именем.
  2. Если такая функция отсутствует, то выполняется функция, оканчивающая на _tpl.
  3. Если существует файл main.html, то объявится функция main_tpl().

Таким образом, для вставки фрагмента шаблона с именем footer, необходимо создать файл footer.html и вызвать {{footer}} там, где надо. Это сработает только в том случае, если функция footer() отсутствует (правило 2). Также можно использовать {{footer_tpl}}. Также можно создать файл footer.tpl.html, результат будет абсолютно тем же. Префикс .tpl всегда можно опускать.

Если необходимо использовать обычную функцию, просто объявляется функция.

Если необходимо использовать пару функция/функция-шаблон, то объявляется функция clients_show(), и шаблон clients_show.html Для вызова первой используется clients_show(), для второй clients_show_tpl().

(не реализовано) При вызове обычной функции, если шаблон существует, то он будет присоединён в цепочке к существующей по умолчанию.

Например, d()->main() при отсутствии функции main() выполнит d()->main_tpl();

Однако, если существует файл main.tpl.html, то объявится функция main_tpl().

Фрагмент по сути есть метод основного объекта системы, который возвращает HTML код либо промежуточные данные. При наличии файла fragmentname.html такая функция объявится автоматически.

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

Цепочки вызовов

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

Допустим, в роутере указана следующая строка

/ content default default_tpl

Это означает, что на страницах, начинащихся с / (всех) при вызове функции content выполнится d()->default(), а затем d()->default_tpl(). В первом файле может проводиться инициализация, а во втором - вывод. Можно вызвать безымянную цепочку напрямую:

d()->call('default', 'default_tpl');

Каждый элемент из цепочки не будет переопределён по правилам переопределения.

set_next_chain

d()->set_next_chain('название_функции')

Переопределяет следующий элемент в цепочке вызовов, определённой в роутере. Она переопределяет следующий элемент в очереди, или добавляет новый, если такого элемента нет.

например, d()->set_next_chain('fosv') вызовет d()->fosv() сразу после окончания работы текущей функции. Её вывод присоединится к выводу текущей функции.

stop_next_chains

d()->stop_next_chains() отменит все следующие функции в цепочке.

Вызов врагментов

Для вызова фрагмента и получения его вывода необходимо вызвать doit()->fragmentname(); Соответственно, для вывода <?php print doit()->fragmentname(); ?>

Внутри шаблонов можно использовать <?php print $this->fragmentname(); ?>. Все функции фрагментов не выводят данные, а только возвращают, это означает, что полученные данные всегда можно дополнительно обработать.

В контексте шаблона можно использовать короткую запись {{fragmentname}}.

В процессе работы сайта, как правило, первым вызывается фрагмент main(), который вызывает остальные (например, content для основного содержимого страницы). На комбинации фрагментов и стоится сайт, с учётом того, что фрагменты могут быть переопределены с учётом текущего URL (например, переопределён основной макет, середина страницы, заголовок (header.html), функция получения списка с данными и иные функции по желанию разработчика).

Если фрагмент или функция вернули return "текст", то вывод переопределится эти текстом.

ini-файлы и их применения

ini - файлы применяются для следующих случаев:

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

Стандартные функции

url

Функция url позволяет узнать, какие параметры есть в адресе текущей страницы

Пример использования:

Допустим, URL страницы /users/ainu/comments/13/14/52/page/4/edit?yes=no

print url() // users/ainu/comments/13/14/52/page/4/edit
print url(1) // users
print url(2) // ainu
print url('users') // ainu
print url('page') // 4
print url('comments',3) // 13/14/52
print url('comments',-2) // 13/14/52/page

Вызов классов

Для вызова нового класса используется тот же подход, что и при вызове объектов ActiveRecord.

Вот пример класса, умножающего на два:

class Multiply
{
    private $param;
    function __construct($params=array(1))
    {
        $this->param = $params[0];
    }
    function result()
    {
        return $this->param * 2 ;
    }

}

function main()
{
    print d()->Multiply(2)->result();
}

При этом в __construct передаётся массив параметров в виде одной переменной.

Также можно использовать запись

print d()->Multiply->result();

При этом имя класса, как и слово после d()-> должно начинаться с большой буквы.

Создаваемые классы не являются синглтонами, но Вы можете сделать их таковыми.

Если используется подобная запись, переопределения и цепочки функций будут проигнорированы.

Перенаправления

Испльзуя перенаправления, можно управлять выводом, симулируя открытие другого адреса

if($id=='83'){
    return d()->redirect('/auth');
}

Формы, валидаторы и действия (action)

TODO:

{{form}}
[validator.mail.title]
d()->action('form#send');

<content for banner>
&bull; Баннер &bull;

</content>

Два типа действий - с перезагрузкой и действием


comments powered by Disqus