Чтобы понять роль контроллера в веб-приложении, надо попытаться выкинуть контроллер вообще. В системе Бикубик основная информация для страниц сайта хранится в таблице Страницы (Статьи). Роутер по УРЛ находит идентификатор записи для таблицы Страницы. Дальше кто-то должен по этому идентификатору получить данные, "одеть" их (обычно в HTML), добавить HEAD, HTTP заголовки и отправить браузеру.
Пытаемся сделать это напрямую, через шаблон. Данные получить легко – в начале Шаблона страницы (файл tpl_page.php) пишем:
Мы откуда-то знаем (записали в комментариях, например), что данные для Заголовка HTML-страницы хранятся в поле title, а основной текст страницы – в поле text. После этого несложно догадаться, что именно мы напишем в Шаблоне страницы дальше:
При такой структуре можно прямо в конце Роутера написать:
– и не нужен никакой специальный контроллер. Пробуем так же получить в браузер Главную страницу сайта (main.html) – и получаем пустое место. Потому что на Главной в этом сайте нет своего текста, а должен выводиться список анонсов одного из разделов сайта (раздела "События" – events.html). Возникает два вопроса: где это записать (особенность вывода главной страницы), и кто потом будет читать и реализовывать всю эту ветвистую логику приложения?
Вот отсюда и появляется понятие контроллера – триггера, выбирающего разные наборы "массив данных – схема отображения" на основании какой-то команды. Если речь идёт об особенностях конкретной страницы, значит команда эта должна храниться в данных, в той же записи, где и текст страницы. В Бикубике поле для этой информации об особой схеме отображения называется tpl (Шаблон) и содержит одно слово – наименование особого шаблона.
Тогда схема визуализации могла бы выглядеть так – в Роутере (для выделения в Контроллер ещё мало кода) пишем:
Ну, а файл anonce_events.php выглядит в точности как tpl_page.php, за исключением центральной части:
И это в корне неправильно. Так как при смене, например, doctype (или любых других общих для всех шаблонов частей) нам придётся вносить правку руками во множество разных файлов (шаблонов). Вот тут-то (когда мы захотим сделать правильно) и начнёт расти код Контроллера. Имя сущности, записанное в поле tpl, ведь можно отнести не к целому файлу шаблона, а, например, к наименованию функции, которая будет как-то править массив данных для шаблона:
Нарушается как попало вся система наименований: Tpl – это вроде бы шаблон, а функция в нём контроллеровская, создающая новую связку Данные – Представление (добавляющая к основному тексту страницы список анонсов раздела События). Ну, Tpl в данном случае – остаток традиции, более важная часть в наименовании здесь – "site", обращающая внимание на то, что файл хранится в папке конкретного сайта, а не в общей библиотеке. Правильнее пока было бы назвать файл (и класс) – Site.php, и хранить в нём разнородные функции и шаблоны, нужные только данному сайту.
Например, в общем наборе шаблонов Tpl.php есть функция Catalog_item, которая вызывается при формировании элемента Каталога товаров и возвращает готовый HTML код одного товара. Там есть картинка, описание товара, заголовок... Но вот на новом сайте понадобилось добавить кнопку "В корзину". Мы же не будем менять основной шаблон. Мы просто переопределим функцию Catalog_item в дочернем классе Tpl_site – изначально это и было его основным назначением. Теперь нам понадобилось добавить на сайт ряд функций другого назначения – не шаблонов низкого уровня, а "мелких контроллеров". Не создавать же ещё один файл Controller_site? Ну, то есть создадим, конечно, если функций накопится много (будем решать проблемы по мере поступления), а пока хватает и одного "уточняющего" файла для конкретизации задач сайта.
Двойственность, неоднозначность "локализующих" функций проявляется, если нам надо, например, поменять не входящие данные (например, основной текст страницы), а сайдбар (или вообще лэйаут), – то есть выбрать именно другой шаблон с другим набором информационных блоков. Мы должны будем не вызывать эту функцию, а указать полученное имя в строке include 'personal_tpl.php'.
Можно решить эту неоднозначность, вернув новый шаблон в качестве результата (и оставив за функцией право внутри ещё что-то там делать):
Ну, а персональный шаблон (у нас anonce_events_tpl.php) должен, конечно, не полностью заменять основной tpl_page.php, а быть его частью, то есть реализовывать что-то вроде наследования. Например, иногда в трёхколоночной вёрстке надо убирать одну из колонок, чтобы расширить область основного текста (например, для большой таблицы). Тогда в середине, на месте главного блока, должно быть не "echo $arr['text'];", а включение части:
А при определении особого шаблона нужно учитывать схему наследования:
То есть для заменяемой части изначально должен быть один шаблон по умолчанию, и разные шаблоны, поставляемые корректирующими функциями:
Здесь возникает потеря контроля за связкой "имя шаблона" – "значение, возвращаемое корректирующей функцией". Из функции мы можем вернуть несуществующий шаблон. Само имя функции мы проверяем перед вызовом (method_exists), да и вообще можем предложить список имён всех возможных функций пользователю в элементе select (выбрав их с помощью get_class_methods), так как это значение записывается в одно из полей таблицы Страницы. Можно объехать эту проблему на кривой козе, создав отдельный список соответствий корр. функций и имён шаблонов; а можно решить её, оставив как есть – используя для отладки сигнал "фатальная ошибка".
Некий контроллер Response выводит с помощью шаблона HTML страницу. Предположим, что контроллер в начале работы (1) готовит данные (получает их откуда-то волшебным образом сразу в виде HTML кода):
Подключаемый позже шаблон ничего не знает о данных, подготовленных контроллером. В шаблоне есть свой набор переменных, и шаблон может только надеяться, что они будут инициализированы где-то выше. Это первый минус "пассивности" (1.1 – риск "недостаточности"). Второй минус – противоположный: в одном из "подшаблонов" у нас две колонки в таблице вместо трёх, и попросту отсутствует место, где бы мы могли показывать погоду и курсы валют; но эти блоки всё равно готовятся в контроллере, на них тратятся ресурсы (1.2 – избыточность контроллера). Вместе эти две стороны одной медали можно назвать асимметрией.
Нам это не нравится, и мы переходим к "активным" действиям, начинаем получать HTML прямо внутри шаблона (2), в том блоке, которому этот HTML нужен:
И – ничего. Ничего нельзя сказать о такой схеме, пока не раскроем, что конкретно написано в функциях получения HTML. Здесь видно как минимум одно ветвление: 2.1) get_title() самостоятельно получает данные (и тогда ему надо где-то брать для этого параметры); 2.2) get_title() берёт готовые данные и извлекает оттуда значение поля title – тогда он должен знать, чьи данные (какой сущности) ему использовать.
Title тут, разумеется, для примера. И неудачно. Выбор способа получения данных как раз очень сильно зависит от сущности, а title достаточно сложный элемент, и его гораздо удобнее получать полностью "пассивно": завести "глобальное" поле и вносить туда правку на разных этапах работы с помощью функций, имеющих достаточно полномочий.
В последнем примере шаблона, как минимум, три значения получаются из (или видоизменяются от) одной записи одной таблицы (главной таблицы – Страницы): title, main_text, main_img. Тут уже тупая логика подсказывает, что такие данные должны получаться где-то раньше, а шаблон просто должен брать соответствующие поля (2.2.1).
Повторяющиеся по всему сайту телефон и email внизу страницы – это тоже данные, но хранятся они не в главной таблице, а где-нибудь в таблице Шаблонов (или в Настройках). И их уместно получать прямо там, где шаблон запросит (2.1.1). Иногда такие данные записывают прямо внутри шаблона, но тогда они перестают быть полноценными данными, так как рядовому пользователю (редактору сайта) править их уже становится затруднительно.
И вот любимое – Меню. Самый центр пересечения Данных и Представления. Вот где клокочут страсти и могуче встаёт вопрос, кто кого определяет, меню – часть шаблона, или это "бизнес-логика"? Здесь без конкретики даже начинать разговор бессмысленно. Поэтому говорим предметно: в CMS Бикубик в публичной части сайта Меню чаще всего генерируется из главной таблицы (Страницы), и важнейшим структурным параметром при его вызове является корневая страница, от которой меню строится. Это либо текущая страница, либо Главная (по умолчанию), либо произвольно указанная разработчиком сайта. Значение текущей страницы в Бикубике является глобальным полем и везде доступно, в частности, по адресу Def::$env['uri'].
В Бикубике ничто не мешает написать вызов меню (и получить полностью готовый HTML-код) в любом месте приложения, в том числе в самой середине шаблона; но нужно помнить при этом, что в шаблон внедряется логика приложения (или даже бизнес-логика), существенно влияющая на поведение пользователя.
Важная деталь – обработка на стороне клиента. Меню ведь может оказаться выпадающим, анимированным, и ему надо на стороне клиента назначать обработчик javascript. Об этом, на наш взглад, должен заботиться код, генерирующий меню (и другие сложные структуры): этот код добавляет в специальную копилку метку для обработки – javascript-маркер, а в самом конце шаблона, перез закрывающим тэгом body (о, как много туда всего рекомендуют помещать!) мы получаем список этих обработчиков и добавляем на страницу внутри элемента script. Подробнее эта техника описана в статье javascript_in_web.aspx
До сих пор мы обходили молчанием такой интересный вопрос: в какой же момент "голые" данные одеваются в HTML (3)? Как-то всё у нас по краям: получили данные, вставили в шаблон данные... а где середина? Она, конечно, бывает и прямо в данных (если разрешена вставка HTML): получили данные основного текста страницы – а они уже отформатированы, можно просто выдавать пользователю. Другой крайний случай – HTML запрещён, пропустили все данные сразу после получения через htmlspecialchars, и довольны, тоже можно выдавать пользователю.
Но вот то же меню... Или прайс-лист какой-нибудь. Таблица, в общем. Куча мелких данных, которые нужно оформить мелкими html-тэгами. Есть разные взгляды на эту проблему. Некоторые предлагают делать это прямо в шаблоне – запускать цикл foreach и внутри него ещё один foreach, для ячеек, – вот всё и генерируется прямо на глазах у изумлённого разработчика (пользователь-то не видит этого, так что не у него на глазах). Такой предельно "нативный" вариант (3.1).
С одной стороны, выглядит удобным: весь HTML собран в кучу в одном громадном файле (ну, в двух). Но, с другой стороны, – файл громадный. Даже не по килобайтам – он структурно громадный, сложный. Мы считаем это не очень удобным для глаз, и в Бикубике таблицы всё-таки генерируются не в шаблонах, а отдельными функциями (не "шаблонный" подход). Контроллер Response определяет по некоторым признакам (или берёт явно указанное значение из специального поля), нужна ли для вывода конкретных данных таблица, и генерирует таблицу с помощью класса View, передавая туда данные в качестве параметра (3.2).
Ну, меню, кажется, удобнее генерировать с помощью "компонента": в одном отдельном классе и получаются данные, и одеваются в HTML. Понятно, что компонент достаточно сложный. Туда ведь приходится передавать с помощью параметра и общую структуру меню – список это (ul) или таблица (table), или просто одна строка (div).
Эту проблему поточнее можно сформулировать так: при одном и том же УРЛ страница выглядит по-разному, в зависимости от каких-то других условий (сессия, куки, сервер, браузер...). Для авторизации это выглядит так: если пользователь не авторизован, он видит форму авторизации, а если всё хорошо, он видит своё имя и статус. Можно, конечно, извернуться, и прямо в шаблоне проверить сессию и подставить другой подходящий к случаю шаблон. Но в Бикубике, например, если пользователь не авторизован, он вообще не увидит на странице ничего, кроме формы. То есть основной шаблон должен быть определён раньше, чем мы начнём вывод какого-нибудь шаблона. То есть не в шаблоне.
Ещё более важным считаем мы отображение редактируемых областей: авторизованый пользователь, в публичной части сайта при наведении мыши должен видеть всплывающие ссылки для редактирования. Если это "компонентные" элементы – меню, каталог товаров – то очевидно, что такие ссылки должен добавлять "генератор" (проверяя авторизацию). Если это основной текст страницы... Ну, в Бикубике для него всегда есть какой-то генератор, по умолчанию – item, который просто транслирует данные "как есть", ну, и вот, добавляет при необходимости ссылку редактирования.
Хуже всего, если это данные, получаемые прямо из шаблона, как "нижний адрес" (см. 2.1.1, выше). В этом случае сам же шаблон и должен заботиться о "логике авторизации" и добавлять "условные ссылки". Там ведь в простом случае не нужна специальная функция, так как "шаблонные" данные доступны глобально по простым ссылкам вида Def::$blanks['bottom_addr']['value']. Ну, можно и придумать специально для такого случая функцию, какую-нибудь get_blanks('bottom_addr'). Этим сейчас и займёмся.