MZZ.Framework 0.3.x: Документация
Разделы

Часть 1. Предисловие

1.1. Введение

Разнообразие современных требований к корпоративным веб-сайтам подтолкнуло к созданию не классической системы управления сайтом (CMS), обещающую недостижимый уровень гибкости, а к созданию фреймворка (Content Management Framework) -- инструментария для дальнейшей разработки и управления web-приложениями.

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

Код mzz написан полностью на языке PHP версии 5 в соответствии с парадигмой ООП (Объектно-ориентированное программирование), работает на популярных веб-серверах под Windows- или *nix-платформами и выпускается под лицензией LGPL.

Mzz разрабатывается методом TDD (Разработка через тестирование), что позволяет сделать код более чистым и иметь набор автоматических модульных тестов для него.

Собственный ORM (Object-Relational Mapping), разработанный на основе шаблона проектирования The Data Mapper Pattern, позволяет весьма быстро и удобно манипулировать уже имеющимися сущностями (доменными объектами) в системе и создавать новые.

Одной из основных особенностей mzz являются "вытягивающие" (pull) шаблоны: они сами запрашивают нужные данные. Для размещения, например, последних новостей (или вообще любого контента, который может быть отображен на сайте), потребуется дописать в шаблоне одну строку с загрузкой этого модуля в нужное место и шаблон сам запросит у модуля и вставит в это место нужные данные.
Наряду с этим Вы можете создавать неограниченное количество сайтов, физически расположенных на одном сервере, которые будут управляться одним экземпляром mzz.

Подведем итоги.

Что mzz может предложить:

Что mzz не предоставит:

1.2. Философия мзз

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

Этому способствует основной принцип, заложенный в mzz: "Приложением управляют шаблоны". Это значит, что за запуск и непосредственно сам процесс работы приложения в первую очередь отвечает шаблон. В процессе запуска приложения - это активный шаблон, в процессе работы - шаблоны модулей. Запуск конкретных модулей также производится непосредственно из шаблона с помощью функции load.

Тут вы наверяняка спросите: "А зачем это было сделано?". Как часто в уже работающем сайте вам приходилось на какую-либо из существующих страниц или её элементов (например "меню", "колонка" и т.д.) вывести результат работы одного из существующих модулей? Например: лидеры продаж магазина, последние посты форума или новости, баннер, ... Этот список можно продолжать долго. Для этого вам нужно было или править скрипты, которые отвечают за вывод этих страниц/элементов, добавлять в них вызов нужных модулей, передавать данные в шаблон, при этом следить, чтобы имена переменных одного модуля не пересекались с именами переменных другого модуля. Мы предлагаем другое решение - просто укажите в нужном месте в шаблоне запуск модуля с нужными параметрами. И всё! Модуль сам запустится в нужном для него окружении, т.е. запускаемый модуль даже не будет подозревать о том, кто и откуда его запустил. Это создаёт отличные перспективы для совмещения функционалов различных модулей: модуль "новости" + модуль "комментарии" == модуль "комментируемые новости". О процессе создания модулей можно прочитать в этом разделе.

В приложении, написанном с использованием mzz, всё является объектами. Из этого утверждения следует исходить, планируя схему БД, действия, производимые над объектами, систему авторизации.

Часть 2. Установка и настройка

2.1. Минимальные требования

Минимальные требования к программному обеспечению веб-сервера:

Перед первым запуском будет определена совместимость с ПО веб-сервера. Если mzz совместим, будет создан файл tmp/checked, блокирующий проверку при следующих запусках. В случае несоответствия будут отображены причины.

2.2. Установка на сервер

Если продукт был загружен с официального сайта mzz (http://www.mzz.ru), то перед установкой необходимо распаковать архив с исходным кодом на локальный сервер. В UNIX-подобной операционной системе для извлечения содержимого архива в корень веб-сервера (например, htdocs) используется следующий способ:

tar -xvzf <имя архива>.tar.gz -C htdocs/

После распаковки проверьте настройки в файле www/configs/config.php и, если требуется, измените их на ваши.

Если mzz установлен не в корень веб-сервера, то необходимо в SITE_PATH указать URL-путь.

Например, DocumentRoot в конфигурации веб-сервера Apache имеет значение c:\www, mzz установлен в c:\www\sites\mzz. Соответственно URL будет иметь примерно следующий вид: http://localhost/sites/mzz/www/. В таком случае SITE_PATH должен иметь значение /sites/mzz/www, кроме этого, в www/.htaccess изменятся некоторые директивы:

#...
RewriteBase /sites/mzz/www
#...
RewriteCond %{REQUEST_URI} !^/sites/mzz/www/?$
#...
RewriteRule (.*) index.php?path=/$1&%{QUERY_STRING} [L]

Следующий шаг: установка прав доступа на файлы и папки. С ними не все так просто, универсальных прав не существует. Они зависят от политики безопасности и настроек хостинг-провайдера. Для рабочего сайта права на запись необходимы только директории tmp/ и ее содержимому. Во время разработки проекта потребуются права на запись для директорий tests/tmp/, system/modules, www/modules и их содержимому. Обычно для этих папок могут подойти права 777 (rwxrwxrwx).

Остальным файлам и папкам требуются только права на чтение, чаще всего подойдут 644 (rw-r--r--) - для файлов, 755 (rwxr-xr-x) - для папок.

Заключительным шагом будет импорт таблиц и данных в MySQL, которые хранятся в файлах db/mzz.sql и db/mzz_test.sql (для тестов). Сделать это можно через phpmyadmin или консоль:

mysql < mzz.sql
mysql < mzz_test.sql

В процессе импорта удалятся существующие БД с именами "mzz" и "mzz_test". Для использования другого имени базы данных отредактируйте в /db/mzz.sql или /db/mzz_test.sql имя базы данных в запросах: DROP DATABASE, CREATE DATABASE, USE.

На этом установка завершена. Для проверки работоспособности mzz рекомендуется запустить тесты (в нашем примере по URL http://localhost/sites/mzz/tests/run.php).

2.3. Конфигурация

2.3.1. Системная конфигурация проекта

Конфигурация mzz начинается с файла config.php в папке configs проекта. При необходимости можно изменить необходимые опции.

Описание опций:

SITE_PATH = null
Абсолютный путь до сайта.
SYSTEM_PATH = ../../system/
Путь до mzz. Возможно указание как относительного, так и абсолютного пути
DEBUG_MODE = true
Включение/отключение debug-режима. Возможные варианты: true или false. Если указано 'true', то ошибки интерпретатора и внутренние ошибки mzz будут отображены непосредственно в браузер.

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

MZZ_USER_GUEST_ID = 1
Идентификатор записи в Базе Данных для неавторизированных пользователей. Изменение требуется при наличии базы данных в которой уже есть пользователь с идентификатором установленным по умолчанию.

Пользователь с указанным идентификатором в константе MZZ_USER_GUEST_ID должен существовать

COOKIE_DOMAIN = null
Домен, которому доступны все устанавливаемые приложением cookie
MZZ_ROOT_GID = 3
Идентификатор группы, для которой ACL всегда будет возвращать true (т.е. предоставит полный доступ)

systemConfig::$i18n = ru
Язык приложения по умолчанию (используется если включен i18n)
systemConfig::$i18nEnable = true
Включение поддержки нескольких языков (i18n)
systemConfig::$db['default']['driver'] = PDO
Драйвер для работы с БД.
systemConfig::$db['default']['dsn'] = mysql:host=localhost;dbname=mzz
DSN, содержит необходимую информацию о базе данных. Более подробно в разделе [todo]
systemConfig::$db['default']['user'] = root
Имя пользователя для доступа к БД, указанной в DSN
systemConfig::$db['default']['password'] = null
Пароль для доступа к БД, указанной в DSN
systemConfig::$db['default']['charset'] = utf8
Кодировка БД. После успешного соединения с БД выполняется запрос: SET NAMES `кодировка`
systemConfig::$db['default']['pdoOptions'] = array()
Дополнительные опции соединения с БД для PDO. Более подробная информация доступна в руководстве по PHP

2.3.2. Настройки для http-сервера Apache

Для функционирования mzz необходимо определить некоторые директивы (настройки) для веб-сервера Apache в файле www/.htaccess

По умолчанию его содержание следующее:

AddDefaultCharset utf-8
 
RewriteEngine on
RewriteBase /
 
Options +FollowSymlinks -Indexes -Includes -MultiViews
 
# rules for media-files urls rewriting
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^templates/css/(.*\.css) templates/external.php?type=css&files=$1 [L]
 
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^templates/images/(.*\.(gif|png|jpg)) templates/external.php?type=$2&files=$1 [L]
 
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^templates/js/(.*\.js) templates/external.php?type=js&files=$1 [L]
 
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^templates/js/(.*\.([^.]*)) templates/external.php?type=$2&files=$1 [L]
 
RewriteCond %{SCRIPT_FILENAME} !-f
#Uncomment the next line if you don't want to rewrite exists folders
#RewriteCond %{SCRIPT_FILENAME} !-d
 
RewriteRule (.*) index.php?path=/$1&%{QUERY_STRING} [L]
 
# If magic_quotes enabled in your php.ini and you can't disable it then uncomment the next lines:
#<IfModule mod_php5.c>
#    php_flag magic_quotes_gpc 0
#    php_flag magic_quotes_runtime 0
#</IfModule>
 
<IfModule mod_expires.c>
    ExpiresActive on
    ExpiresByType application/x-javascript "access plus 3 day"
    ExpiresByType text/css "access plus 3 day"
    ExpiresByType image/gif "access plus 5 day"
    ExpiresByType image/jpeg "access plus 5 day"
    ExpiresByType image/png "access plus 5 day"
</IfModule>

Рассмотрим основные директивы, которые, возможно, потребуется изменить.

Кроме того, необходимо отключить (если они включены) PHP-опции magic_quotes_gpc и magic_quotes_runtime в php.ini. Если у вас нет доступа к этому файлу и php установлен как apache-модуль, можно попробовать отключить их через .htaccess добавив следующий код.

<IfModule mod_php5.c>
    php_flag magic_quotes_gpc 0
    php_flag magic_quotes_runtime 0
</IfModule>

Более подробную информацию о .htaccess и его директивах можно найти в руководстве Apache.

Часть 3. Структура mzz

3.1. Шаблоны

3.1.1. Общие сведения

Шаблоны можно считать View-составляющей парадигмы MVC, на основе которой построен mzz.

В качестве обработчика шаблонов используется Smarty.

В mzz образно все шаблоны делятся на 2 вида: "активные" и "пассивные". Первые служат своеобразной точкой входа в приложение. Запуском "активного" шаблона занимается core.

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

Так как активные шаблоны в большинстве случаев отличаются только значениями параметров, существует несколько активных шаблонов по умолчанию. Вы можете указать активный шаблон в конфигурации действий модуля с помощью параметра main. По умолчанию существуют следующие активные шаблоны: active.main.tpl, active.blank.tpl, active.admin.tpl. Первый отобразит результат в шаблоне для публичной части, следующие без дополнительного шаблона и в шаблоне для административной части. Кроме того, вы можете запретить вызов действия напрямую через URL указав main="deny". Таким образом, когда указан параметр main, необходимости в создании отдельного активного шаблона нет.

"Активные" шаблоны располагаются в файлах <проект>/templates/act/<модуль>/<действие>.tpl. "Пассивные" шаблоны располагаются в system/modules/<модуль>/templates/<действие>.tpl и могут быть переопределены в <проект>/templates/<модуль>/<действие>.tpl

3.1.2. Плагин {load}

Функция {load} предназначена для запуска модулей из шаблонов. Этот метод является реализацией некоей "стратегии вытягивания", в которой шаблоны (по сути клиентский код) сами определяют, какие данные нужно получить и отобразить пользователю. За счёт этого достигается удобство и гибкость приложений, использующих данный принцип.

Синтаксис функции:

{load module="" action="" section="" <имя>="значение" ...}

Описание основных аргументов:

Пример 1. Выполнение действия "list" модуля "Новости" в текущей секции:

{load module="news" action="list"}

Пример 2. Отображение новости с ID 15 в секции "mainNews":

{load module="news" action="view" id="15" section="mainNews"}

Дополнительно у {load} есть два аргумента: 403tpl и 403header. Первый определяет имя шаблон, отображаемого в случае, если прав нет (по умолчанию используется simple403Controller), а второй -- нужно ли выдавать HTTP-ответ 403 Forbidden, если прав нет. Смотрите также раздел о взаимодействие системы проверки прав и шаблонов.

3.1.3. Плагин {add}

Функция {add} является простым способом включить в шаблон JS и CSS файлы. Если файл уже был включен, второй раз он подключен не будет. Результатом этих функций являются HTML-теги <script> или <style> (в зависимости от типа подключаемого файла).

Синтаксис функции:

{add file="имя файла"[ tpl="имя шаблона"]}

Описание аргументов:

- file: имя подключаемого файла;
- tpl: имя шаблона, определяющий как будет выглядеть результат (по умолчанию это в зависимости от расширения файла js.tpl или css.tpl)

Пример 1. Подключение CSS и JS файла:

{add file="style.css"}
{add file="style.js"}

Пример 2. Подключение JS файла, используя специальный шаблон "some_template.tpl":

{add file="script.js" tpl="some_template.tpl"}

Для вывода результата в необходимом месте шаблона (например, header.tpl) размещается код, который включит в шаблон все добавленные ранее файлы:

...
<head>
<title>mzz</title>
{include file='include.css.tpl'}
{include file='include.js.tpl'}
</head>
...

Также в mzz имеется возможность включения «склеенных» файлов. Благодаря этому для получения нескольких JS или CSS файлов будет выполнен лишь один запрос к web-серверу, что благоприятно скажется на работоспособности. Для этого достаточно просто изменить шаблон вывода следующим образом:

...
<head>
<title>mzz</title>
{include file='include.external.css.tpl'}
{include file='include.external.js.tpl'}
</head>
...

Тогда шаблон вида

{add file="file1.js"}
{add file="file2.js"}
{add file="file3.js"}
{add file="file4.css"}
{add file="file5.css"}

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

<link rel="stylesheet" type="text/css" href="/templates/external.php?type=css&files=file4.css,file5.css" />
<script type="text/javascript" src="/templates/external.php?type=js&files=file1.js,file2.js,file3.js"></script>

Следует отметить, что ввиду некоторых обстоятельств, не все JS и CSS файлы будут работать корректно в «склееном» виде. Поэтому существует возможность явно запретить файлу участвовать в склеивании. Для этого достаточно передать параметр join=false к аргументам функции:

{add file="file1.js" join=false}

3.1.4. Плагин {url}

Для вставки адреса (URL) в шаблон можно использовать функцию {url}. Результатом работы этой функции является сгенерированный URL, который содержит:

- текущий протокол (HTTP/HTTPS);
- адрес HTTP-хоста (HTTP_HOST);
- порт сервера, если он отличается от стандартного "80";
- дополнительный путь, который указан в конфигурационной константе SITE_PATH;
- используемый язык, если включена мультиязычность в Routes;
- секция;
- параметры, если они были указаны;
- действие.

Для получения текущего URL вызывается {url} без каких-либо аргументов.

Синтаксис функции:

{url module="модуль" section="секция" action="действие" route="имя"}

Описание аргументов:

- module: имя модуля, если секция не указана по нему будет определена секция;
- section: имя секции, на которую будет ссылаться URL. Если не указана, будет использована текущая или, если указан модуль, секция модуля;
- action: действие для указанного section;
- route: имя правила для маршрутизации сборки URL.

Все аргументы являются не обязательными.

Функция может принимать так же и любые другие аргументы, которые будут являться параметрами. Примером генерации http://example.com/news/4/asc/edit в соответствии с правилом маршрутизации :section/:id/:sort/:action является:

{url section="news" action="edit" id="4" sort="asc" route="newsList"}

Пример генерации URL для редактирования объекта с ID 4 в секции "news" (http://example.com/news/4/edit):

{url section="news" action="edit" id="4"}

Функция генерирует только URL. Например, сделать ее ссылкой можно следующим образом: <a href="{url}">link</a>

Иногда возникает ситуация, когда требуется также передать и обычные GET параметры. Сделать это можно так:

<a href="{url _param="value" _param2="value2"}>link</a>

Параметр appendGet определяет надо ли добавить в URL текущие GET-параметры или исключить их из URL.

3.1.5. Плагин {title}

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

Примеры использования для добавления части заголовка:

{title append="Новости" separator=" - "}
{title append="2009"}
{title append="Список"}

Примеры вывода заголовка:

{title separator=" | "}
{title separator=" | " end=" - "}
{title separator=" | " start=" - "}

Соответствует следующий результатам:

Новости - 2009 | Список
Новости - 2009 | Список -
- Новости - 2009 | Список

Значения аргументов end или start присоединяются к результату только при выводе заголовка и если он не пустой (основное применение этих аргументов: разделитель для названия сайта и цепочки заголовков).

3.1.6. Плагин {meta}

Плагин для установки meta информации (ключевых слов и описания страницы в <meta> тегах).

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

{keywords show="description" default="информационный сайт"}
{keywords keywords="новости, просмотр, в мире"}
{keywords description="просмотр новостей в мире"}
{keywords show="keywords"}
{keywords show="description" default="информационный сайт"}

Результатом выполнения этого кода будет:

информационный сайт
новости, просмотр, в мире
просмотр новостей в мире

Эти плагины можно использовать для указания content атрибута для соответствующих meta-тегов.

3.1.7. Плагин {icon}

Плагин для генерации "системных" иконок.

Синтаксис функции:

{icon sprite="описание sprite'а" [jip=true] [active=true|disabled=true]}

Описание аргументов:

Формат строки спрайта:

sprite:name/index[/overlay]

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

{icon sprite="/templates/images/icon.gif"}
    {icon sprite="sprite:mzz-icon/mzz-icon-folder/mzz-overlay-add"}
    {icon sprite="sprite:mzz-icon/mzz-icon-folder/mzz-overlay-add" active=true}
    {icon sprite="sprite:mzz-icon/mzz-icon-folder/mzz-overlay-add" disabled=true}
    {icon sprite="sprite:mzz-icon/mzz-icon-folder/mzz-overlay-add" jip=true}

Результатом выполнения этого кода будет:

<img src="/templates/images/icon.gif" width="16" height="16" alt="icon" />
<span class="mzz-icon mzz-icon-folder"><span class="mzz-overlay mzz-overlay-add"></span></span>
<span class="mzz-icon mzz-icon-folder active"><span class="mzz-overlay mzz-overlay-add"></span></span>
<span class="mzz-icon mzz-icon-folder disabled"><span class="mzz-overlay mzz-overlay-add"></span></span>
{'sprite':'mzz-icon','index':'mzz-icon-folder', 'overlay':'mzz-overlay-add'}

Этот плагин используется для генерации соответсвующего html-кода или строки для jipMenu. Пока генерит иконки 16х16.

3.2. Контроллеры

3.2.1. simpleController

Базовый контроллер, от которого наследуются все контроллеры приложения. При создании объекта этого класса в защищённые (protected) свойства инициализируется ряд системных объектов, необходимых для работы конечных контроллеров.

3.2.2. simple403Controller

Данный контроллер запускается в случае, когда не хватает прав доступа на запуск действия. К моменту написания главы его реализация выглядела следующим образом:

<?php
 
class simple403Controller extends simpleController
{
    public function getView()
    {
        $module = 'page';
        $action = 'view';
        $name = '403';
 
        if ($this->request->getModule() == $module
        && $this->request->getString('name') == $name
        && $this->request->getAction() == $action) {
            throw new mzzRuntimeException('Recursion detected: the 403 controller was called twice.');
        }
 
        $header = $this->request->getBoolean('403header');
        $this->request->setModule($module);
        $this->request->setParams(array('name' => $name));
        $this->request->setAction($action);
 
        if ($header) {
            $this->response->setStatus(403);
        }
 
        return $this->forward($module, $action);
    }
}
 
?>

Как видно из кода, устанавливались значения, обеспечивающие запуск страницы 403 вместо вызываемого модуля. А также отправка соответствующих заголовков пользователю.

3.2.3. simple404Controller

Данный контроллер запускается в случае, когда запрашиваемый объект не найден:

<?php
class simple404Controller extends simpleController
{
    /**
     * Свойство, определяющее - отправлять контент или только заголовки
     *
     * @var boolean
     */
    protected $onlyHeaders;
 
    /**
     * Результат работы контроллера, обрабатывающего 404 ошибку конкретных ДО
     *
     * @var string
     */
    private $result;
 
    /**
     * Конструктор
     *
     * @param boolean $onlyHeaders
     */
    public function __construct($onlyHeaders = false)
    {
        parent::__construct();
        $this->onlyHeaders = (bool)$onlyHeaders;
    }
 
    /**
     * Установка результата работы 404 контроллера для определенного маппера
     *
     * @param simpleMapper $mapper
     */
    public function applyMapper(mapper $mapper)
    {
        $this->result = $this->forward404($mapper);
    }
 
    protected function getView()
    {
        $this->response->setStatus(404);
 
        if ($this->result) {
            return $this->result;
        }
 
        $module = 'page';
        $action = 'view';
        $name = '404';
 
        if ($this->request->getModule() == $module && $this->request->getString('name') == $name && $this->request->getAction() == $action) {
            throw new mzzRuntimeException('Recursion detected: the 404 controller was called twice.');
        }
 
        $this->request->setModule($module);
        $this->request->setParams(array('name' => $name));
        $this->request->setAction($action);
 
        return $this->onlyHeaders ? false : $this->forward($module, $action);
    }
}
 
?>

Как видно из кода, устанавливались значения, обеспечивающие запуск страницы 404 вместо вызываемого модуля. А также отправка соответствующих заголовков пользователю.

3.2.4. messageController

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

$controller = new messageController('Запрашиваемая новость не найдена', messageController::INFO);
return $controller->run();

Вторым аргументом может быть передан тип выводимого сообщения. По умолчанию messageController::WARNING.
Доступны следующие типы:

3.2.5. Передача управления другому контроллеру

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

$this->forward('news', 'list');

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

$this->forward404($newsMapper);

Аргумент $newsMapper необязательный, но он определяет класс, при поиске объекта которого была получена данная ошибка. Если для этого класса определён свой контроллер-обработчик, то будет вызван именно он. В противном случае - simple404Controller. Аналогичным методом передаётся управление через метод forward403.

3.2.6. Переадресация

Переадресация в контроллерах осуществляется с помощью метода redirect:

$this->redirect($url); // в переменной $url хранится адрес, на который нужно перейти

3.3. Основные системные классы

3.3.1. toolkit

Тулкит предназначен для того, чтобы получать необходимыe для работы экземпляры классов. Тулкит является реализацией The Composite Pattern и The Registry Pattern. И в свою очередь его можно назвать своеобразным глобальным хранилищем для различных объектов. В стандартной поставке в состав тулкита (конкретнее - класс stdToolkit) входят следующие методы:

Более подробную информацию о методах можно посмотреть в тестах, классе stdToolkit.

Для получения инстанции тулкита необходимо воспользоваться следующей конструкцией:

$toolkit = systemToolkit::getInstance();

3.3.2. httpRequest

К GET, POST, COOKIE и к параметрам в пути необходимо обращаться через класс httpRequest. В приложении существует только один объект этого класса, который может быть получен из Toolkit следующим образом:

$request = $toolkit->getRequest();

Для получения значения сушествует несколько методов, которые отличаются типом значения, которое будет получено из запроса.

<?php
// Правило маршрутизации: :section/:action
// URL: http://example.com/demo/view?show_execute_time=1
$request->getString('section'); // demo
$request->getString('action', SC_PATH); // view
$request->getBoolean('action', SC_PATH); // true
$request->getBoolean('show_execute_time', SC_GET); // true
?>

Доступные методы:

Имя Описание
getString() string (строка)
getInteger() integer (целое число)
getNumeric() numeric (любое число)
getArray() array (массив)
getBoolean() boolean (true или false)
getRaw() любой тип (значение как есть)

Если значение является массивом, а указанный тип не 'array', то из массива будет получен первый элемент, который и будет приведен к нужному типу. Если он тоже окажется массивом, результат будет null.

Каждый метод может принимать два аргумента: имя и источник данных. По умолчанию источник данных SC_PATH.

httpRequest может получать данные из следующих источников данных:

Константа Описание
SC_GET массив $_GET (при GET-запросе)
SC_POST массив $_POST (при POST-запросе)
SC_REQUEST массив $_GET или $_POST (приоритет имеет $_POST)
SC_COOKIE массив $_COOKIE
SC_SERVER массив $_SERVER
SC_FILES массив $_FILES
SC_PATH результат обработки запрошенного пути объектом класса requestRouter

Из объекта класса httpRequest могут быть получены такие данные, как: запрошенный URL, текущее действие, секция, принят ли запрос средствами AJAX и др. Описание всех методов можно найти в API-документации.

3.3.3. httpResponse

Класс httpResponse предназначен для хранения данных, отсылаемых клиенту в качестве ответа. К таким данным относятся куки, заголовки и непосредственно сам ответ. Отправка данных производится автоматически в конце, после выполнения всего кода.

Для установки кук используется метод setCookie(), с синтаксисом, сходным с синтаксисом нативной функции php setcookie. Типичный пример установки кук:

$response->setCookie('name', 'value', time() + 3600 * 24, '/');

В результате выполнения этого кода в куку с именем name установится значение value на 1 сутки и с доступом на весь текущий домен.

Отправка заголовков производится с помощью метода setHeader(). Первым аргументом идёт имя заголовка, вторым - значение. В случаях, когда заголовок не содержит как такового имени (например, статусный ответ 404), аргумент с именем следует оставить пустым или установить в null. Примеры:

$response->setHeader('', 'HTTP/1.x 404 Not Found');
$response->setHeader('Location', $url);

Для добавления к текущему ответу даных, используется метод append(). Его аргументом является строка, добавляемая к уже установленным данным. Для очистки ответа необходимо использовать метод clear().

$response->append('Sample response text');

3.3.4. Routers

Маршрутизация (Routing) - это процесс разделения запрошенного URL на ассоциативный массив с помощью правила (route) при совпадении пути из URL с ним. Правила маршрутизации хранятся в файле <project_folder>/configs/routes.php.

Доступ к результату возможен через объект httpRequest с указанием в качестве второго аргумента SC_PATH.

Пример простейшего правила:

$router->addRoute('nameOfRule', new requestRoute('/ru/:section/:action'));

Перебор для поиска подходящего правил осуществляется в обратном порядке.

Где nameOfRule - уникальное имя для правила, /ru/:section/:action - шаблон. Шаблон может содержать разделитель / (прямой слэш), placeholder и raw-текст. В нашем примере :section и :action - placeholder, ru - raw-текст. Запрашиваемый URL example.com/ru/foo/bar совпадет с этим шаблоном и выполнится декомпозиция пути: в httpRequest будет помещен ассоциативный массив, где ключи - имена placeholder: section и action, значения - foo и bar соответственно. Raw-текст при этом сохранен не будет, он учитывается только в процессе проверки на совпадение.

Имя placeholder должно состоять только из латинских букв и знака подчеркивания ("_").

Таким образом в httpRequest будет помещен массив:

array([section] => foo, [action] = bar)

Любой placeholder может иметь значение по умолчанию, которые указываются во втором аргументе. Существовать этому placeholder в шаблоне необязательно (в примере ниже указано значение по умолчанию для placeholder-а id, но в самом шаблоне его нет).

$router->addRoute('nameOfRule', new requestRoute('/:section/:action', array('section' => 'news', 'action' => 'list', 'id' => 1)));

Данное правило совпадет со следующими URL: example.com/news/list, example.com/news, example.com, example.com/users, example.com/users/edit и т.п.
После декомпозиции в httpRequest будет помещен массив:

// example.com/news/list
array([section] => news, [action] => list, [id] => 1)
 
// example.com/news
array([section] => news, [action] => list, [id] => 1)
 
// example.com
array([section] => news, [action] => list, [id] => 1)
 
// example.com/users
array([section] => users, [action] => list, [id] => 1)
 
// example.com/users/edit
array([section] => users, [action] => edit, [id] => 1)

По умолчанию границами для placeholder является разделитель / (прямой слэш). Вы можете это изменить, указав необходимое требование на языке perl-совместимых регулярных выражений (PCRE) в третьем аргументе:

$router->addRoute('nameOfRule', new requestRoute('/:section/:id-:action'), array(), array('id' => '\d+'));

Правило совпадет с URL example.com/news/1-view и в httpRequest будет помещено:

array([section] => news, [id] => 1, [action] => view)

Пример получения результата:

$section = $this->request->getString('section');
$id = $this->request->getInteger('id');
$action = $this->request->getString('action');

3.3.5. Resolver

Резолверы предназначены для упрощения процесса подключения файлов. Резолверы возвращают полный путь к файлу (если он существует), часть которого передана в качестве единственного аргумента в методе resolve(). Например:

<?php
        $resolver->resolve('news'); // вернёт system/modules/news/news.php
?>

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

<?php
        $compositeResolver = new compositeResolver();
        $resolver1 = new firstResolver();
        $resolver2 = new secondResolver();
 
        $compositeResolver->addResolver($resolver1);
        $compositeResolver->addResolver($resolver2);
 
        $compositeResolver->resolve('file.php');
?>

В этом примере файл file.php будет сначала передан в $resolver1. Если $resolver1 не сможет отрезолвить запрос, тогда запрос будет передан в $resolver2.

В приложениях, однако, обращаться напрямую к резолверам вам не придётся. Специально для непосредственной загрузки файлов предназначена обёртка для резолвера - fileLoader. В самом начале загрузки приложения составляется композитный резолвер, который будет резолвить все запросы поиска файлов. Затем этот резолвер передаётся в fileLoader, и затем при каждом запросе fileLoader::load, запрос будет делегироваться этому резолверу. Отметим также, что набор резолверов по умолчанию сделан таким образом, что для "подмены" любого оригинального файла из mzz, вам необходимо создать файл с таким же именем в каталоге www вашего проекта. (Например для подмены файла system/modules/news/controllers/newsSaveController.php, который отвечает за процесс отрисовки и обработки формы создания и редактирования новости - создайте файл www/modules/news/controllers/newsSaveController.php). Для вашего проекта это произойдёт прозрачно - никаких путей изменять не надо, за вас это сделают резолверы.

Не забудьте удалить файл resolver.cache, располагающийся в каталоге tmp вашего проекта, иначе будет использован кэш и может возвращаться неактуальная информация.


Пример создания резолвера и загрузки файлов с его помощью:

<?php
            $baseresolver = new compositeResolver();
            $baseresolver->addResolver(new appFileResolver());
            $baseresolver->addResolver(new sysFileResolver());
 
            $resolver = new compositeResolver();
            $resolver->addResolver(new classFileResolver($baseresolver));
            $resolver->addResolver(new moduleResolver($baseresolver));
            $resolver->addResolver(new configFileResolver($baseresolver));
            $resolver->addResolver(new libResolver($baseresolver));
            $cachingResolver = new cachingResolver($resolver);
 
            fileLoader::setResolver($cachingResolver); // установка резолвера
 
            fileLoader::load('exceptions/init'); // использование
?>

fileLoader работает аналогично управляющей структуре php require_once. Иными словами - fileLoader подключает файл только 1 раз.

3.3.6. arrayDataspace

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

<?php
        $item_one = "foo";
        $item_two = "bar";
 
        $dataspace->set('foo', $item_one);
        $dataspace->set('bar', $item_two);
 
        $dataspace->get('foo');
        $dataspace->get('bar');
?>

3.4. Процесс запуска приложения

WEB-сервер принимает запрос от клиентского приложения. Этот запрос отправляется в единственную точку входа в приложение - index.php - с помощью правила mod_rewrite

RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule (.*) index.php?path=/$1&%{QUERY_STRING} [L]

Файл index.php подключает файл с конфигурацией (config.php). В конфигурации определяются пути до необходимых для работы mzz каталогов (ссылка на соотв. раздел) и некоторые другие опции, например - параметры соединения с БД.

Далее подключается файл %system%/index.php (где %system% - условное обозначение папки до системного каталога mzz). Этот файл в свою очередь запускает (единственный раз при первом запуске) скрипт check.php, который проверяет установленное на сервере Программное Обеспечение и его настройки на совместимость с mzz. Затем подключается информационный файл version.php, который содержит данные о версии mzz. После этого подключается файл с ядром системы /core/core.php и инициализируется объект класса core. Для запуска ядра у этого объекта вызывается метод run().

Вначале выполнения core::run() идёт блок с подключением минимально необходимого для функционирования приложения набора файлов. Происходит инициализация резольверов и некоторых других служебных классов. В последнюю очередь создаётся цепочка фильтров. Один из фильтров (contentFilter), входящий в эту цепочку, запускает приложение на выполнение. Остальные фильтры предназначены для создания необходимого приложению окружения.

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

3.5. MVC

Шаблон проектирования MVC (Model-View-Controller) организует и разделяет приложение на три отдельные роли:

Слой модели представлен в mzz ORM.

Слой представления использует Smarty в качестве шаблонного движка. Более подробное описание работы со Smarty вы можете найти на официальном сайте.

Контроллер -- слой, связывающий модель и представление. Именно в контроллер поступают данные запроса, на основе которых происходит получение нужных данных с помощью модели, и именно в контроллере эти данные в "сыром" виде передаются в шаблон. Контроллер вызывается шаблоном и обрабатывает какое-то определённое действие (action). Рассмотрим типичный контроллер mzz (отображение конкретной новости):

<?php
 
class newsViewController extends simpleController
{
    protected function getView()
    {
        $newsMapper = $this->toolkit->getMapper('news', 'news');
 
        $id = $this->request->getInteger('id');
        $news = $newsMapper->searchByKey($id);
 
        if (empty($news)) {
            return $this->forward404($newsMapper);
        }
 
        $this->smarty->assign('news', $news);
        return $this->smarty->fetch('news/view.tpl');
    }
}
 
?>

Контроллер в mzz должен быть отнаследован от базового класса simpleController и реализовывать как минимум один метод - getView(). Уровень доступа к этому методу должен быть protected, потому как этот метод запускается автоматически из simpleController, после того как будут произведены необходимые подготовительные действия. Ручной запуск контроллеров рекомендуется делать через метод run() (однако, вероятно, вам это не понадобится делать).

В классе simpleController на стадии инициализации создаются ссылки на следующие объекты:

Данные, которые возвращает метод getView(), будут добавлены к текущему ответу клиенту.

3.6. ORM

3.6.1. Общая информация

ORM в mzz предназначен для упрощения работы с данными в БД. Стандартные классы, входящие в ORM обладают минимально необходимым функционалом - CRUD (Create, Retrieve, Update, Delete).

ORM построен на базе The Data Mapper Pattern (рекомендуем ознакомиться со статьей перед продолжением чтения документации). Вкратце: в контектсте этого паттерна мы оперируем двумя терминами - маппер и доменный объект (ДО). Упрощённо: доменный объект - контейнер для данных, маппер - класс для заполнения ДО данными. Все действия по модификации также осуществляются через маппер.

Также отметим, что ДО является отображением данных приложения и данных в БД (в пределах 1 сеанса). Из этого следует, что до тех пор, пока объект не был сохранён специальным методом маппера, он будет выдавать "старые" данные (именно те, которые сейчас находятся в БД). Проиллюстрирую это на примере:

<?php
 
// $news        - Доменный Объект
// $newsMapper  - его маппер
 
$news = $newsMapper->searchById(1);
 
echo $news->getId();                    // 1
echo $news->getTitle();                 // "Заголовок для новости 1"
 
$news->setTitle('Новый заголовок');
 
echo $news->getTitle();                 // "Заголовок для новости 1"
 
$newsMapper->save($news);
 
echo $news->getTitle();                 // "Новый заголовок"
 
?>

DO лежат в корневом каталоге каждого модуля, мапперы - в подкаталоге mappers.

3.6.2. Мапперы

Мапперы это активная составляющая реализации паттерна The Data Mapper pattern. Именно мапперы производят отображение записей базы данных на объекты приложения и обратно, организуют связи между объектами разных типов, различными средствами автоматизируют рутинные задачи.

Пример типичного маппера:

<?php
/**
 * $URL: svn://svn.subversion.ru/usr/local/svn/mzz/trunk/docs/documentation/codes/structure.orm.mapper-1.php $
 *
 * MZZ Content Management System (c) 2005-2007
 * Website : http://www.mzz.ru
 *
 * This program is free software and released under
 * the GNU/GPL License (See /docs/GPL.txt).
 *
 * @link http://www.mzz.ru
 * @version $Id: structure.orm.mapper-1.php 3292 2009-06-02 09:57:30Z zerkms $
 */
 
fileLoader::load('news');
fileLoader::load('orm/plugins/acl_extPlugin');
fileLoader::load('modules/comments/plugins/commentsPlugin');
fileLoader::load('modules/jip/plugins/jipPlugin');
fileLoader::load('modules/i18n/plugins/i18nPlugin');
 
/**
 * newsMapper: маппер для новостей
 *
 * @package modules
 * @subpackage news
 * @version 0.3
 */
class newsMapper extends mapper
{
    /**
     * Имя класса DataObject
     *
     * @var string
     */
    protected $class = 'news';
    protected $table = 'news_news';
 
    protected $map = array(
        'id' => array(
            'accessor' => 'getId',
            'mutator' => 'setId',
            'options' => array(
                'pk', 'once',
            ),
        ),
        'folder_id' => array(
            'accessor' => 'getFolder',
            'mutator' => 'setFolder',
            'relation' => 'one',
            'foreign_key' => 'id',
            'mapper' => 'news/newsFolderMapper'
        ),
        'title' => array(
            'accessor' => 'getTitle',
            'mutator' => 'setTitle',
            'options' => array(
                'i18n',
            ),
        ),
        'editor' => array(
            'accessor' => 'getEditor',
            'mutator' => 'setEditor',
            'relation' => 'one',
            'foreign_key' => 'id',
            'mapper' => 'user/userMapper'
        ),
        'annotation' => array(
            'accessor' => 'getAnnotation',
            'mutator' => 'setAnnotation',
            'options' => array(
                'i18n',
            ),
        ),
        'text' => array(
            'accessor' => 'getText',
            'mutator' => 'setText',
            'options' => array(
                'i18n',
            ),
        ),
        'created' => array(
            'accessor' => 'getCreated',
            'mutator' => 'setCreated',
            'options' => array(
                'once',
            ),
        ),
        'updated' => array(
            'accessor' => 'getUpdated',
            'mutator' => 'setUpdated',
        ),
    );
 
    public function __construct()
    {
        parent::__construct();
        $this->plugins('acl_ext');
        $this->plugins('jip');
        $this->plugins('i18n');
        $this->plugins('comments');
    }
 
    protected function preInsert(& $data)
    {
        if (is_array($data)) {
            $data['updated'] = $data['created'];
        }
    }
 
    protected function preUpdate(& $data)
    {
        if (is_array($data)) {
            $data['updated'] = new sqlFunction('UNIX_TIMESTAMP');
        }
    }
 
    /**
     * Выполняет поиск объектов по идентификатору каталога
     *
     * @param integer $id идентификатор папки
     * @return array
     */
    public function searchByFolder($folder_id)
    {
        return $this->searchAllByField('folder_id', $folder_id);
    }
 
    public function convertArgsToObj($args)
    {
        if (isset($args['id'])) {
            $news = $this->searchByKey($args['id']);
            if ($news) {
                return $news;
            }
        }
 
        throw new mzzDONotFoundException();
    }
}
 
?>

В общем случае каждый маппер должен лишь переопределить 2 свойства:

Следует также обратить внимание на переменную $class. Эта переменная является опциональной и представляет собой имя класса, с которым работает данный маппер (в нашем случае это news). Если эта переменная не задана явно, то за имя класса берется имя таблицы.

3.6.3. Схема объекта

Свойство маппера $map представляет собой набор правил наложения данных БД на объектную модель. Вот несколько упрощенный пример map:

protected $map = array(
    'id' => array(
        'accessor' => 'getId',
        'mutator' => 'setId',
        'options' => array(
            'pk', 'once',
        )
    ),
    'title' => array(
        'accessor' => 'getTitle',
        'mutator' => 'setTitle'
    )
)

Ключами массива $map являются имена полей таблицы БД (которая задается в параметре $table маппера, ссылка). В общем случае у каждого поля должно быть описано имена двух методов — accessor и mutator.

Имя accessor'а имеет префикс "get" и используется для получения данных, которые хранятся в данном поле.

Mutator, соответственно, имеет префикс "set" и используется для сохранения данных в DO

У каждого поля может содержаться любое количество дополнительных параметров в ключе 'options'. Приведем примеры нескольких опций:

 

Рассмотрим основные методы для работы с мапперами:

Также для удобства имеется ряд методов для получения записей:

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

<?php
 
class userMapper extends simpleMapper
{
    [...]
    /**
     * Выполняет поиск объекта по логину
     *
     * @param string $login логин
     * @return object
     */
    public function searchByLogin($login)
    {
        return $this->searchOneByField('login', $login);
    }
}
 
?>

Экранировать значения, передаваемые в аргументах, не нужно. За вас это сделает генератор запросов.

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

3.6.4. Хуки

В ORM mzz на основе паттерна The Observer реализована система хуков. Каждый из хуков вызывается после (или перед) определённого действия в маппере. Также в хуки передаются данные, собственно с которыми код хука и должен работать. Полный список хуков может быть уточнён вами в файле orm/observer.php. Пример работы маппера с хуками:

<?php
 
class newsMapper extends mapper
{
    /**
     * Имя класса DataObject
     *
     * @var string
     */
    protected $class = 'news';
    protected $table = 'news_news';
 
    protected $map = array(...);
 
    [...]
 
    protected function preInsert(& $data)
    {
        if (is_array($data)) {
            $data['updated'] = $data['created'];
        }
    }
 
    protected function preUpdate(& $data)
    {
        if (is_array($data)) {
            $data['updated'] = new sqlFunction('UNIX_TIMESTAMP');
        }
    }
 
    [...]
}
 
?>

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

3.6.5. Плагины

Плагины предназначены для расширения функциональности мапперов без непосредственно их модификации. Функционал плагинов базируется на системе событий (на которой также построены хуки). Устройство плагинов рассмотрим на примере:

<?php
 
class obj_idPlugin extends observer
{
    protected $options = array(
        'obj_id_field' => 'obj_id'
    );
 
    protected function updateMap(& $map)
    {
        $map[$this->options['obj_id_field']] = array(
            'accessor' => 'getObjId',
            'mutator' => 'setObjId',
            'options' => array('once')
        );
    }
 
    public function postCreate(entity $object)
    {
        if (!$object->getObjId()) {
            $obj_id = systemToolkit::getInstance()->getObjectId();
            $map = $this->mapper->map();
            $object->{$map[$this->options['obj_id_field']]['mutator']}($obj_id);
            $this->mapper->save($object);
        }
    }
 
    public function preInsert(array & $data)
    {
        $data[$this->options['obj_id_field']] = systemToolkit::getInstance()->getObjectId();
    }
}
 
?>

Этот плагин добавляет в маппер возможность работать с полем obj_id (оно активно используется в acl и некоторых других задачах, когда нужно уникально в пределах приложения определить объект). В этом плагине определено свойство $options, в котором хранится имя поля, в котором будет собственно сам уникальный счётчик. Метод updateMap будет автоматически вызван классом-предком observer и в качестве аргумента будет передана схема объекта, в которую, как видно из кода, будет добавлен новый метод.

Событие postCreate вызывается сразу после создания объекта. В обработчике проверяется, есть ли уже какое-то значение obj_id для данного объекта. Если нет - генерируется новое значение, передаётся объекту, после чего объект сохраняется. Таким образом это событие используется в случае, когда плагин подключен для уже имеющегося набора данных, для которого ещё не определены obj_id. Событие preInsert предназначено для объектов, которые только создаются - в этом случае уникальный номер генерируется и сразу передаётся в массив данных объекта.

Подключение плагинов происходит с помощью двух методов маппера:

$this->plugins('obj_id');

Это простой метод подключения, в котором невозможно задать различные настройки плагину (если он их подразумевает). Второй метод:

$this->attach(new tree_mpPlugin(array('path_name' => 'id')));

В этом случае аргументом является объект плагина.

Общесистемные плагины располагаются в директории system/orm/plugins. Местоположение плагинов модулей жёстко не декларируется, но предпочтительно их размещать в поддиректории plugins директории с модулями.

В настоящее время вместе с mzz поставляются следующие плагины:

3.7. ACL

3.7.1. Обзор

Одной из основных идей mzz является: "Всё на сайте является объектом". Т.е. новость это объект класса news, фотография в галерее - объект класса photo, пользователь - объект класса user. Каждый из объектов обладает набором действий, которые можно над ним произвести. Например "удалить", "создать", "изменить", "переместить" и т.д. Этот набор не является жёстко фиксированным и создаётся программистом в процессе разработки приложения. Система прав mzz позволяет разграничивать права на выполнение определённых действий на уровне конкретных объектов, для пользователей и групп. Т.е. вы можете разрешить (или наоборот, запретить) выполнение "редактирования" новости определённым пользователем (или группой пользователей). При этом проверка прав на действия - производится автоматически. Писать код, работающий с правами, вам придётся лишь в том случае, если будет необходимо реализовать нетривиальную схему авторизации, не вписывающуюся в имеющуюся в mzz. Обо всём по порядку.

3.7.2. Хранение прав в БД

Данные о правах хранятся в группе таблиц в БД mzz.

Имя таблицы Описание
sys_access Главная таблица ACL. Непосредственно в ней хранятся все права на все объекты в mzz.
sys_access_registry Реестр объектов. Для того, чтобы объекту можно было назначать права, он должен быть зарегистрирован в ACL. Эта таблица хранит в себе список объектов и ссылку на тип объекта и раздел, в контектсе которого этот объект существует.
sys_actions Справочник действий. Таблица, содержащая список всех действий, которые могут быть выполнены с объектами системы.
sys_classes Справочник классов. Таблица, содержащая список всех типов объектов, доступных в системе.
sys_classes_actions Таблица, связывающая между собой конкретный класс и действия. Именно на основании данных этой таблицы определяется - какие действия возможно производить над объектом.
sys_classes_sections Таблица, связывающая между собой конкретный класс и секцию, в контексте которой хранится объект этого класса.
sys_modules Справочник модулей. Таблица, содержащая список всех типов модулей, доступных в системе.
sys_obj_id Таблица, выполняющая роль секвенции. Используется для определения следующего значения obj_id (уникального в пределах системы идентификатора объекта).
sys_obj_id_named Таблица, содержащая набор "именованных" объектов. Они используются в тех случаях, когда реального объекта не существует, но он необходим идеологически.
sys_sections Справочник разделов. Таблица, содержащая список всех типов разделов, в контексте которых могут работать модули.

3.7.3. Наложение прав

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

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

Пусть имеется в наличии объект класса news с уникальным идентификатором obj_id = 100. Для простоты представим, что с объектом класса news можно производить 2 действия: просмотр и редактирование. Также зарегистрировано 3 пользователя с именами visitor, editor и hacker, которые в примерах будут фигурировать, как рядовой посетитель, контент-менеджер системы и злоумышленник соответственно. Также в системе имеются 3 группы: viewers (посетители), managers (контент-менеджеры системы), banned (забаненные пользователи). Пользователь editor состоит в группах viewers и managers, пользователь visitor - только в группе viewers, а пользователь hacker - в группе banned.

Пользователи

Ид Имя
1 visitor
2 editor
3 hacker

Группы

Ид Имя
1 viewers
2 managers
3 banned

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

Ситуация 1. Права по умолчанию на все объекты класса news.

Действие Пользователь Группа Правило
Просмотр 1 разрешено
Редактирование 2 разрешено
Просмотр 3 запрещено

Из этой таблицы видно, что по умолчанию всем пользователям из группы viewers разрешён просмотр новостей, группе managers разрешено редактирование всех новостей, а пользователям, включённым в группу banned доступ на просмотр всех новостей закрыт. Применительно к пользователям: visitor и editor получат права на просмотр, причём у editor'а также будет право и на редактирование всех новостей. А у hacker доступ ко всем действиям всех объектов класса news - закрыт.

Ситуация 2. Права на конкретный объект news (obj_id = 100).

Теперь давайте добавим несколько частных прав на конкретную новость (у которой obj_id = 100).

Действие Пользователь Группа Правило
Редактирование 1 разрешено
Редактирование 2 запрещено
Просмотр 3 разрешено

Разберёмся с этими установленными правами:

1 строка: в ней мы разрешаем доступ для пользователя visitor на редактирование. Ранее у этого пользователя на редактирование права выставлены не были, сейчас правило - разрешающее. Вердикт: доступ разрешён (См. тезис 2).

2 строка: в ней мы запрещаем доступ для редактирования для всей группы managers. До этого группе было выставлено разрешающее правило. Вердикт: доступ запрещён (См. тезис 3).

3 строка: в ней мы разрешаем доступ на просмотр конкретной новости пользователю hacker. Для этого пользователя это единственное правило. Но он состоит в группе, на которую уже было установлено запрещающее. доступ запрещён (См. тезис 3).

3.7.4. Владельцы объектов

В системе управления правами mzz также существует и такое понятие, как владелец. Это тот пользователь, от чьего имени был создан объект. Для владельцев можно установить специальный набор прав, разрешающий или запрещающий ряд действий, производимых над вновь созданным объектом. Номинально - при создании и регистрации объекта набор "прав для владельца" копируется для конкретного объекта и конкретного пользователя, т.е. правила фактически становятся конкретными правилами для объекта. Из этого следует, что если вы измените "права для владельца" объекта, то новый набор прав будет применён лишь к создаваемым после этого события объектам. На права, выставленные на объекты, созданные до изменения набора "прав для владельца", это никак не повлияет.

3.7.5. Работа с ACL

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

Перед описанием принципов работы с ACL, приведём прототип конструктора класса acl:

public function __construct($user = null, $object_id = 0, $class = '', $section = '')

Получение прав (в качестве примера возьмём все данные из примера в прошлом пункте):

<?php
 
    $user = $userMapper->searchByLogin('editor');       // получаем пользователя 'editor'
    $news = $newsMapper->searchById(100);               // получаем новость с id = 100 (obj_id этой новости примем также равным 100)
 
    $acl = new acl($user, $news->getObjId());
 
    $access = $acl->get();                              // будет возвращён массив array('edit' => true, 'view' => true);
    $access = $acl->get('view');                        // true
 
    $user2 = $userMapper->searchByLogin('visitor');     // получаем пользователя 'visitor'
    $acl = new acl($user2, $news->getObjId());
 
    $access = $acl->get();                              // будет возвращён массив array('edit' => false, 'view' => true);
    $access = $acl->get('view');                        // true
    $access = $acl->get('edit');                        // false
 
    $user3 = $userMapper->searchByLogin('hacker');      // получаем пользователя 'hacker'
    $acl = new acl($user3, $news->getObjId());
 
    $access = $acl->get();                              // будет возвращён массив array('edit' => false, 'view' => false);
    $access = $acl->get('view');                        // false
    $access = $acl->get('edit');                        // false
 
?>

Модификация прав:

<?php
 
    $user = $userMapper->searchByLogin('editor');       // получаем пользователя 'editor'
    $news = $newsMapper->searchById(100);               // получаем новость с id = 100 (obj_id этой новости примем также равным 100)
 
    $acl = new acl($user, $news->getObjId());
    $access = array('edit' => false, 'view' => false);
    $acl->set($access);                                 // будут установлены запрещающие права для пользователя 'editor'
    $acl->set('view', true);                            // будет разрешён просмотр новости
 
    $group = $groupMapper->searchByName('banned');
    $acl->set('view', true, $group->getId());           // для группы 'banned' будет разрешён просмотр новости
                                                        //(однако в результате всё равно никто из этой группы просмотреть эту новость не сможет, см. тезис 3)
 
?>

3.7.6. Запуск модулей из шаблонов

ACL автоматически проверяет на возможность запуска действия модуля из шаблона. В случае, если права на выполнение действия есть - происходит запуск модуля, если прав нет - показывается страница 403 с соответствующим сообщением для пользователя (либо произвольная, определённая программистом страница). Для управления поведением системы проверки прав используется параметр 403handle. Он может принимать 3 значения:

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

Пример запуска модулей:

{load module="news" action="list" section="news" 403handle="manual"}
{load module="news" action="list" section="news" 403tpl="news/deny.tpl"}

В случае, если значение 403handle не было изменено, и 403tpl также не был указан - вызывается контроллер simple403Controller, реализация которого и определяет, что именно будет показано в случае, когда у пользователя не хватает прав на запуск действия. Реализация по умолчанию, идущая в фреймворке, представляет собой отображение страницы с именем 403 модуля page, секции page. В случае, если вам нужно переопределить логику работы этого контроллера - воспользуйтесь предусмотренным для этого механизмом переопределения классов, используя резолверы.

3.7.7. obj_id

obj_id это служебное поле, используемое в системе проверки прав доступа, а также в некоторых служебных целях в ORM. Это поле должно присутствовать во всех таблицах, которые находятся под управлением ACL и ORM, и иметь тип integer. obj_id является уникальным в пределах проекта, т.е. не существует двух объектов с одинаковым значением этого параметра. Доступ к значению obj_id производится с помощью метода ДО getObjId(). Это достигается с помощью таблицы sys_obj_id.

Бывают ситуации, когда необходимо зарегистрировать объект в ACL и дать ему некоторое значение obj_id, но создавать целую сущность ради одной записи в таблице - не оправданно. Тогда следует прибегнуть к механизму так называемых "фейковых" объектов. В терминологии мзз - это такие объекты, которые фактически не существуют в БД, однако зарегистрированы в ACL и имеют свои значения obj_id. Это реализуется с помощью метода getObjectId() тулкита. "Фейковые" объекты идентифицируются по имени. Пример получения obj_id для "фейкового" объекта:

$obj_id = $this->toolkit->getObjectId('sample_fake_object_name');

В результате - если "фейковый" объект с именем sample_fake_object_name не существовал, то он будет создан и возвращено значение его obj_id, если уже существовал - тогда просто возвращён obj_id.

Также бывают ситуации, когда использование obj_id нецелесообразно. В число таких ситуаций можно отнести случаи, когда объекты не участвуют в ACL и когда использование лишнего поля - просто не имеет смысла. В число демо-приложения входят как минимум 2 класса, для которых obj_id не нужно - это userAuth и userOnline. Эти сущности предназначены для реализации "запоминания" аутентификации и хранения пользователей онлайн соответственно. "Отключение" использования obj_id для такого рода классов осуществляется следующим образом:

<?php
class userAuthMapper extends simpleMapper
{
        protected $obj_id_field = null;
        [...]
}
?>

Т.е. достаточно защищённое свойство $obj_id_field установить в null.

3.7.8. Метод convertArgsToObj()

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

<?php
 
class newsMapper extends simpleMapper
{
    [...]
 
    public function convertArgsToObj($args)
    {
        $news = $this->searchOneByField('id', $args['id']);
 
        if ($news) {
            return $news;
        }
 
        throw new mzzDONotFoundException();
    }
}
 
?>

3.7.9. Метод getAcl()

Метод getAcl класса simple предназначен для усложнения системы прав, в тех случаях, когда штатных средств ACL становится недостаточно. Этот метод принимает аргументом имя экшна, и должен возвращать булево значение, true - в случае если права есть и false в противном случае.

<?php
 
class photo extends simple
{
    [...]
 
    public function getAcl($name = null)
    {
        $access = parent::getAcl($name);
 
        if (in_array($name, array('viewPhoto', 'viewThumbnail', 'view')) && $access) {
            $access = $this->getAlbum()->getAcl('viewAlbum');
        }
 
        return $access;
    }
}
 
?>

В этом примере происходит уточнение прав на некоторые экшны доменного объекта photo. Из кода видно, что доступ к действиям view, viewPhoto и viewThumbnail есть лишь в том случае, если есть доступ к экшну viewAlbum просматриваемого альбома.

3.8. Структура каталогов mzz

Структура каталогов mzz выглядит следующим образом:

mzz/
  db/
  docs/
  files/
  libs/
  system/
  tests/
  tmp/

Перечислим все представленные каталоги и опишем их назначение:

Структура каталогов проекта описана в разделе "Структура проекта".

3.9. JIP и AJAX

JIP - это быстрый доступ к действиям над объектом.

Все действия, на которые есть права у текущего пользователя, собраны в виде элементов меню, которое отображается при нажатии на кнопку JIP рядом с объектом. При нажатии на такой элемент открывается JIP окно и благодаря технологии AJAX позволяет выполнить действие над объектом не покидая текущую страницу.

Реализация клиентской части JIP состоит из трех Javascript-файлов: prototype.js, effects.js, jip.js и одного CSS: jip.css. Два последних файла подгружается только при наличии на странице хотя бы одного JIP-меню. Подключение Javascript/CSS файлов, используемых только в JIP-окнах, может располагаться в шаблоне jip.tpl (см. {add}).

Кнопка JIP появляется только когда у текущего пользователя есть хотя бы одно разрешенное действие над объектом.

В качестве JavaScript Framework используется Prototype, существенно облегчающий разработку JavaScript сценариев. Кроме того, используется библиотека script.aculo.us.

JIP имеет каждый DataObject, наследованный от класса simple.

Действия, которые доступны в меню при наличии прав на них, имеют опцию jip со значением 1 в конфигурации действий:

[edit]
controller = "edit"
jip = "1"

Для отображения кнопки JIP необходимо в шаблоне вызвать метод simple::getJip():

{$news->getJip()}

При нажатии на один из элементов соответствующая страница открывается в JIP-окне.

Также в JIP-окне открываются любые ссылки, принадлежащие CSS-классу jipLink:

<a href="{url route="default" section="news" action="info"}" class="jipLink">Сис. информация</a>

Открываемые в JIP-окне страницы должны содержать как минимум его заголовок. Он определяется HTML-элементом <DIV>, который принадлежит к CSS-классу jipTitle:

<div class="jipTitle">Создать новость</div>

Форму можно отправить через Ajax добавив атрибут onsubmit="return mzzAjax.sendForm(this);":

<form action="/winner/add" method="post" onsubmit="return mzzAjax.sendForm(this);"><br />
Имя: <input size="60" name="name" type="text"><br />
<input type="submit"><br />
</form>
Закрыть JIP-окно можно вызовом Javascript-функции jipWindow.close():
<input type="reset" onclick="jipWindow.close();">
<a href="javascript: void(jipWindow.close());">закрыть</a>

3.10. Хелперы и формы

3.10.1. Основные хелперы

Для генерации полей (элементов) формы в шаблоне используется механизм хелперов. Данное решение освобождает от необходимости заботиться о восстановлении значений формы, в случае неправильного её заполнения и о выделении ошибочно заполненных полей. Файлы с классами хелперов расположены в каталоге system/forms. Следует отметить, что хелперами генерируется валидный xhtml-код. Запускаются они из шаблона следующим образом:

{form->helper_name ...}

Хелперы могут принимать различные параметры, но есть и несколько общих:

Также могут быть указаны любые параметры html-синтаксиса, используемые в тегах. Например: style, class, id. Примеры их использования:

{form->text style="form_text_field" id="some_field" name="login"}

К использованию доступны следующие хелперы:

3.10.2. Создание собственных элементов форм

Mzz позволяет легко расширять список доступных хелперов форм. Все хелперы располагаются в каталоге forms. Создание элемента рассмотрим на примере элемента формы типа text.
Все элементы формы наследуются от класса formElement. Создадим файл с именем formAdvancedTextField.php в каталоге forms:

<?php
 
class formAdvancedTextField extends formElement
{
    public function __construct()
    {
        $this->setAttribute('type', 'text');
        $this->setAttribute('value', '');
    }
 
    public function render($attributes = array(), $value = null)
    {
        return $this->renderTag('input', $attributes);
    }
}
 
?>

Всё. Теперь в шаблонах можно использовать новый тип хелпера. Запуск его не будет отличаться от запуска базовых хелперов:

{form->advancedText name="new_helper" value="new helper value"}

3.10.3. Валидация форм

В mzz генерация и валидация форм разделены. Стандартные правила валидации располагаются в каталоге system/forms/validators. Для валидации формы вначале нужно подключить класс валидатора форм и создать объект этого класса. Это делается так:

fileLoader::load('forms/validators/formValidator');
$validator = new formValidator();

Единственным аргументом для конструктора класса formValidator является имя поля, свидетельствующего о том, что форма отправлена. По умолчанию используется значение submit. Для добавления правила валидации формы используется следующий синтаксис:

$validator->add('имя валидатора', 'имя поля', 'сообщение об ошибке', 'дополнительные параметры');

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

3.10.4. Создание собственных валидаторов

Валидаторы, так же как и хелперы, также могут быть легко расширены новыми. Создаваемые правила должны быть отнаследованы от базового класса formAbstractRule. Как и в случае с callback-функциями, валидация считается пройденной, если валидатор возвращает true, и проваленной в противном случае. Рассмотрим процесс создания валидаторов на примере. Пусть это будет новый валидатор, который проверяет, что введённое число попадает в определёный диапазон.

Создадим файл formRangeRule.php в каталоге system/forms/validators:

<?php
 
class formRangeRule extends formAbstractRule
{
    public function validate()
    {
        // проверяем, что введённое значение больше либо равно минимального
        // и меньше либо равно максимального
        return $this->value >= $this->params[0] && $this->value <= $this->params[1];
    }
}
 
?>

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

<?php
 
$validator->add('range', 'hour', 'Введённое число не входит в интервал от 1 до 24', array(1, 24));
 
?>

3.11. Описание timer

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

Для вызова в нужном месте шаблона размещается конструкция:

{$timer->toString()}
Оформление хранится в шаблоне filter/time.tpl. Пример оформления:
<div>Время генерации: {$timer->getPeriod()|round:5} сек. <br />
Запросов к БД {$timer->getQueriesNum()}/{$timer->getPreparedNum()}: {$timer->getQueriesTime()|number_format:5} сек.</div>
Метод timer::toString() может принимать аргументом имя шаблона, которым будут оформлены данные:
{$timer->toString('admin/time.tpl')}

Часть 4. Быстрый старт

4.1. Структура проекта

Структура типичного проекта, выполненного на основе mzz выглядит следующим образом:

<папка проекта>/
  tmp/
  www/
    configs/
    files/
    templates/
    .htaccess
    application.php
    index.php

Перечислим все представленные каталоги и опишем их назначение:

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

4.2. Создание конфигурации для проекта

Вся конфигурация проекта содержится в следующих файлах в каталоге проекта:

configs/
    .htaccess
    config.php
    modules.php
    routes.php
.htaccess
application.php

Файл configs/.htaccess содержит лишь одну строку, которая запрещает внешний просмотр и выполнение файлов в каталоге configs/

Deny from all

Файл configs/config.php содержит системную конфигурацию

<?php
 
/**
 * Абсолютный путь до сайта.
 * Если mzz установлен в корень веб-сервера, оставьте поле пустым
 * Правильно: /mzz, /new/site
 * Неправильно: site1, site1/, /site1/
 *
 */
define('SITE_PATH', '');
define('COOKIE_DOMAIN', '');
 
define('DEBUG_MODE', 1);
define('SYSTEM_PATH', '../system/');
 
/**
 * Идентификатор записи в БД для неавторизированных пользователей
 */
define('MZZ_USER_GUEST_ID', 1);
 
/**
 * Идентификатор группы, для которой ACL всегда будет возвращать true (т.е. предоставит полный доступ)
 */
define('MZZ_ROOT_GID', 2);
 
require_once(SYSTEM_PATH . 'systemConfig.php');
 
systemConfig::$db['default']['driver'] = 'pdo';
systemConfig::$db['default']['dsn']  = 'mysql:host=localhost;dbname=mzz';
systemConfig::$db['default']['user'] = 'root';
systemConfig::$db['default']['password'] = '';
systemConfig::$db['default']['charset'] = 'utf8';
systemConfig::$db['default']['pdoOptions'] = array();
 
systemConfig::$pathToApplication = dirname(__FILE__) . '';
systemConfig::$pathToTemp = realpath(dirname(__FILE__) . '/../tmp');
systemConfig::$pathToConf = dirname(__FILE__) . '/configs';
 
systemConfig::init();
 
?>

Файл configs/modules.php содержит ассоциативный массив связи секция => модуль

$modules = array (
    'access' => 'access',
    'admin' => 'admin',
    'comments' => 'comments',
    'config' => 'config',
    'menu' => 'menu',
    'news' => 'news',
    'page' => 'page',
    'captcha' => 'captcha',
    'user' => 'user',
);

Файл configs/routes.php содержит настройку Routes для URL

<?php
 
$router->addRoute('default', new requestRoute('', array('section' => 'news', 'action' => 'list', 'name' => 'root')));
$router->addRoute('default2', new requestRoute(':section/:action'));
$router->addRoute('withId', new requestRoute(':section/:id/:action', array('action' => 'view'), array('id' => '\d+')));
 
?>

Файл application.php (todo)

<?php
/**
 * $URL: svn://svn.subversion.ru/usr/local/svn/mzz/trunk/docs/documentation/codes/quick_start.config-2.php $
 *
 * MZZ Content Management System (c) 2006
 * Website : http://www.mzz.ru
 *
 * This program is free software and released under
 * the GNU/GPL License (See /docs/GPL.txt).
 *
 * @link http://www.mzz.ru
 * @package system
 * @subpackage core
 * @version $Id: quick_start.config-2.php 2182 2007-11-30 04:41:35Z zerkms $
*/
 
/**
 * application: приложение
 *
 * @package system
 * @subpackage application
 * @version 0.1
 */
 
class applicaion extends core
{
 
}
 
?>

Файл .htaccess содержит настройки для http-сервера Apache

AddDefaultCharset UTF-8
RewriteEngine on
Options +FollowSymlinks -Indexes -Includes -MultiViews
# +MultiViews
RewriteBase /
RewriteCond %{SCRIPT_FILENAME} !-f
#Uncomment next line if you want no rewrite exists folders
#RewriteCond %{SCRIPT_FILENAME} !-d
 
RewriteRule (.*) index.php?path=/$1&%{QUERY_STRING} [L]

Часть 5. Модули системы

5.1. Описание структуры модуля

5.1.1. Структура каталогов

Структура каталогов типичного модуля mzz выглядит следующим образом (рассмотрим на примере стандартного модуля news):

news/
  actions/
  controllers/
  i18n/
  mappers/
  templates/
  news.php
  newsFolder.php

Перечислим все представленные элементы и опишем их назначение:

Далее рассмотрим примеры файлов рассматриваемого модуля. Для демонстрации выберем метод View.

5.1.2. Actions

Пример типичного файла с actions (действиями):

; news actions config
 
[view]
controller = "view"
 
[edit]
controller = "save"
jip = 1
icon = "sprite:mzz-icon/mzz-icon-doc-edit"
lang = 1
main = "active.blank.tpl"
 
[move]
controller = "move"
jip = 1
icon = "sprite:mzz-icon/mzz-icon-doc-move"
 
[delete]
controller = "delete"
jip = 1
icon = "sprite:mzz-icon/mzz-icon-doc-del"
confirm = "_ news/confirm_delete"
main = "active.blank.tpl"
 
[admin]
controller = "admin"
title = "_ admin"
admin = "1"
 
[searchByTag]
controller = "searchByTag"
title = "searchByTag"

Каждая секция (Например: [view]) обозначает имя определяемого действия. Имя может состоять из букв латинского алфавита (желательно в нижнем регистре), цифр и знака подчёркивания. В каждой секции указывается свойство controller - это имя контроллера, который будет обслуживать данное действие (ссылка на теорию MVC). Это свойство является обязательным (?).

Следующие свойства опциональны:

5.1.3. Controllers

Пример типичного контроллера (также для действия View):

<?php
/**
 * $URL: svn://svn.subversion.ru/usr/local/svn/mzz/trunk/docs/documentation/codes/modules.description.controllers-1.php $
 *
 * MZZ Content Management System (c) 2005-2007
 * Website : http://www.mzz.ru
 *
 * This program is free software and released under
 * the GNU/GPL License (See /docs/GPL.txt).
 *
 * @link http://www.mzz.ru
 * @version $Id: modules.description.controllers-1.php 3008 2009-02-06 15:02:36Z striker $
 */
 
/**
 * NewsViewController: контроллер для метода list модуля news
 *
 * @package modules
 * @subpackage news
 * @version 0.1.1
 */
 
class newsViewController extends simpleController
{
    protected function getView()
    {
        $newsMapper = $this->toolkit->getMapper('news', 'news');
 
        $id = $this->request->getInteger('id', SC_PATH);
        $news = $newsMapper->searchById($id);
 
        if (empty($news)) {
            return $this->forward404($newsMapper);
        }
 
        $this->smarty->assign('news', $news);
        return $this->smarty->fetch('news/view.tpl');
    }
}
 
?>

В каждом контроллере должен быть определён метод getView(). Этот метод возвращает результат работы действия.

5.1.4. Mappers

О мапперах читайте в соответствующем разделе: орм.мапперы.

5.2. Написание модуля "Комментарии"

5.2.1. Введение

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

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

В этой главе будем рассмотрен процесс создания нового модуля. В качестве примера будет рассмотрен новый модуль Comments, которого ещё нет в демо-приложении "лента новостей", работающего под управлением mzz.

Для начала давайте определимся, что же мы хотим получить в результате. Модуль Comments будет служить для добавления возможности комментирования любых объектов приложения. Это значит, что его легко и непринуждённо можно будет подключить как к модулю News, так и к Page и к любому произвольному модулю системы. Приступим.

5.2.2. Планирование

Определимся с набором возможных действий модуля.

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

5.2.3. Структура БД

Для таблицы с комментариями нам нужны следующие поля:

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

CREATE TABLE `comments_comments` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `obj_id` int(11) UNSIGNED DEFAULT NULL,
  `text` text,
  `author` int(11) UNSIGNED DEFAULT NULL,
  `time` int(11) UNSIGNED DEFAULT NULL,
  `folder_id` int(11) UNSIGNED DEFAULT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Эту же структуру опишем в файле comments.map.ini:

[id]
accessor = "getId"
mutator = "setId"
once = true
 
[text]
accessor = "getText"
mutator = "setText"
 
[author]
accessor = "getAuthor"
mutator = "setAuthor"
 
[time]
accessor = "getTime"
mutator = "setTime"
 
[folder_id]
accessor = "getFolder"
mutator = "setFolder"

Как вы можете заметить - в этой схеме мы ещё не учли отношения с таблицами пользователей и commentsFolder. Это будет сделано чуть позднее.

Для сущности commentsFolder таблица БД будет следующей:

CREATE TABLE `comments_commentsfolder` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `obj_id` int(11) UNSIGNED DEFAULT NULL,
  `parent_id` int(11) UNSIGNED DEFAULT NULL,
  PRIMARY KEY  (`id`),
  KEY `parent_id` (`parent_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Эту структуру также опишем в файле commentsFolder.map.ini:

[id]
accessor = "getId"
mutator = "setId"
once = true
 
[parent_id]
accessor = "getParentId"
mutator = "setParentId"

5.2.4. Общий вид урлов

В соответствии с нашими потребностями определим вид ссылок (URL), которые будут доступны пользователю.

Из всех необходимых экшнов только post будет заметен пользователю приложения. Соответственно ссылка будет выглядеть следующим образом:

http://mzz/comment/555/post

Где 555 - значение obj_id комментируемого объекта (например: новости, статьи, фотографии в галерее, и т.д.).

Правило для requestRouter, обрабатывающее подобные ссылки, в routes.php (ссылка) добавлять не нужно, т.к. правило по умолчанию:

$router->addRoute('withId', new requestRoute(':section/:id/:action', array('action' => 'view'), array('id' => '\d+')));
предназначено для ссылок такого вида.

5.2.5. Создание структуры каталогов

Для создания структуры каталогов в командной строке перейдите в каталог system/modules и выполните следующую команду:

generateModule.bat comments

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

Module root folder created successfully:
- comments
 
Folder created successfully:
- comments/actions
- comments/controllers
- comments/mappers
- comments/maps
- comments/views
 
File created successfully:
- comments/commentsFactory.php
- generateDO.bat
- generateAction.bat
 
ALL OPERATIONS COMPLETED SUCCESSFULLY

5.2.6. Создание сущностей

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

5.2.7. Регистрация модуля в системе

Зарегистрируем модуль Comments в приложении.

Из всех определённых нами необходимых экшнов в системе уже зарегистрированы 3: list, edit, delete. Соответственно в таблицу `sys_actions` добавляем только одну запись со значением post. Для удобства выпишем идентификаторы всех используемых далее экшнов:

Естественно идентификаторы ваших экшнов (и последующие идентификаторы) могут отличаться от приведённых в документации.

В таблицу `sys_modules` также добавляем одну запись - comments, которой присваивается id = 8.

В таблице `sys_classes` хранятся все сущности приложения. Добавляем в неё 2 записи - comments и commentsFolder. В качестве `module_id` для обоих записей указываем 8 (идентификатор модуля). Для вновь созданных записей значения id составили 10 и 11 соответственно.

Теперь мы должны указать какие действия каким сущностям должны соответствовать. Для comments это будут edit, delete, editACL, для commentsFolder - list, post, editACL. Поэтому в таблицу `sys_classes_actions` добавляем записи, связывающие конкретную сущность и соответствующие им экшны. В нашем случае это будут следующие записи:

class_idaction_id
101
102
109
115
1119
119

В таблицу `sys_sections` добавляем имя раздела, в котором будет "располагаться" новый модуль - comments. Получаем id = 8.

Через таблицу `sys_classes_sections` связываем сущности нового модуля и раздел. Добавляем 2 записи с `section_id` = 8 и `class_id` = 10 и 11 соответственно. Этим записям будут присвоены id = 10 (для comments) и 11 (для commentsFolder). Совпадение случайное - у вас они могут быть другими.

5.2.8. Программирование действий

В первую очередь давайте создадим действие list, которое будет выводить список комментариев к данному объекту. Программировать будем для модуля news (однако не будем забывать, что полученный модуль должен быть универсален и быть возможным подключенным к любому объекту).

Открываем файл news.view.tpl, который располагается в каталоге www/templates/news и добавляем в него следующую строку:

{load module="comments" section="comments" action="list" parent_id=$news->getObjId() owner=$news->getEditor()->getId()}

Последний аргумент указывает - кто будет владельцем только что созданного commentsFolder'а. Затем попытаемся открыть какую-либо новость. Ссылка для определённой новости будет выглядеть приблизительно так:

http://mzz/news/4/view

Естественно по этому запросу мы увидим исключительную ситуацию (Exception).

System Exception. Thrown in file D:\server\sites\mzz\system\action\action.php (Line: 182) with message:
Действие "list" не найдено для модуля "comments"

Из этого описания мы видим, что не было создано действие list. Давайте его создадим. В командной строке, находясь в корневом каталоге модуля comments выполним команду:

generateAction.bat commentsFolder list

Результатом успешного выполнения будет:

File edited successfully:
- actions/commentsFolder.ini
 
File created successfully:
- controllers/commentsFolderListController.php
- views/commentsFolderListView.php
 
ALL OPERATIONS COMPLETED SUCCESSFULLY

Теперь обновим страницу http://mzz/news/4/view. Сообщение об ошибке изменилось:

Invalid Parameter. Thrown in file D:\server\sites\mzz\system\acl\acl.php (Line: 803) with message:
Свойство obj_id должно быть целочисленного типа и иметь значение > 0 (0)

И это логично, т.к. commentsFolder должен возвращать obj_id текущего commentsFolder'а, а мы этот метод ещё не написали. Но перед этим - давайте добавим в таблицу `comments_commentsFolder` одну запись, с которой мы и будем сейчас работать (в конце написания модуля commentsFolder'ы будут создаваться автоматически). В нашем случае obj_id будет равно 76 (это значение посмотрите в таблице `sys_obj_id`, вручную добавив ещё одну запись), а `parent_id` = 66 (это значение можно посмотреть либо в поле `obj_id` таблицы `news_news`, либо дописав {$news->getObjId()} в шаблон, с которым мы сейчас работаем, и обновив его). Также зарегистрируем новый объект в ACL. Для этого в таблицу `sys_access_registry` добавим запись со значениями obj_id = 76 и class_section_id = 11.

Теперь открываем файл commentsFolderMapper.php, расположенный в каталоге mappers модуля comments. Нас интересует метод convertArgsToId(). В массиве $args передаётся parent_id, по которому мы можем найти необходимый нам commentsFolder. Чтобы в этом убедиться в теле метода напишите var_dump($args); и обновите страницу. Если всё было проделано правильно, то $args['parent_id'] будет равно 66 (в вашем случае - возможно иное). Теперь по значению parent_id нам нужно найти соответствующую запись и вернуть obj_id, который соответствует этой записи. Это делается следующим кодом:

<?php
 
class commentsFolderMapper extends simpleMapper
{
    [...]
    /**
     * Возвращает уникальный для ДО идентификатор исходя из аргументов запроса
     *
     * @return object
     */
    public function convertArgsToId($args)
    {
        $comment = $this->searchOneByField('parent_id', $args['parent_id']);
        return $comment->getObjId();
    }
}
 
?>

Обновите страницу. Если всё выполнено верно - то на том месте, где должны располагаться комментарии, вы увидите надпись "доступ запрещён". Давайте дадим доступ текущему пользователю для списка комментариев. В таблицу `sys_access` добавим запись со значениями: `action_id` = 9 (editACL), `class_section_id` = 11, `obj_id` = 76, `uid` = 2 (admin), `allow` = 1. Поле gid оставим со значением null. Теперь чтобы дать полный доступ на этот объект воспользуемся графическим интерфейсом для изменения прав. Он доступен по ссылке: http://mzz/access/76/editACL. В этом окне кликнем на пользователя admin и выделим действие list, после чего нажмём кнопку "Установить права". Закроем это окно и обновим окно с новостью. Увидим сообщение:

Runtime Exception. Thrown in file D:\server\sites\mzz\system\template\mzzFileSmarty.php (Line: 50) with message:
Шаблон 'D:\server\sites\mzz\www/templates/comments/comments.list.tpl' отсутствует.

Это потому, что мы не создали необходимые шаблоны. Перейдём в каталог с шаблонами (www/templates) и создадим каталог comments, в котором создадим файл comments.list.tpl. В этот файл запишем произвольный текст для проверки - например 'hello world'. И снова обновим страницу. Теперь нашему взору должна предстать новость, после которой появилась наша надпись hello world. Однако если мы попробуем открыть другую новость, мы увидим следующую ошибку:

Fatal error: Call to a member function getObjId() on a non-object in D:\server\sites\mzz\system\modules\comments\mappers\commentsFolderMapper.php on line 49

Её причина в том, что для этой другой новости не создана commentsFolder. Естественно, это добавляться она будет автоматически. Давайте напишем код, который будет добавлять к текущему объекту commentsFolder если такового не существует.

<?php
 
class commentsFolderMapper extends simpleMapper
{
    [...]
    /**
     * Возвращает уникальный для ДО идентификатор исходя из аргументов запроса
     *
     * @return object
     */
    public function convertArgsToId($args)
    {
        $comment = $this->searchOneByField('parent_id', $args['parent_id']);
 
        if (is_null($comment)) {
            $toolkit = systemToolkit::getInstance();
            $request = $toolkit->getRequest();
            $ownerId = $request->get('owner', 'string', SC_PATH);
            $userMapper = $toolkit->getMapper('user', 'user', 'user');
            $owner = $userMapper->searchById($ownerId);
 
            $comment = $this->create();
            $comment->setParentId($args['parent_id']);
            $this->save($comment, $owner);
        }
 
        return $comment->getObjId();
    }
}
 
?>

Теперь обновим страницу. Увидим сообщение, что к только что добавленному commentsFolder'у у нас нет доступа. Это логично - ведь права доступа для этого модуля comments пока ещё не были установлены нами. Но вначале, перед установкой прав, убедимся, что запись в таблице `comments_commentsFolder` создалась. Для установки прав по умолчанию откроем страницу

http://mzz/access/comments/commentsFolder/editDefault

На просмотр этой страницы у нас пока также нет прав. Установим их. В таблице `sys_obj_id_named` посмотрим id у записи со значением поля `name` = access_comments_commentsFolder. В моём случае это 78. Затем в таблице `sys_access_registry` найдём этот объект, и увидим что у него поле `class_section_id` = 7. Теперь в таблицу `sys_access` добавляем запись со следующими значениями: `action_id` = 9 (editACL), `class_section_id` = 7, `obj_id` = 78, `uid` = 2 (admin), `allow` = 1. Поле `gid` оставим со значением null. Теперь откроем страницу

http://mzz/access/78/editACL

И уже средствами интерфейса добавим право на editDefault для пользователя admin. Теперь вернёмся к странице

http://mzz/access/comments/commentsFolder/editDefault

В этом интерфейсе добавим обе группы: auth и unauth. Для обоих выставим право на list, а для auth - ещё и право на editACL. Однако естественно для уже созданного commentsFolder'а эти права применены не будут. Так что давайте удалим из системы все упоминания об втором созданном commentsFolder'е и обновим страницу. Удалять нужно: вторую запись в таблице `comments_commentsFolder` и запись из `sys_access_registry`. Теперь обновим страницу со второй новостью. Если всё сделано верно - тогда тут тоже появится надпись 'hello world' и в таблицы будет добавлен новый commentsFolder и права на него.

На этом отвлекёмся от программирования метода list и создадим второй метод - post. Процесс генерации кода совершенно аналогичен генерации кода для list. Разберёмся, что должен делать метод post: отрисовка формы и получение данных из $_POST-массива с последующим добавлением в БД. Ну что ж, откроем comments.list.tpl. Удалим в нём hello world и добавим запуск этого же модуля comments, но действия post.

{load module="comments" section="comments" action="post" parent_id=$news->getObjId()}

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

http://mzz/access/79/editACL

Обновляем страницу и видим - что не найден шаблон comments.post.tpl. Естественно создаём его с уже знакомой нам фразой hello world. После очередного обновления страницы ошибок быть не должно, но должна появиться надпись hello world. На место этой надписи нам нужно добавить форму. Чтобы создать форму - создаём новый файл commentsPostForm.php в каталоге views:

<?php
 
class commentsPostForm
{
    static function getForm($parent_id)
    {
        fileLoader::load('libs/PEAR/HTML/QuickForm');
        fileLoader::load('libs/PEAR/HTML/QuickForm/Renderer/ArraySmarty');
 
        $url = new url();
        $url->addParam($parent_id);
        $url->setAction('post');
 
        $form = new HTML_QuickForm('post', 'POST', $url->get());
 
        $form->addElement('textarea', 'text', 'Ваш комментарий', 'rows=7 cols=50');
 
        $toolkit = systemToolkit::getInstance();
        $request = $toolkit->getRequest();
 
        $form->addElement('hidden', 'url', $request->get('REQUEST_URI', 'string', SC_SERVER));
 
        $form->addElement('reset', 'reset', 'Сброс');
        $form->addElement('submit', 'submit', 'Отправить');
 
        return $form;
    }
}
 
?>

Отредактируем созданные генератором кода файлы commentsFolderPostController.php и commentsFolderPostView.php

<?php
 
fileLoader::load('comments/views/commentsFolderPostView');
fileLoader::load('comments/views/commentsPostForm');
 
class commentsFolderPostController extends simpleController
{
    public function getView()
    {
        $form = commentsPostForm::getForm($this->request->get('parent_id', 'integer', SC_PATH));
 
        return new commentsFolderPostView($form);
    }
}
 
?>
<?php
 
class commentsFolderPostView extends simpleView
{
    public function toString()
    {
        $renderer = new HTML_QuickForm_Renderer_ArraySmarty($this->smarty, true);
        $this->DAO->accept($renderer);
 
        $this->smarty->assign('form', $renderer->toArray());
 
        return $this->smarty->fetch('comments.post.tpl');
    }
}
 
?>

Ну и конечно же шаблон для отображения формы comments.post.tpl

<form {$form.attributes}>
{$form.hidden}
{$form.javascript}
    <table border="0" cellpadding="0" cellspacing="1" width="50%">
        <tr>
            <td>{$form.text.label}</td>
        </tr>
        <tr>
            <td>{$form.text.html}</td>
        </tr>
        <tr>
            <td>{$form.submit.html}{$form.reset.html}</td>
        </tr>
    </table>
</form>

Теперь по обновлению страницы вы должны увидеть форму, состоящую из поля для ввода и кнопок "Отправить" и "Сброс". Эта форма снабжена примитивной проверкой на то, что в поле с комментарием ввели какую-либо информацию. Напишите что-либо и отправьте сообщение. Появится сообщение об ошибке:

Runtime Exception. Thrown in file D:\server\sites\mzz\system\controller\sectionMapper.php (Line: 81) with message:
Не найден активный шаблон для section = "comments", action = "post"

Перейдём в каталог www/templates/act и создадим в нём подкаталог comments. В нём будут располагаться активные шаблоны модуля comments. В этом каталоге создадим файл post.tpl со следующим содержанием:

{load module="comments" action="post"}

Снова обновим страницу. Увидим сообщение об ошибке:

PHP Error Exception. Thrown in file D:\server\sites\mzz\system\exceptions\errorDispatcher.php (Line: 49) with message:
Notice in file D:\server\sites\mzz\system\modules\comments\mappers\commentsFolderMapper.php:48: Undefined index: parent_id

Оно появилось потому, что правило requestRouter-а по умолчанию аргумент в ссылке именует как id, а мы обращаемся к элементу массива parent_id. Давайте добавим и обработку id также в commentsFolderMapper:

<?php
 
class commentsFolderMapper extends simpleMapper
{
    [...]
    /**
     * Возвращает уникальный для ДО идентификатор исходя из аргументов запроса
     *
     * @return object
     */
    public function convertArgsToId($args)
    {
        $parent_id = isset($args['parent_id']) ? $args['parent_id'] : $args['id'];
 
        $comment = $this->searchOneByField('parent_id', $parent_id);
 
        if (is_null($comment)) {
            $toolkit = systemToolkit::getInstance();
            $request = $toolkit->getRequest();
            $ownerId = $request->get('owner', 'string', SC_PATH);
            $userMapper = $toolkit->getMapper('user', 'user', 'user');
            $owner = $userMapper->searchById($ownerId);
 
            $comment = $this->create();
            $comment->setParentId($parent_id);
            $this->save($comment, $owner);
        }
 
        return $comment->getObjId();
    }
}
 
?>

Обновим страницу. И увидим форму с введённым значением. Это потому, что метод post пока не умеет обрабатывать post-запросы. Давайте научим его делать это ;). Модифицируем соответствующим образом файл commentsFolderPostController.php

<?php
 
fileLoader::load('comments/views/commentsFolderPostView');
fileLoader::load('comments/views/commentsPostForm');
fileLoader::load('comments/views/commentsPostSuccessView');
 
class commentsFolderPostController extends simpleController
{
    public function getView()
    {
        $form = commentsPostForm::getForm($this->request->get('parent_id', 'integer', SC_PATH));
 
        if ($form->validate() == false) {
            $view = new commentsFolderPostView($form);
        } else {
            $parent_id = $this->request->get('id', 'integer', SC_PATH);
 
            $commentsMapper = $this->toolkit->getMapper('comments', 'comments', 'comments');
            $commentsFolderMapper = $this->toolkit->getMapper('comments', 'commentsFolder', 'comments');
 
            $commentsFolder = $commentsFolderMapper->searchOneByField('parent_id', $parent_id);
 
            $values = $form->exportValues();
 
            $comment = $commentsMapper->create();
            $comment->setText($values['text']);
 
            $user = $this->toolkit->getUser();
 
            $comment->setAuthor($user->getId());
            $comment->setFolder($commentsFolder->getId());
 
            $commentsMapper->save($comment);
 
            $view = new commentsPostSuccessView($values['url']);
        }
 
        return $view;
    }
}
 
?>

А также создадим новый класс commentsPostSuccessView, который будет перенаправлять нас после удачного добавления комментария на исходную страницу, и поместим его в каталог views:

<?php
 
class commentsPostSuccessView extends simpleView
{
    public function toString()
    {
        header('Location: ' . $this->DAO);
        exit;
    }
}
 
?>

Если сейчас посмотреть в БД, то можно увидеть, что поле `time` не заполняется для создаваемых комментариев. Чтобы поправить это, добавим в commentsMapper следующий метод:

<?php
 
class commentsMapper extends simpleMapper
{
    /**
     * Выполнение операций с массивом $fields перед вставкой в БД
     *
     * @param array $fields
     */
    protected function insertDataModify(&$fields)
    {
        $fields['time'] = time();
    }
}
 
?>

После этого изменения у всех вновь добавляемых комментариев поле `time` будет устанавливаться автоматически.

Теперь давайте приступим к экшну `list` и выведем список комментариев. Для этого давайте вернёмся к схеме БД в файле commentsFolder.map.ini и отношением 1:N свяжем сущности "комментарий" и "папка с комментариями". В commentsFolder.map.ini добавим ещё одно поле, теперь этот файл будет выглядеть так:

[id]
accessor = "getId"
mutator = "setId"
once = true
 
[parent_id]
accessor = "getParentId"
mutator = "setParentId"
 
[comment]
accessor = "getComments"
mutator = "setComments"
 
hasMany = "id->comments.folder_id"

Также подправим контроллер, отображение и шаблон для метода list соответственно:

<?php
 
class commentsFolderListController extends simpleController
{
    public function getView()
    {
        $commentsFolderMapper = $this->toolkit->getMapper('comments', 'commentsFolder', 'comments');
 
        $parent_id = $this->request->get('parent_id', 'integer', SC_PATH);
 
        $commentsFolder = $commentsFolderMapper->searchOneByField('parent_id', $parent_id);
 
        return new commentsFolderListView($commentsFolder);
    }
}
 
?>
<?php
 
class commentsFolderListView extends simpleView
{
    public function toString()
    {
        $this->smarty->assign('parent_id', $this->DAO->getParentId());
        $this->smarty->assign('comments', $this->DAO->getComments());
        $this->smarty->assign('folder', $this->DAO);
        return $this->smarty->fetch('comments.list.tpl');
    }
}
 
?>
{foreach from=$comments item=comment}
    {$comment->getAuthor()}, {$comment->getTime()|date_format:"%e %B %Y / %H:%M"}
    {$comment->getText()}
    <hr>
{/foreach}
{$folder->getJip()}
{load module="comments" section="comments" action="post" parent_id=$parent_id}

Как видите - заодно мы вывели jip для commentsFolder. Теперь по обновлению страницы вы должны увидеть список комментариев. Однако, как видно из шаблона и непосредственно в списке комментариев, вместо логина автора комментария у нас выводится его id. Свяжем соотношением 1:1 комментарий и пользователей, а также комментарий и "папку с комментариями". сomments.map.ini и шаблон соответственно изменятся:

[id]
accessor = "getId"
mutator = "setId"
once = true
 
[text]
accessor = "getText"
mutator = "setText"
 
[author]
accessor = "getAuthor"
mutator = "setAuthor"
 
owns = "user.id"
section = "user"
module = "user"
 
 
[time]
accessor = "getTime"
mutator = "setTime"
 
[folder_id]
accessor = "getFolder"
mutator = "setFolder"
 
owns = "commentsFolder.id"
{foreach from=$comments item=comment}
    {$comment->getAuthor()->getLogin()}, {$comment->getTime()|date_format:"%e %B %Y / %H:%M"}
    {$comment->getText()|htmlspecialchars}
    <hr>
{/foreach}
{load module="comments" section="comments" action="post" parent_id=$news->getObjId()}

Также для удобства можно немного изменить класс commentsFolderPostController:

<?php
 
fileLoader::load('comments/views/commentsFolderPostView');
fileLoader::load('comments/views/commentsPostForm');
fileLoader::load('comments/views/commentsPostSuccessView');
 
class commentsFolderPostController extends simpleController
{
    public function getView()
    {
        $form = commentsPostForm::getForm($this->request->get('parent_id', 'integer', SC_PATH));
 
        if ($form->validate() == false) {
            $view = new commentsFolderPostView($form);
        } else {
            $parent_id = $this->request->get('id', 'integer', SC_PATH);
 
            $commentsMapper = $this->toolkit->getMapper('comments', 'comments', 'comments');
            $commentsFolderMapper = $this->toolkit->getMapper('comments', 'commentsFolder', 'comments');
 
            $commentsFolder = $commentsFolderMapper->searchOneByField('parent_id', $parent_id);
 
            $values = $form->exportValues();
 
            $comment = $commentsMapper->create();
            $comment->setText($values['text']);
 
            $user = $this->toolkit->getUser();
 
            $comment->setAuthor($user);
            $comment->setFolder($commentsFolder);
 
            $commentsMapper->save($comment);
 
            $view = new commentsPostSuccessView($values['url']);
        }
 
        return $view;
    }
}
 
?>

Теперь выводятся логины авторов сообщений. Осталось реализовать методы delete и edit. Продолжим. Новый метод edit создаём как обычно, но доменный объект укажем не commentsFolder, а comments. Затем добавим это действие в jip для объекта comments:

[edit]
controller = "edit"
jip = "1"
icon = "/templates/images/edit.gif"
title = "Редактировать"

Также модифицируем шаблон списка комментариев, добавив {$comment->getJip()}, что выведет в указанном месте иконку с выпадающим меню jip, в котором будут 2 пункта: "Редактирование" и "Права доступа". Если попробовать изменить права доступа к любому из комментариев - мы увидим сообщение о том, что нет доступа. Это потому, что для comments мы ещё не сделали создание прав по умолчанию. Делается это по известной уже схеме: в таблицах `sys_obj_id_named` и `sys_access_registry` мы смотрим obj_id и class_section_id добавляем запись в `sys_access` запись со значениями: `action_id` = 18 (editDefault), `class_section_id` = 7, `obj_id` = 86, `uid` = 2, `allow` = 1. Теперь открываем урл

http://mzz/access/comments/comments/editDefault

И в правах по умолчанию для автора прописываем действия edit и editACL. Добавьте новый комментарий и попробуйте изменить для него права доступа. Если всё сделано верно - то ошибок быть не должно. Теперь открываем в новом окне урл

http://mzz/comments/7/edit

где 7 - id только что созданного комментария. На экране должна появиться ошибка:

Runtime Exception. Thrown in file D:\server\sites\mzz\system\controller\sectionMapper.php (Line: 81) with message:
Не найден активный шаблон для section = "comments", action = "edit"

По уже известной методике создадим новый активный шаблон comments/edit.tpl.

{load module="comments" action="edit"}

После создания активного шаблона увидим:

Invalid Parameter. Thrown in file D:\server\sites\mzz\system\acl\acl.php (Line: 803) with message:
Свойство obj_id должно быть целочисленного типа и иметь значение > 0 (0)

Открываем commentsMapper и модифицируем его:

<?php
 
class commentsMapper extends simpleMapper
{
    [...]
    /**
     * Возвращает уникальный для ДО идентификатор исходя из аргументов запроса
     *
     * @return object
     */
    public function convertArgsToId($args)
    {
        $comment = $this->searchOneByField('id', $args['id']);
        return $comment->getObjId();
    }
}
 
?>

Обновляем страницу. Видим:

Runtime Exception. Thrown in file D:\server\sites\mzz\system\template\mzzFileSmarty.php (Line: 50) with message:
Шаблон 'D:\server\sites\mzz\www/templates/comments/comments.edit.tpl' отсутствует.

Замечу также, что файл commentsEditView.php нам не нужен (мы будем использовать класс commentsPostForm вместо него). Так что его можно удалить. Затем модифицируем файлы commentsEditController.php, commentsPostForm.php, comments/edit.tpl, commentsMapper.php, commentsFolderPostView, comments.post.tpl. Теперь они будут выглядеть так (в том же порядке):

<?php
 
fileLoader::load('comments/views/commentsFolderPostView');
fileLoader::load('comments/views/commentsPostForm');
 
class commentsEditController extends simpleController
{
    public function getView()
    {
        $id = $this->request->get('id', 'integer', SC_PATH);
        $commentsMapper = $this->toolkit->getMapper('comments', 'comments', 'comments');
 
        $comment = $commentsMapper->searchById($id);
 
        $form = commentsPostForm::getForm($id, 'edit', $comment);
 
        if ($form->validate() == false) {
            $view = new commentsFolderPostView($form);
        } else {
            $values = $form->exportValues();
 
            $comment->setText($values['text']);
            $commentsMapper->save($comment);
 
            $view = new simpleJipCloseView();
        }
 
        return $view;
    }
}
 
?>
<?php
 
class commentsPostForm
{
    static function getForm($parent_id, $action = 'post', $comment = null)
    {
        fileLoader::load('libs/PEAR/HTML/QuickForm');
        fileLoader::load('libs/PEAR/HTML/QuickForm/Renderer/ArraySmarty');
 
        $url = new url();
        $url->addParam($parent_id);
        $url->setAction($action);
 
        $form = new HTML_QuickForm('post', 'POST', $url->get());
 
        if ($action == 'edit') {
            $defaultValues = array();
            $defaultValues['text']  = $comment->getText();
            $form->setDefaults($defaultValues);
        }
 
        $form->addElement('textarea', 'text', 'Ваш комментарий', 'rows=7 cols=50');
 
        $toolkit = systemToolkit::getInstance();
        $request = $toolkit->getRequest();
 
        $form->addElement('hidden', 'url', $request->get('REQUEST_URI', 'string', SC_SERVER));
 
        $form->addElement('reset', 'reset', 'Отмена', 'onclick="javascript: hideJip();');
        $form->addElement('submit', 'submit', 'Отправить');
 
        return $form;
    }
}
 
?>
{* main="popup.tpl" placeholder="content" *}
{load module="comments" action="edit"}
<?php
 
class commentsMapper extends simpleMapper
{
    /**
     * Имя модуля
     *
     * @var string
     */
    protected $name = 'comments';
 
    /**
     * Имя класса DataObject
     *
     * @var string
     */
    protected $className = 'comments';
 
    /**
     * Выполнение операций с массивом $fields перед вставкой в БД
     *
     * @param array $fields
     */
    protected function insertDataModify(&$fields)
    {
        $fields['time'] = time();
    }
 
    public function searchById($id)
    {
        return $this->searchOneByField('id', $id);
    }
 
    /**
     * Возвращает уникальный для ДО идентификатор исходя из аргументов запроса
     *
     * @return object
     */
    public function convertArgsToId($args)
    {
        $comment = $this->searchOneByField('id', $args['id']);
        return $comment->getObjId();
    }
}
 
?>
<?php
 
class commentsFolderPostView extends simpleView
{
    private $action;
 
    public function __construct($form, $action = 'post')
    {
        $this->action = $action;
        parent::__construct($form);
    }
 
    public function toString()
    {
        $renderer = new HTML_QuickForm_Renderer_ArraySmarty($this->smarty, true);
        $this->DAO->accept($renderer);
 
        $this->smarty->assign('action', $this->action);
        $this->smarty->assign('form', $renderer->toArray());
 
        return $this->smarty->fetch('comments.post.tpl');
    }
}
 
?>
<form {$form.attributes} {if $action eq 'edit'}onsubmit="return sendFormWithAjax(this);return false;"{/if}>
{$form.hidden}
{$form.javascript}
    <table border="0" cellpadding="0" cellspacing="1" width="50%">
        <tr>
            <td>{$form.text.label}</td>
        </tr>
        <tr>
            <td>{$form.text.html}</td>
        </tr>
        <tr>
            <td>{$form.submit.html}{$form.reset.html}</td>
        </tr>
    </table>
</form>

Если всё выполнено верно - то теперь метод edit должен работать. Остася финальный рывок - метод delete. Он не составит больших проблем для вас. Создаём очередной экшн для сущности comments. Также добавляем его в jip:

[delete]
controller = "delete"
jip = "1"
icon = "/templates/images/delete.gif"
title = "Удалить"
confirm = "Вы хотите удалить этот комментарий?"

Добавляем активный шаблон comments/delete.tpl:

{load module="comments" action="delete"}

Теперь даём администратору права на удаление какого-либо комментария и пробуем его удалить. Если всё сделано верно - то выбранный комментарий должен быть удалён.

Ну и напоследок - забытый в начале, но несомненно важный функционал - удаление комментариев при удалении комментируемого объекта. В реализации конечно же нет ничего сложного. Создаём новый экшн - deleteFolder. В отличие от всех остальных экшнов этот не будет доступен извне. Другими словами этот метод может быть только вызван в другом шаблоне. Поэтому создавать новый активный шаблон, а также прописывать новое действие в таблицу `sys_actions` и ассоциировать его с этим классом - не нужно. Также нужно прописать для этого экшна inACL = 0 в commentsFolder.ini, для того чтобы ACL действительно его не включил в список доступных методов. Вызов метода будет выглядеть вот так:

{load module="comments" section="comments" action="deleteFolder" 403handle="manual"}

Последний аргумент 403handle="manual" здесь добавлен для потому, что acl для этого метода всегда будет возвращать false, потому что этого метода у этого класса для acl не существует (мы его не прописали). Контроллер для удаления будет выглядеть следующим образом:

<?php
 
class commentsFolderDeleteFolderController extends simpleController
{
    public function getView()
    {
        $commentsFolderMapper = $this->toolkit->getMapper('comments', 'commentsFolder', 'comments');
        $commentsMapper = $this->toolkit->getMapper('comments', 'comments', 'comments');
 
        $criteria = new criteria();
        $criteria->addJoin('sys_access_registry', new criterion('r.obj_id', 'commentsfolder.parent_id', criteria::EQUAL, true), 'r');
        $criteria->add('r.obj_id', null, criteria::IS_NULL);
        $commentsFolders = $commentsFolderMapper->searchAllByCriteria($criteria);
 
        foreach ($commentsFolders as $val) {
            $commentsFolderMapper->remove($val->getId());
        }
 
        return '';
    }
}
 
?>

Т.е. вначале мы ищем все commentsFolder'ы, которые ссылаются на уже не существующие объекты, и затем в цикле удаляем эти объекты. Как можно заметить - удаление производится не методом simpleMapper::delete(), а remove. Приведём код этого метода:

<?php
 
class commentsFolderMapper extends simpleMapper
{
    [...]
 
    /**
     * Удаление папки вместе с содержимым на основе id
     *
     * @param string $id
     * @return void
     */
    public function remove($id)
    {
        $toolkit = systemToolkit::getInstance();
 
        $commentsMapper = $toolkit->getMapper('comments', 'comments', 'comments');
 
        foreach ($commentsMapper->searchAllByField('folder_id', $id) as $comment) {
            $commentsMapper->delete($comment->getId());
        }
 
        $this->delete($id);
    }
 
    [...]
}
 
?>

Также не нужно забывать, что вызов этого же экшна нужно добавить и в активный шаблон news/deleteFolder.tpl.

5.2.9. Подведение итогов

Давайте подведём итог проделанной работы. В течение нескольких часов мы написали достаточно универсальный модуль, который теперь без лишних трудозатрад можно запустить в контексте другого модуля, таким образом расширив функциональность, не прибегая к каким либо модификациям кода модуля. Продемонстрируем это на практике: добавьте в шаблон отображения конкретной страницы (page.view.tpl) следующую строку и откройте любой объект модуля Page, т.е. страницу.

{load module="comments" section="comments" action="list" parent_id=$page->getObjId()}

Однако стоит заметить, что эта запись не совсем корректна, поскольку автором будет установлен тот, кто ПЕРВЫМ откроет указанную страницу. Это устраняется добавлением поля author в модуль Page и передачей значения этого поля в модуль Comments.

Снизу должно появиться поле для ввода. Теперь страницы, как и новости, комментируемы. И так - для любого из объектов. Удобно, не правда ли? ;)

Подобные модули мы решили назвать addon'ы, потому как сами по себе они никакой смысловой нагрузки не несут, однако могут быть дополнениями к существующим модулям.

И в заключении - а зачем же был нужен commentsFolder? Как вы заметили - commentsFolder является неким "контейнером" для комментариев. Именно сущносте commentsFolder принадлежат методы list и post. Также, изменяя права на commentsFolder, можно управлять правами - кому можно добавлять комментарии для данного объекта, а кому - нет, а также устанавливать права по умолчанию для вновь создаваемых комментариев.

5.3. Написание модуля "Сообщения"

5.3.1. Введение

В этой главе будет описан процесс создания модуля messages, предназначенного для обмена пользователями сообщениями.

5.3.2. Планирование

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

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

В систему будет добавлено следующие 2 сущности:

Теперь давайте создадим эти сущности. Для этого откроем devToolbar (по умолчанию урл будет такой: http://mzz/admin/devToolbar). Создадим новый модуль message. В результате этого действия в каталоге с активными шаблонами будет создан подкаталог message, а в каталоге с модулями - необходимая иерархия каталогов для модуля. Теперь в этот модуль добавим 2 сущности: message и messageCategory. Также создадим новый раздел message и зарегистрируем в нём только что созданный модуль.

5.3.3. Структура БД

Таблица для сущности message будет содержать следующие поля:

CREATE TABLE `message_message` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL,
  `text` text,
  `sender` int(11) DEFAULT NULL,
  `recipient` int(11) DEFAULT NULL,
  `time` int(11) DEFAULT NULL,
  `watched` tinyint(4) DEFAULT NULL,
  `category_id` int(11) DEFAULT NULL,
  `obj_id` int(11) UNSIGNED DEFAULT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Для сущности messageCategory таблица будет несколько проще:

CREATE TABLE `message_messageCategory` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` char(255) DEFAULT NULL,
  `name` char(20) DEFAULT NULL,
  `obj_id` int(11) UNSIGNED DEFAULT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Теперь вернёмся снова в devToolbar, и для каждой из сущностей откроем редактор map-файла. В результате message.map.ini и messageCategory.map.ini будут сгенерированы на основе таблиц в БД. Если всё выполнено верно, то файлы будут выглядеть так:

[id]
accessor = "getId"
mutator = "setId"
 
[title]
accessor = "getTitle"
mutator = "setTitle"
 
[text]
accessor = "getText"
mutator = "setText"
 
[sender]
accessor = "getSender"
mutator = "setSender"
 
[recipient]
accessor = "getRecipient"
mutator = "setRecipient"
 
[time]
accessor = "getTime"
mutator = "setTime"
 
[watched]
accessor = "getWatched"
mutator = "setWatched"
 
[category_id]
accessor = "getCategory_id"
mutator = "setCategory_id"
[id]
accessor = "getId"
mutator = "setId"
 
[title]
accessor = "getTitle"
mutator = "setTitle"
 
[name]
accessor = "getName"
mutator = "setName"

Теперь свяжем сообщения с категорями, а пользователей - с сущностью пользователь системы, а также переименуем методы для category_id в более удобные:

[id]
accessor = "getId"
mutator = "setId"
 
[title]
accessor = "getTitle"
mutator = "setTitle"
 
[text]
accessor = "getText"
mutator = "setText"
 
[sender]
accessor = "getSender"
mutator = "setSender"
owns = "user.id"
module = "user"
section = "user"
 
[recipient]
accessor = "getRecipient"
mutator = "setRecipient"
owns = "user.id"
module = "user"
section = "user"
 
[time]
accessor = "getTime"
mutator = "setTime"
 
[watched]
accessor = "getWatched"
mutator = "setWatched"
 
[category_id]
accessor = "getCategory"
mutator = "setCategory"
owns = "messageCategory.id"

5.3.4. Программирование действий

Начнём с действия list. Это действие будет показывать список сообщений в данной категории. Для начала создадим необходимые нам категории: "Входящие", "Исходящие", "Корзина". Для этого в devToolbar сгенерируем 3 obj_id для объектов типа messageCategory. И добавим в таблицу message_messageCategory 3 записи с этими идентификаторами. Также добавим одно сообщение, поместим его в категорию incoming (в рассматриваемом примере у этой категории id = 1). Для сообщения укажем отправителем, допустим, гостя (id = 1), а получаетелем - себя (id = 2). В вашем случае id могут не совпадать. Для сообщения по известной схеме сгенерируем obj_id и добавим всю эту информацию в таблицу message_message. Теперь добавим действие, в devToolbar. Теперь запросим урл http://mzz/message/incoming/list. Если всё прошло успешно будет надпись:

Автоматически сгенерированный шаблон
Модуль: message
Экшн: list
Путь до этого файла: templates\list.tpl

Т.к. просмотр списка сообщений будет всегда разрешён всем пользователям, отключим ACL для этого экшна. Для этого в активном шаблоне act/message/list.tpl пропишем 403handle="none" для этого метода. Теперь напишем код, который будет получать список сообщений:

<?php
 
class messageListController extends simpleController
{
    public function getView()
    {
        $name = $this->request->get('name', 'string'); // получаем имя категории
        $isSent = $name == 'sent';
 
        $messageCategoryMapper = $this->toolkit->getMapper('message', 'messageCategory'); // получаем маппер
        $messageCategory = $messageCategoryMapper->searchOneByField('name', $name); // ищем категорию
 
        if (empty($messageCategory)) { // если не нашли - отображаем 404 ошибку
            return $messageCategoryMapper->get404()->run();
        }
 
        $me = $this->toolkit->getUser(); // получаем текущего пользователя
 
        $messageMapper = $this->toolkit->getMapper('message', 'message'); // получаем маппер
        $criteria = new criteria(); // составляем критерий поиска
        $criteria->add('category_id', $messageCategory->getId()); // сообщения из текущей категории
        $criteria->add($isSent ? 'sender' : 'recipient', $me->getId()); // для текущего пользователя (если категория "исходящие", то текущий пользователь является отправителем)
        $messages = $messageMapper->searchAllByCriteria($criteria); // ищем все сообщения
 
        $messageCategories = $messageCategoryMapper->searchAll(); // ищем все категории
 
        // передаём полученные данные в шаблон
        $this->smarty->assign('messages', $messages);
        $this->smarty->assign('isSent', $isSent);
        $this->smarty->assign('categories', $messageCategories);
        $this->smarty->assign('messageCategory', $messageCategory);
        return $this->smarty->fetch('message/list.tpl');
    }
}
 
?>

Сам шаблон выглядит так (в начале добавлен блок {add...} для того, чтобы загрузить необходимые для использования jip файлы. Если бы в шаблоне встрачался хоть один вызов метода getJip(), это было бы произведено автоматически):

{add file="prototype.js"}
{add file="prototype_improvements.js"}
{add file="effects.js"}
{add file="dragdrop.js"}
{add file="popup.js"}
{add file="jip.css"}
{add file="jip.js"}
 
{title append=$messageCategory->getTitle()|htmlspecialchars}
{title append="Список сообщений"}
 
{foreach from=$categories item=category name=cat}
    {if $messageCategory->getName() ne $category->getName()}<a href="{url route=withAnyParam action=list section=message name=$category->getName()}">{$category->getTitle()|htmlspecialchars}</a>{else}{$category->getTitle()|htmlspecialchars}{/if}
    {if not $smarty.foreach.cat.last} | {/if}
{/foreach}
<br /><br />
<a href="{url route=default2 action=send section=message}" class="jipLink">отправить сообщение</a>
<br /><br />
 
<table border="0" width="99%" cellpadding="4" cellspacing="0" class="systemTable">
    <tr align="center">
        <td><strong>id</strong></td>
        <td><strong>сообщение</strong></td>
        <td><strong>{if $isSent}кому{else}от{/if}</strong></td>
        <td><strong>дата</strong></td>
    </tr>
    {foreach from=$messages item=message}
        <tr align="center">
            <td>{$message->getId()}</td>
            <td><a href="{url route=withId action=view section=message id=$message->getId()}">{if not $message->getWatched()}<b>{/if}{$message->getTitle()|htmlspecialchars}{if not $message->getWatched()}</b>{/if}</a>{$message->getJip()}</td>
            <td>{if $isSent}{$message->getRecipient()->getLogin()}{else}<a href="{url route=withAnyParam action=send section=message name=$message->getSender()->getLogin()}">{$message->getSender()->getLogin()}</a>{/if}</td>
            <td>{$message->getTime()|date_format:"%H:%M:%S %e-%m-%Y"}</td>
        </tr>
    {/foreach}
</table>

И теперь запрос урла http://mzz/message/incoming/list должен нам выдавать список сообщений в данной категории. Сверху списка располагаются список категорий.

Далее реализуем метод просмотра сообщений. Создадим экшн view. Это будет экшн сущности message. Право на просмотр сообщения будет определяться просто - если получателем является текущий пользователь (или в случае с исходящими сообщениями - наоборот отправитель), тогда доступ разрешён. Эта проверка будет осуществляться с помощью специального метода getAcl() в классе message:

<?php
 
class message extends simple
{
    [...]
 
    public function getAcl($name = null)
    {
        // у экшна 'delete' логика проверки авторизации будет такая же
        if ($name == 'view' || $name == 'delete') {
            // получаем id получателя сообщения (или отправителя, в случае с категорией 'sent'
            $user_id = ($this->getCategory()->getName() == 'sent') ? $this->getSender()->getId() : $this->getRecipient()->getId();
            // если id получателя/отправителя == id текущего пользователя - доступ есть
            return $user_id == systemToolkit::getInstance()->getUser()->getId();
        }
 
        return parent::getAcl($name);
    }
}
 
?>

Также, для того чтобы ACL работал, необходимо реализовать метод convertArgsToObj в messageMapper'е:

<?php
 
class messageMapper extends simpleMapper
{
    [...]
 
    public function convertArgsToObj($args)
    {
        $message = $this->searchByKey($args['id']);
        if ($message) {
            return $message;
        }
 
        throw new mzzDONotFoundException();
    }
}
 
?>

Реализуем контроллер:

<?php
 
class messageViewController extends simpleController
{
    public function getView()
    {
        $id = $this->request->get('id', 'integer'); // получаем id сообщения
        $messageMapper = $this->toolkit->getMapper('message', 'message'); // получаем маппер
        $message = $messageMapper->searchByKey($id); // получаем сообщение
 
        // если сообщение не найдено - показываем ошибку
        if (!$message) {
            return $messageMapper->get404()->run();
        }
 
        // если сообщение ещё не было просмотрено - устанавливаем флаг "просмотра" в 1
        if (!$message->getWatched()) {
            $message->setWatched(1);
            $messageMapper->save($message);
        }
 
        $category = $message->getCategory(); // получаем категорию сообщения
        $isSent = $category->getName() == 'sent';
 
        $messageCategoryMapper = $this->toolkit->getMapper('message', 'messageCategory'); // получаем маппер категорий
        $messageCategories = $messageCategoryMapper->searchAll(); // выбираем все категории
 
        // передаём данные в шаблон
        $this->smarty->assign('categories', $messageCategories);
        $this->smarty->assign('messageCategory', $category);
        $this->smarty->assign('isSent', $isSent);
 
        $this->smarty->assign('message', $message);
 
        return $this->smarty->fetch('message/view.tpl');
    }
}
 
?>

Шаблон:

{add file="prototype.js"}
{add file="prototype_improvements.js"}
{add file="effects.js"}
{add file="dragdrop.js"}
{add file="popup.js"}
{add file="jip.css"}
{add file="jip.js"}
 
{title append=$message->getTitle()|htmlspecialchars}
{title append="Просмотр сообщения"}
 
{foreach from=$categories item=category name=cat}
    <a href="{url route=withAnyParam action=list section=message name=$category->getName()}">{$category->getTitle()|htmlspecialchars}</a>
    {if not $smarty.foreach.cat.last} | {/if}
{/foreach}
<br /><br />
<strong>Тема:</strong> {$message->getTitle()|htmlspecialchars}<br />
{if $isSent}
    <strong>Получатель:</strong> {$message->getRecipient()->getLogin()}<br />
{else}
    <strong>Отправитель:</strong> {$message->getSender()->getLogin()}<br />
{/if}
<strong>Текст сообщения:</strong><br />{$message->getText()|htmlspecialchars|nl2br}<br />
<br />
{if not $isSent}
<a href="{url route=withAnyParam section=message action=send name=$message->getSender()->getLogin()}">ответить</a> | 
{/if}
<a href="{url route=withId section=message action=delete id=$message->getId()}" class="jipLink">удалить</a>

Теперь кликом на заголовке сообщения в списке сообщений либо по урлу http://mzz/message/1/view должно быть доступно созданное вручную сообщение. Теперь реализуем отправку сообщения, метод send у сущности messageCategory. Создадим его, и также пропишем на него 403handle="none". Этот экшн будет в jip-окне, поэтому поставим для него опцию "Добавить в JIP", а также из активного шаблона уберём загрузку в основной шаблон, оставив лишь {load module="message" action="send" 403handle="none"}. Откроем ссылку http://mzz/message/send. Форма отправки сообщения должна представлять собой поля для ввода темы и текста сообщения, а также выпадающего списка для выбора получателя сообщения. После отправки сообщения, копия должна поместиться в категорию "отправленные". Реализация этого экшна:

<?php
 
class messageSendController extends simpleController
{
    public function getView()
    {
        // получаем текущего пользователя
        $me = $this->toolkit->getUser();
 
        // из запроса - получаем имя получателя
        $recipient = $this->request->get('name', 'string');
 
        // ищем получателя
        $userMapper = $this->toolkit->getMapper('user', 'user', 'user');
        $recipient_user = $userMapper->searchByLogin($recipient);
        // если получатель был указан в УРЛе, но такого пользователя не существует - показываем ошибку
        if ($recipient && !$recipient_user) {
            $controller = new messageController('Получателя не существует', messageController::WARNING);
            return $controller->run();
        }
 
        // ищем пользователей, которым можно отправить сообщение (все, кроме текущего пользователя и гостя)
        $criteria = new criteria();
        $criteria->add('id', MZZ_USER_GUEST_ID, criteria::NOT_EQUAL);
        $criteria->setOrderByFieldAsc('login');
        $users = $userMapper->searchAllByCriteria($criteria);
        $usersArray = array();
        foreach ($users as $user) {
            $usersArray[$user->getId()] = $user->getLogin();
        }
        unset($usersArray[$me->getId()]);
 
        // составляем валидатор для формы
        $validator = new formValidator();
        $validator->add('required', 'message[title]', 'Необходимо указать тему сообщения');
        $validator->add('required', 'message[text]', 'Необходимо указать текст сообщения');
        $validator->add('required', 'message[recipient]', 'Необходимо указать получателя сообщения');
        $validator->add('callback', 'message[recipient]', 'Пользователь не найден', array('checkRecipient', $usersArray));
 
        // валидируем форму
        if ($validator->validate()) {
            // получаем данные сообщения
            $msg = $this->request->get('message', 'array', SC_POST);
 
            // получаем необходимые мапперы
            $messageMapper = $this->toolkit->getMapper('message', 'message');
            $messageCategoryMapper = $this->toolkit->getMapper('message', 'messageCategory');
 
            // ищем категории сообщений для отправленного и входящего сообщений
            $incoming = $messageCategoryMapper->searchOneByField('name', 'incoming');
            $sent = $messageCategoryMapper->searchOneByField('name', 'sent');
 
            // составляем сообщение, которое будет отправлено пользователю
            $message = $messageMapper->create();
            $message->setTitle($msg['title']);
            $message->setText($msg['text']);
            $message->setRecipient($msg['recipient']);
            $message->setSender($me);
            $message->setWatched(0);
            $message->setCategory($incoming);
 
            // делаем копию, помещаем её в "отправленные"
            $messageSent = $messageMapper->create();
            $messageSent->setTitle($msg['title']);
            $messageSent->setText($msg['text']);
            $messageSent->setRecipient($msg['recipient']);
            $messageSent->setSender($me);
            $messageSent->setWatched(1);
            $messageSent->setCategory($sent);
 
            // сохраняем оба сообщения
            $messageMapper->save($message);
            $messageMapper->save($messageSent);
 
            // закрываем jip-окно
            return jipTools::redirect();
        }
 
        // генерируем урл
        if ($recipient) {
            // если пользователь был указан, то урл будет вида: site/message/USERNAME/send
            $url = new url('withAnyParam');
            $url->add('name', $recipient);
        } else {
            // иначе: site/message/send
            $url = new url('default2');
        }
        $url->setSection('message');
        $url->setAction('send');
 
        // передаём в шаблон данные
        $this->smarty->assign('recipient', $recipient_user->getId());
        $this->smarty->assign('action', $url->get());
        $this->smarty->assign('errors', $validator->getErrors());
        $this->smarty->assign('users', $usersArray);
        return $this->smarty->fetch('message/send.tpl');
    }
}
 
// функция-валидатор, проверяющая что выбранный пользователь может являться получателем сообщения
function checkRecipient($user_id, $users)
{
    return isset($users[$user_id]);
}
 
?>
 

Также в messageMapper реализуем хуки, для автоматического добавления текущего времени в сообщение:

<?php
 
class messageMapper extends simpleMapper
{
    [...]
    /**
     * Выполнение операций с массивом $fields перед обновлением в БД
     *
     * @param array $fields
     */
    protected function updateDataModify(&$fields)
    {
        $fields['time'] = new sqlFunction('UNIX_TIMESTAMP');
    }
 
    /**
     * Выполнение операций с массивом $fields перед вставкой в БД
     *
     * @param array $fields
     */
    protected function insertDataModify(&$fields)
    {
        $this->updateDataModify($fields);
    }
}
 
?>

Шаблон для этого экшна будет выглядеть так:

<div class="jipTitle">Отправка сообщения</div>
<form action="{$action}" method="post" onsubmit="return jipWindow.sendForm(this);">
    <table width="100%" border="0" cellpadding="5" cellspacing="0" align="center">
        <tr>
            <td style='width: 20%;'>{form->caption name="message[recipient]" value="Получатель"}</td>
            <td style='width: 80%;'>{form->select name="message[recipient]" options=$users value=$recipient}{$errors->get('message[recipient]')}</td>
        </tr>
        <tr>
            <td style='width: 20%;'>{form->caption name="message[title]" value="Тема"}</td>
            <td style='width: 80%;'>{form->text name="message[title]" size="60"}{$errors->get('message[title]')}</td>
        </tr>
        <tr>
            <td style='width: 20%;'>{form->caption name="message[text]" value="Текст"}</td>
            <td style='width: 80%;'>{form->textarea name="message[text]" rows="6" cols="50"}{$errors->get('message[text]')}</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>{form->submit name="submit" value="Сохранить"} {form->reset jip=true name="reset" value="Отмена"}</td>
        </tr>
    </table>
</form>

Ну и последний экшн, удаление сообщения. Создаём его по уже известной схеме, но дополнительно - укажем для него иконку delete.gif и сообщение "Вы действительно хотите удалить сообщение?". При удалении сообщения - оно перемещается в категорию "Удалённые", при удалении из этой категории - оно удаляется безвозвратно. Проверку прав на удаление сообщения мы уже реализовали ранее. Удаление сообщения будет вызываться из jip-меню, из списка сообщений. Добавим соответствующий функционал в шаблон list.tpl. Интересующая нас строка будет выглядеть следующим образом:

<td><a href="{url route=withId action=view section=message id=$message->getId()}">{if not $message->getWatched()}<b>{/if}{$message->getTitle()|htmlspecialchars}{if not $message->getWatched()}</b>{/if}</a>{$message->getJip()}</td>

Сам шаблон delete.tpl, сгенерированный автоматически, следует удалить, т.к. использоваться он не будет. Сама реализация экшна:

<?php
 
class messageDeleteController extends simpleController
{
    public function getView()
    {
        // получаем id удаляемого сообщения и само сообщение
        $id = $this->request->get('id', 'integer');
        $messageMapper = $this->toolkit->getMapper('message', 'message');
        $message = $messageMapper->searchByKey($id);
 
        // если сообщение не найдено - показываем ошибку
        if (!$message) {
            return $messageMapper->get404()->run();
        }
 
        // если сообщение находится не в категории "удалённые" - перемещаем его туда
        if ($message->getCategory()->getName() != 'recycle') {
            $messageCategoryMapper = $this->toolkit->getMapper('message', 'messageCategory');
            $recycle = $messageCategoryMapper->searchOneByField('name', 'recycle');
            $message->setCategory($recycle);
            $messageMapper->save($message);
        } else {
            // если уже в "удалённых" - тогда удаляем окончательно
            $messageMapper->delete($message->getId());
        }
 
        // закрываем jip-окно
        return jipTools::redirect();
    }
}
 
?>

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

5.3.5. Подведение итогов

Таким образом за это время мы написали систему обмена сообщений между пользователями. Система примитивная, но выполняющая свои задачи. Естественно при желании она может быть расширена и дополнена.

В приведённом примере во всех экшнах проверка прав осуществлялась вручную. Это обусловлено тем, что в данном конкретном случае это правильнее и удобнее. О встроенных механизмах проверки прав вы можете прочитать в соответствующей главе, посвещённой ACL и о методе convertArgsToObj() в частности

5.4. Обработка ошибки 404 в модулях

Перехват и обработка ошибки 404 производится mzz автоматически. В случае, если не найден запрашиваемый объект, будет запущен simple404Controller, в обязанности которого входит отображение сообщения об ошибке. Заменить стандартное сообщение, можно двумя путями:

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

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

Не забывайте чистить кэш! Резолвер по умолчанию является кеширующим, и до того, как файл с кешем резолвера (tmp/resolver.cache) не будет удалён, по запросу файла simple404Controller.php будет резолвиться системный файл.

В этом случае будет изменено отображение 404 страницы для всего сайта.

2. Создание собственного класса-обработчика. Приведём пример для модуля меню:

<?php
 
class menu404Controller extends simpleController
{
    protected function getView()
    {
        return $this->smarty->fetch('menu/notfound.tpl');
    }
}
 
?>

Как видно из кода - этот контроллер только выводит заранее определённый шаблон. Однако в нём может быть запрограммирована сколь угодно сложная логика для обработки данной ситуации.

5.5. Постраничный вывод списков

Класс pager предназначен для постраничного вывода коллекций объектов. Он автоматически получает из запроса номер текущей страницы, и также автоматически добавляет в запрос LIMIT с нужными параметрами. Приведём прототип конструктора пейджера:

public function __construct($baseurl, $page, $perPage, $roundItems = 2, $reverse = false)

Для использования пейджера — необходимо подключить плагин pagerPlugin к мапперу, который будет получать коллекцию:

fileLoader::load('modules/pager/plugins/pagerPlugin');
$pager = new pager('/news', 10, 10);
$mapper->attach(new pagerPlugin($pager));
$newsArray = $newsMapper->searchAll();

Для упрощения процесса добавления пейджера, в класс simpleController добавлен специальный метод setPager:

<?php
 
class simpleController
{
    [...]
 
    /**
     * Метод установки пейджера для получаемой коллекции объектов
     *
     * @param mapper $item маппер, который возвращает требуемую коллекцию объектов
     * @param integer $per_page число объектов на странице
     * @param boolean $reverse флаг, изменяющий порядок страниц на противоположный (от больших к меньшим)
     * @param integer $round_items число выводимых номеров страниц рядом с текущим (Например: ... 4 5 6 _7_ 8 9 10 ... -> $roundItems = 3)
     * @return pager
     */
    public function setPager(mapper $mapper, $per_page = 20, $reverse = false, $round_items = 2)
    {
        fileLoader::load('modules/pager/plugins/pagerPlugin');
        $pager = new pager($this->request->getRequestUrl(), $this->request->getInteger('page', SC_REQUEST), $per_page, $round_items, $reverse);
        $mapper->attach(new pagerPlugin($pager));
 
        $this->smarty->assign('pager', $pager);
 
        return $pager;
    }
}
 
?>

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

<?php
 
class newsListController extends simpleController
{
    protected function getView()
    {
        $newsFolderMapper = $this->toolkit->getMapper('news', 'newsFolder');
        $path = $this->request->getString('name');
        $newsFolder = $newsFolderMapper->searchByPath($path);
 
        if (empty($newsFolder)) {
            return $this->forward404($newsFolderMapper);
        }
 
        $config = $this->toolkit->getConfig('news');
        $this->setPager($newsFolderMapper, $config->get('items_per_page'), true);
 
        $this->smarty->assign('news', $newsFolderMapper->getItems($newsFolder));
        $this->smarty->assign('folderPath', $newsFolder->getTreePath());
        $this->smarty->assign('rootFolder', $newsFolderMapper->searchByPath('root'));
        $this->smarty->assign('newsFolder', $newsFolder);
 
        return $this->smarty->fetch('news/list.tpl');
    }
}
 
?>

После этого - нужно передать пейджер в шаблон, в случае если пейджер устанавливается вручную. Если пейджер устанавливается методом simpleController'а, то этого делать не нужно. Затем в шаблоне следует расположить следующий код:

{$pager->toString()}

Это выведет в нужном месте пейджер. Сам шаблон пейджера находится в www/templates/pager.tpl, естественно изменять его вы можете как вам требуется:

{add file="pager.css"}
<span class="pageNumber">{if !is_null($pager->getPrev())}<a href="{$pager->getPrev()}">Предыдущая</a>{else}Предыдущая{/if}</span>
{foreach from=$pages item=current}
<span class="pageNumber">{if not empty($current.skip)}...{elseif not empty($current.current)}&nbsp;<strong>{$current.page}</strong>&nbsp;{else}&nbsp;<a href="{$current.url}">{$current.page}</a>&nbsp;{/if}</span>
{/foreach}
<span class="pageNumber">{if !is_null($pager->getNext())}<a href="{$pager->getNext()}">Следующая</a>{else}Следующая{/if}</span>

Часть 6. Работа с БД

6.1. Генератор SQL-запросов

Для генерации SQL-запросов в mzz предназначен специальный класс simpleSelect. SimpleSelect собирает запрос из составных частей, которыми являются объекты классов criteria. Рассмотрим простейший пример:

<?php
    $criteria = new criteria('table');
    $select = new simpleSelect($criteria);
    $select->toString(); // вернёт "SELECT * FROM `table`"
?>

Как видите - генератор запросов сам позаботился о помещении имени таблицы в обратные кавычки (`). Аналогичным образом вам не нужно заботиться об одинарных кавычках ('), в которые помещаются строковые константы, и об экранировании этих строковых констант.

Рассмотрим основные приёмы работы с генератором запросов (более полный вариант - как всегда смотрите в модульных тестах).

Criterion

Класс criterion является атомарной составляющей в генераторе запросов. Именно он хранит информацию об операндах и условиях их сравнения. Обычной практикой является передача ему первым аргументом - имени поля, вторым - строковой константы, имени другого поля, массива с данными (для случаев с IN и BETWEEN). Третьим аргументом является тип сравнения операндов. Четвёртым - флаг, обозначающий что второй операнд является полем (вследствие чего его нужно заключать в `, а не в ' и не экранировать). Примеры использования класса:

<?php
    $criterion = new criterion('field', array('value1', 'value2'), criteria::IN);
    $criterion->generate(); // "`field` IN ('value1', 'value2')"
 
    $criterion = new criterion('field', 'value', criteria::NOT_EQUAL);
    $criterion->generate(); // "`field` <> 'value'"
 
    $criterion = new criterion('field', '%q_', criteria::LIKE);
    $criterion->generate(); // "`field` LIKE '%q_'"
 
    $criterion = new criterion('field', '', criteria::IS_NULL );
    $criterion->generate(); // "`field` IS NULL"
 
    $criterion = new criterion('field', array(1, 10), criteria::BETWEEN);
    $criterion->generate(); // "`field` BETWEEN '1' AND '10'"
 
    $criterion = new criterion('field', 'field2', criteria::EQUAL, true);
    $criterion->generate(); // "`field` = `field2`"
?>

Также объекты criterion можно объединять друг с другом посредством методов criterion::addAnd() и criterion::addOr(), обозначающих соответственно связь посредством логических and и or.

<?php
    $criterion = new criterion('field', 'value');
    $criterion->addAnd(new criterion('field2', 'value2'));
    $criterion->generate(); // "(`field` = 'value') AND (`field2` = 'value2')
 
 
 
 
    $criterion = new criterion();
 
    $cr1 = new criterion('field1', 'value1');
    $cr2 = new criterion('field2', 'value2');
    $cr3 = new criterion('field2', 'value3');
    $cr4 = new criterion('field4', 'value4', criteria::GREATER_EQUAL);
    $cr5 = new criterion('field5', 'value5', criteria::LESS_EQUAL);
 
    $cr2->addOr($cr3);
    $cr2->generate(); // "(`field2` = 'value2') OR (`field2` = 'value3')"
 
    $cr1->addAnd($cr2);
    $cr1->generate(); // "(`field1` = 'value1') AND ((`field2` = 'value2') OR (`field2` = 'value3'))"
 
    $cr4->addAnd($cr5);
    $cr4->generate(); // "(`field4` >= 'value4') AND (`field5` <= 'value5')"
 
    $criterion->add($cr1);
    $criterion->generate(); // "((`field1` = 'value1') AND ((`field2` = 'value2') OR (`field2` = 'value3')))"
 
    $criterion->addOr($cr4);
    $criterion->generate(); // "((`field1` = 'value1') AND ((`field2` = 'value2') OR (`field2` = 'value3'))) OR ((`field4` >= 'value4') AND (`field5` <= 'value5'))"
?>

Метод criteria::generate() здесь вызывается в демонстрационных целей - вам не придётся его вызывать вручную, эту работу выполняет сам simpleSelect.

6.2. Функции в генераторе

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

<?php
 
// простые функции без аргументов:
$sqlFunction = new sqlFunction('Unix_Timestamp');
echo $sqlFunction->toString(); // UNIX_TIMESTAMP()
 
// функции с несколькими аргументами
$sqlFunction = new sqlFunction('Function', array('field' => true, "value", 3));
echo $sqlFunction->toString(); // FUNCTION(`field`, 'value', 3)
 
// автоматическое добавление ` для имён таблиц и полей:
$sqlFunction = new sqlFunction('Function', 'table.field', true);
echo $sqlFunction->toString(); // FUNCTION(`table`.`field`)"
 
// составление композиций из нескольких функций
$function1 = new sqlFunction('Function_1', 'table.field', true);
 
$function2_arguments = array('table.field' => true, 'value');
$function2 = new sqlFunction('Function_2', $function2_arguments);
 
$arguments = array($function1, $function2, 'value', 'field' => true);
$sqlFunction = new sqlFunction('Function', $arguments);
echo $sqlFunction->toString() // FUNCTION(FUNCTION_1(`table`.`field`), FUNCTION_2(`table`.`field`, 'value'), 'value', `field`)
 
?>

6.3. Операторы в генераторе

Для оперирования операторами mysql в запросе используется класс sqloperator. Этот класс поддерживает вложенное использование операторов, а также автоматический контроль приоритетов операторов (путём добавления в нужные места скобок).Приведём несколько примеров использования, характеризующие типичное применение этого класса (естественно, наиболее полный набор вариантов использования можно посмотреть в тестах на этот класс):

<?php
 
// примитивный оператор
$sqlOperator = new sqlOperator('+', array(1, 2));
echo $sqlOperator->toString(); // 1 + 2
 
// использование с несколькими операндами
$sqlOperator = new sqlOperator('-', array('table.field', 'field2', 1 , 2));
echo $sqlOperator->toString(); // `table`.`field` - `field2` - 1 - 2
 
// организация вложенности
$operatorNested = new sqlOperator('+', array(1, 2));
$operatorNested2 = new sqlOperator('/', array($operatorNested, 'field'));
 
$sqlOperator = new sqlOperator('*', array($operatorNested2, $operatorNested));
echo $sqlOperator->toString(); // ((1 + 2) / `field`) * (1 + 2)
 
// использование совместно с sqlFunction
$sqlOperator = new sqlOperator('-', array(new sqlFunction('NOW'), new sqlOperator('INTERVAL', array('1 DAY'))));
echo $sqlOperator->toString(); // NOW() - INTERVAL 1 DAY
 
?>

6.4. Работа с древовидными структурами

Для хранения и работы с древовидными структурами используется класс dbTreeNS. Этот класс обеспечивает работу с деревьями по технологии Nested Sets. Причём в одной таблице со структурой дерева может храниться несколько деревьев.
В работе используется 2 таблицы, назовем их условно - tree и data.

Вся работа с деревьями осуществляется через simpleMapperForTree и доменные объекты типа simpleForTree. simpleMapperForTree предоставляет следующие методы:

Остальные методы этого класса наследуются от simpleMapper'a.

У класса simpleForTree есть следующие методы для работы с деревьями:

В остальном работа с классом simpleForTree аналогична работе с любым наследником класса simple.

Настройка дерева производится с помощью переопределения метода simpleMapperForTree::getTreeParams(), который по умолчанию возвращает массив следующего вида:

protected function getTreeParams()
{
        return array('nameField' => 'name', 'pathField' => 'path', 'joinField' => 'parent', 'tableName' => $this->table . '_tree', 'treeIdField' => 'tree_id');
}

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

Часть 7. Стандарты написания кода

7.1. Основы

Код mzz требуется оформлять в соответствии с этим стандартом, разработчикам проектов на mzz рекомендуется применять их тоже. Стандарты написаны на основе собственного опыта и нескольких других стандартов.

Все PHP-файлы должны быть сохранены в кодировке UTF-8 с расширением ".php" без BOM-метки.

Если PHP теги являются началом и/или концом файла, то не делайте переноса строк и пробелов перед ними и после.

Используйте для отступов 4 пробела. Символ табуляции не допускается.

Строки рекомендуется заканчивать символом перевода на новую строку с возвратом каретки (CRLF). Не используйте перенос в стиле Unix (LF).

7.2. Соглашения об именах

7.2.1. Имена файлов

Имена файлов, как и имена папок, могут состоять только из латинских букв верхнего или нижнего регистра, чисел или знаков подчеркивания ("_"). Содержащие PHP-код файлы должны иметь расширение ".php".

Примеры правильных имен:

core.php
modules/news/controller/newsEditController.php

Если PHP-файл содержит класс, то его имя должно совпадать с именем этого класса.

7.2.2. Классы

В одном файле может быть определен только один класс.

Имена классов выбираются в зависимости от решаемых им задач. Старайтесь выбрать наиболее подходящее название, раскрывающее суть класса. Для этого можно использовать латинские буквы. Знак подчеркивания ("_") не рекомендуется. Обычно имя класса начинается со строчной буквы. Иерархия классов также отражается на их именах, каждый уровень отделяется заглавной буквой.

Примеры правильных имен:

newsDeleteController
httpRequest
adminMapper

Класс может быть определен как абстрактный (abstract class core) или как финальный (final class core). Подумайте прежде чем объявлять класс финальным.

Свойства класса должны быть определены как public, private, или protected. Использование var (который хоть и является алиасом public), для указания доступа к свойству не допускается.

Пример класса:

class funnyAction
{
    const SOME_CONSTANT = 'value';
    public $foo;
    protected $some;
    private $bar = 'Default value';
}

7.2.3. Интерфейсы

Интерфейсы именуются так же, как классы, но первая буква имени обязательно "i".

Примеры имен интерфейсов:

iRequest
iResponse
iResolver

7.2.4. Функции и методы

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

Имена функций и методов должны быть оформлены в соответствии с camelCase-нотацией.

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

Функции должны иметь префикс в виде имени пакета для того, чтобы избежать проблем с функциями из других пакетов. Первая буква в имени должна быть в нижнем регистре, каждая первая буква "слова" - в верхнем.

Примеры имен функций (аналогично для методов):

function setTitle()
{
    //...
}
 
function resolve()
{
    //...
}

7.2.5. Переменные

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

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

class sample
{
    public $originalVar = 'foo';
 
    public function __construct($simpleView)
    {
        $test_var = $this->originalVar;
    }
}
$sample_object = new Sample;

7.2.6. Константы

Константы могут содержать латинские буквы, числа и символы нижнего подчеркивания.

Имена констант должны быть в верхнем регистре.

Имена констант в классах подчиняются таким же правилам.

7.3. Стиль написания кода

7.3.1. Обрамление PHP-кода

PHP-код должен всегда обрамлятся только полными PHP-тегами.

<?php
 
?>

7.3.2. Строки

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

$sample = 'world';

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

$sample = "world's";

Использовать heredoc-синтаксис запрещено.

Строки соединяются с другими строками или переменными с помощью оператора ".". Пробел должен всегда добавлятся до и после этого оператора:

$sample = 'hello ' . 'world';
$sample = $msg . "world's";

Если строка слишком длинная необходимо разрывать строку на несколько. Следующая строка должна быть дополнена пробелами так, чтобы оператор "." был выровнен под оператором "=":

$query = 'SELECT `a`.`name`, `a`.`id` FROM `sys_access_registry` `r`'
       . 'INNER JOIN `sys_classes_sections` `cs` ON `cs`.`id` = `r`.`class_section_id`'
       . 'INNER JOIN `sys_classes_actions` `ca` ON `ca`.`class_id` = `cs`.`class_id`'
       . 'INNER JOIN `sys_actions` `a` ON `a`.`id` = `ca`.`action_id`'
       . 'WHERE `r`.`obj_id` = ' . $this->obj_id;

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

$qry = 'SELECT `a`.`name`, `a`.`id` FROM `sys_access_registry` `r`
         INNER JOIN `sys_classes_sections` `cs` ON `cs`.`id` = `r`.`class_section_id`
          INNER JOIN `sys_classes_actions` `ca` ON `ca`.`class_id` = `cs`.`class_id`
           INNER JOIN `sys_actions` `a` ON `a`.`id` = `ca`.`action_id`
            WHERE `r`.`obj_id` = ' . $this->obj_id;

7.3.3. Ключевые слова

Ключевые слова должны быть строго в нижнем регистре:

true, false, null, new, class, function, ...

7.3.4. Массивы

Каждый элемент массива должен быть отделен от другого пробелом после запятой:

$sample = array(1);
$sample = array(1, 2, 'hello', "world's");
$sample = array(1, 2, 'hello', "world's",
                'cms', $a, $b, $c,
                $f, 'value', null, -1);

При указании ключа перед и после '=>' необходимо поставить пробел:

$sample = array('key' => 'value');

7.3.5. Классы

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

Каждый класс должен иметь блок комментариев в соответствии со стандартом PHPDocumentor.

Только один класс разрешен внутри одного PHP-файла.

Размещение дополнительно кода в файле с классом разрешено, но не приветствуется. В таких файлах, две пустые строки должны разделять класс и дополнительный PHP-код.

Пример класса:

/**
 * Блок комментариев
 */
class sample
{
    // содержимое класса
}

Любые переменные, определенные в классе, должны быть определены в начале класса, до определения любого метода.

Ключевое слово var не разрешено. Члены класса должны всегда определять их область видимости, используя ключевое слово private, protected или public. Доступ к переменным-членам класса напрямую используя префикс public разрешено, но не приветствуется в пользу методов.

7.3.6. Функции и методы

Методы должны всегда определять свою область видимости с помощью одного из префиксов private, protected или public.

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

Функции в глобальной области видимости крайне не приветствуются.

Пример определения метода:

/**
 * Блок комментариев
 */
class Foo
{
    /**
     * Блок комментариев
     */
    public function bar($arg, $name, $value = 'default')
    {
        // содержимое класса
    }
}

Возвращаемое значение не должно обрамляться в круглые скобки:

return $this->bar; // ПРАВИЛЬНО
return($this->bar); // НЕПРАВИЛЬНО

7.3.7. Управляющие структуры

Управляющие структуры, основанные на конструкциях if и else(if), должны иметь один пробел до открывающей круглой скобки условия, и один пробел после закрывающей круглой скобки.

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

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

if ($a != 2) {
    $a = 2;
}
 
if ($a != 2 && $b == 'value') {
    $a = 2;
} elseif ($a == 3) {
    $a = 4;
} else {
    $a = 7;
}

Для всех "if", "elseif" или "else" выражений необходимо обязательно использовать фигурные скобки.

Управляющие структуры написанные с использованием "switch" конструкции должны иметь один пробел до открывающей круглой скобки условного выражения, и также один пробел после закрывающей круглой скобки.

Содержимое каждого "case" выражения должно писаться с отступом в дополнительные четыре пробела.

switch ($value) {
    case 1:
        $a = 'b';
        break;
 
    case 2:
        break;
 
    default:
        break;
}

7.3.8. Комментарии

Все блоки комментариев должны быть совместимы с форматом phpDocumentor. Для получения дополнительной информации смотрите: http://phpdoc.org/

Подходят комментарии в стилях C (/* */) и C++ (//). Первые используются в определении классов, методов, функций и т.д., а вторые внутри методов, функций и в глобальной видимости. Использование комментариев в стиле Perl/shell (#) не допускается.

Все PHP-файлы должны содержать минимальный блок комментариев в качестве заголовка:

/**
 * $URL$
 *
 * MZZ Content Management System (c) 2005-2008
 * Website : http://www.mzz.ru
 *
 * This program is free software and released under
 * the GNU Lesser General Public License (See /docs/LGPL.txt).
 *
 * @link http://www.mzz.ru
 * @version $Id$
 */

Часть 8. Термины и определения

8.1. Общие

Секция (section)

Раздел сайта, служит своеобразным неймспейсом, позволяющий одному модулю оперировать разными наборами однотипных данных. Например: с помощью секций вы можете создать на сайте два форума, или 2 раздела для страниц (допустим, одни будут для отображения обычных страниц, а другой, служебный раздел, будет содержать информацию из служебных блоков на сайте).
Активный шаблон
Шаблон, располагающийся в каталоге "templates/act/section" (где section - имя раздела). Именно в этом шаблоне происходит определение основного конента страницы.
Сущность (доменный объект)
Набор данных (свойств), которые принадлежат какому-то элементу. Это может быть новость, страница, комментарий или профиль пользователя.
JIP-окно
Способ открытия страниц, который используется для административных действий. Открывается непосредственно на текущей странице и не требует перезагрузки окна браузера, т.к. используется AJAX.
JIP-меню
Используется для административных действий. Кнопка открытия JIP-меню находится рядом с каждым доменном объектом на сайте, у которого есть действия, помеченные как JIP, и к которым текущий пользователь имеет доступ. Открываются все действия в JIP-окне.