MZZ.Framework 1.0
Разделы

Часть 1 Введение

1.1 Введение

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

Краткий список особенностей MZZ:

1.2 Сообщество

Дополнительные ресурсы, где вы можете получить помощь по MZZ:

О найденных ошибках вы можете сообщить в баг-трекер http://trac.mzz.ru/

 

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

2.1 Исходный код

Исходные коды MZZ могут быть получены с официального сайта из раздела загрузки.

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

svn checkout svn://svn.mzz.ru/mzz/trunk

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

2.2 Системные требования

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

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

2.3 Установка MZZ на сервер

Для установки MZZ на сервер необходимо скачать его и распаковать в любую директорию, доступную для чтения веб-сервером. Далее вашему приложению необходимо указать этот путь в качестве системной директории (константа SYSTEM_PATH, см.далее). Нет необходимости настраивать доступ к этой директории через веб (это необходимо только для запуска тестов и чтения поставляемой документации).

2.4 Установка и конфигурирование demo-приложения

2.4.1 Скачивание

Исходные коды demo-приложения могут быть получены из SVN-репозитория:

svn checkout svn://svn.mzz.ru/mzz/apps/demo/trunk

2.4.2 Установка

Готово! Теперь demo-приложение доступно по адресу, указанному в настройке виртуального хоста!

2.5 Установка и конфигурирование dummy-приложения

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

Исходные коды dummy-приложения могут быть получены из SVN-репозитория:

svn checkout svn://svn.mzz.ru/mzz/apps/dummy/trunk

Установка dummy-приложения практически аналогична установке demo-приложения.

 

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

3.1 MZZ

Структура фреймворка:

3.2 Приложение

3.2.1 Обзор

Структура типичного приложения:

3.2.2 Конфигурационный файл

Типичный конфигурационный файл (config.php):

<?php
/**
 * Абсолютный путь до сайта.
 * Если mzz установлен в корень веб-сервера, оставьте поле пустым
 * Правильно: /mzz, /new/site
 * Неправильно: site1, site1/, /site1/
 */
define('SITE_PATH', '');
define('COOKIE_DOMAIN', '');
 
define('DEBUG_MODE', true);
 
// Путь до директории system в mzz
define('SYSTEM_PATH', realpath(dirname(__FILE__) . '/../system/'));
 
// Идентификатор записи в БД для неавторизированных пользователей
define('MZZ_USER_GUEST_ID', 1);
 
// Идентификатор группы, для которой ACL всегда будет возвращать true (т.е. предоставит полный доступ)
define('MZZ_ROOT_GID', 3);
 
require_once SYSTEM_PATH . DIRECTORY_SEPARATOR . 'systemConfig.php';
 
// Дефолтный язык приложения
systemConfig::$i18n = 'ru';
 
// Включаем мультиязычность
systemConfig::$i18nEnable = true;
 
// Устанавливаем дефолтную кодировку для выдачи
ini_set('default_charset', 'utf-8');
 
// Настройка соединения с БД приложения
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']['options'] = array();
systemConfig::$db['default']['options']['init_query'] = 'SET NAMES utf8';
 
// Установка переменных окружения
systemConfig::$appName = 'demo';
systemConfig::$appVersion = '1.0-alpha';
systemConfig::$enabledModules = array('captcha', 'comments', 'fileManager', 'menu', 'news', 'page');
systemConfig::$pathToApplication = dirname(__FILE__);
systemConfig::$pathToWebRoot = systemConfig::$pathToApplication . '/www';
systemConfig::$pathToTemp = systemConfig::$pathToApplication . '/tmp';
systemConfig::$pathToConfigs = systemConfig::$pathToApplication . '/configs';
 
// Настройка дефолтного мейлера
systemConfig::$mailer['default']['backend'] = 'PHPMailer';
systemConfig::$mailer['default']['params'] = array('html' => true, 'smtp' => true, 'smtp_host' => 'localhost');
 
/**
 * Здесь могут находиться другие системные настройки приложения, 
 * такие как настройки кеширования, шаблонизатора, сессий и др.
 */
 
systemConfig::init();
 
?>

Опишем параметры конфигурационного файла:

Директива Описание
define('DEBUG_MODE', ...);
Режим отладки.
define('SYSTEM_PATH', ...));
Абсолютный путь до system каталога MZZ.
systemConfig::$appName
systemConfig::$appVersion
Имя и версия вашего приложения.
systemConfig::$pathToApplication
Путь до каталога вашего приложения.
systemConfig::$pathToWebRoot
Путь до web-root вашего приложения.
systemConfig::$pathToTemp
Путь до tmp каталога приложения.
systemConfig::$pathToConf
Путь до каталога с конфигурационными файлами приложения.

3.2.3 Класс приложения

Типичный класс приложения (application.php):

<?php
class application extends core
{
 
}
?>

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

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

3.3 Модуль

3.3.1 Обзор

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

3.3.2 Класс модуля

newsModule.php представляет собой класс, описывающий модуль.

class newsModule extends simpleModule
{
    protected $classes = array(
        'news',
        'newsFolder');
 
    protected $roles = array(
        'moderator',
        'user');
 
    public function getRoutes()
    {
        return array(
            array(),
            array(
                'newsFolder' => new requestRoute('news/:name/:action', array(
                    'module' => 'news',
                    'name' => 'root',
                    'action' => 'list'), array(
                    'name' => '.*?',
                    'action' => '(?:list|create|createFolder|editFolder|deleteFolder|moveFolder)'))));
    }
}

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

Помимо всего прочего, данный класс отвечает за получение мапперов и действий модуля, что позволит, к примеру, реализовать нестандартную схему получения маппера для объекта или создать так называемые multi-action контроллеры (todo: сделать раздел доки tricks и описать там такой финт)

3.3.3 Actions

В директории actions расположены файлы, определяющие набор возможных действий с каждой из сущностей модуля. Для модуля news это будут файлы news.php и newsFolder.php:

news.php:

<?php
return array(
    'view' => array(
        'controller' => 'view'
    ),
    'edit' => array(
        'controller' => 'save',
        'jip' => 1,
        'role' => array('moderator'),
        'icon' => 'sprite:mzz-icon/page-text/edit',
        'lang' => true,
        'main' => 'active.blank.tpl'
    ),
    'move' => array(
        'controller' => 'move',
        'jip' => 1,
        'role' => array('moderator'),
        'icon' => 'sprite:mzz-icon/page-text/move'
    ),
    'delete' => array(
        'controller' => 'delete',
        'jip' => 1,
        'role' => array('moderator'),
        'icon' => 'sprite:mzz-icon/page-text/del',
        'confirm' => '_ news/confirm_delete',
        'main' => 'active.blank.tpl'
    ),
    'admin' => array(
        'role' => array('moderator'),
        'controller' => 'admin',
        'title' => '_ admin',
        'admin' => true
    )
);
?>

В этом файле определены 5 действий: view, edit, move, delete и admin. Назначение этих действий легко определяется по их названию.

Рассмотрим пример простейшего actions-конфига:

<?php
return array(
    'view' => array(
        'controller' => 'view',
    )
);
?>

Как видно из примера, действие имеет только один обязательный параметр — controller. Представляет собой имя контроллера, который будет обслуживать данное действие.

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

Имя переменной Описание Принимаемые значения
main

Main-шаблон для действия (todo: где описать для чего это? в процессе запуска приложения или прям тут?). Может принимать следующие значения:

  • active.main.tpl — результат работы действия будет включено в main.tpl шаблон
  • active.blank.tpl — результат работы действия будет выведено на экран без main.tpl шаблона
  • active.admin.tpl — результат работы действия будет выведен в интерфейсе административной части приложения
  • deny — данный экшн запрещен к вызову напрямую через адресную строку браузера.

Если данный параметр не задан, то по-умолчанию будет использоваться active.main.tpl шаблон.

string
jip Является ли действие действием JIP (todo: ссылка на описание JIP). Чтобы у модели было JIP-меню, в маппере необходимо подключить плагин jip. int
jip_target Указывает для JIP будет ли действие открываться в JIP-окне (по умолчанию) или будет открываться в новом окне/вкладке (значение 'new'). string
icon Иконка действия (используется для JIP) (todo: ссылка на описание). string
confirm Сообщение перед выполнением действия (используется для JIP). string
title Заголовок действия (используется для JIP). Если строка начинается со знака "_", то значение возмется из i18n. string
admin Является ли действие действием admin-части фреймворка. Если флаг выставлен в true, то данное действие будет отображаться в административной части приложения (/admin), где дополнительно появится пункт в меню с названием этого действия, параметр main будет выставлен в 'active.admin.tpl' (если не был выставлен в какое-то значение) bool
role Массив или строка ролей модуля, которым будем разрешен доступ к этому действию. Если данный параметр не указан, то доступ к действию разрешен всем. (todo: ссылка на описание механизма ролей) array|string
route_name Имя роута, который будет использоваться для построения URL'а этого действия, если используется JIP. string
route.myparam Значение, которое будет подставлено в параметр myparam роута route_name (если используется). В действии можно описать несколько параметров вида route.myparam для всех необходимых параметров роута. В качестве значения этого параметра можно использовать имена методов модели, к которой относится данный экшн, в виде '->getId' или '->getUser->getId'. Также можно использовать и скаляры. string

3.3.4 Controllers

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

Подробнее о контроллерах читайте в соответствующем разделе.

3.3.5 i18n

В этой директории хранятся файлы локализации модуля (если она требуется). Файлы создаются отдельно для каждого из языков. Причём файлы должны именоваться следующим образом: <имя_языка>.ini. Например: en.ini.

todo ссылка на главу о i18n

3.3.6 Mappers и models

В каталоге mappers/ модуля хранятся мапперы классов, в каталоге models/ — соответствующие модели. Вместе эти классы являются частью компоненты Model в парадигме MVC.

Подробнее про Model и MVC читайте в соответствующем разделе.

3.3.7 Templates

В этой директории хранятся smarty шаблоны

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

3.4.1 Обзор

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

3.4.2 Порядок выполнения кода

  1. Точкой входа для приложения является index.php, который располагается в корне web-директории приложения (папке /www). В этом файле подключается основной файл конфигурации приложения config.php, базовые классы приложения (в system/index.php), основной класс приложения application.php. В классе application могут быть переопределен процесс запуска приложения, если это необходимо (бывает редко), также в нем можно скомпоновать toolkit и подгрузить какие-то свои базовые классы.
  2. Далее приложение стартует путем вызова $application->run().
  3. В процессе запуска происходит компановка резолвера (метод composeResolvers) (todo: ссылка на описание резолвера), загрузка необходимых для старта файлов (метод loadCommonFiles), сборка композитного тулкита (метод composeToolkit), проверка приложения и его компонентов (метод check), затем управление переходит на метод handle.
  4. В методе handle происходит сборка цепи фильтров (метод composeFilters), которые должны обработать входящий запрос. Обычный набор фильтров состоит из следующей последовательности:
    • timingFilter — фильтр для подсчета времени выполнения запроса
    • sessionFilter — фильтр предназначен для подготовки и старта сессии
    • routingFilter — фильтр, в котором происходит процесс роутинга запроса
    • userFilter — фильтр определяет и устанавливает текущего пользователя, который выполняет запрос
    • userPreferencesFilter — фильтр устанавливает текущие настройки пользователя для приложения (локаль, часовой пояс, скин...)
    • contentFilter — фильтр получения и отображения контента
    Фильтры запкускаются друг за другом в описанном выше порядке. При необходимости можно определить свой набор фильтров и их порядок, переопределив метод application::composeFilters.

3.4.3 Маршрутизация запросов (Routing)

 

Часть 4 MVC

4.1 Model

Компонент Model парадигмы MVC в MZZ представлен собственной реализацией паттерна проектирования The Data Mapper Pattern. Этот паттерн подразумевает два набора классов: мапперы - классы, которые хранят метаописание сущностей, отношения между сущностями, выполняют работу по извлечению из БД и сохранению данных в БД, итд; объекты - контейнеры, которые хранят предоставляют интерфейс для доступа и модификации данных. Более подробно о работе с моделями можно познакомиться в главе ORM (todo ссылка).

4.2 View

4.2.1 Smarty

В MZZ для реализации компонента View используется библиотека Smarty 2.6.26 с небольшими модификациями.

Доступ к smarty есть в каждом контроллере приложения, посредством свойства smarty (подробнее). Пример работы со smarty в контроллере:

$this->smarty->assign('foo', 'bar');

Также, если требуется получить объект smarty не в контроллере, его можно получить из toolkit'а: (todo ссылка)

$toolkit = systemToolkit::getInstance();
$smarty = $toolkit->getSmarty();

4.2.2 Функции

Функция {load} предназначена для запуска модулей из шаблонов. MZZ предоставляет возможность запускать в шаблонах любые действия модулей, также предоставляя средства для организации взаимодействия модулей.

Синтаксис:

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

Пример: запуск действия list модуля news:

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

4.3 Controller

4.3.1 Создание

Контроллеры располагаются в директории controllers модуля. Имена контроллеров должны соответствовать следующему соглашению: <Имя_модуля><Имя_контроллера>Controller (todo: описать, откуда берется <Имя_контроллера>, а то не совсем ясно что это за имя). Например, для модуля news контроллер view будет называться: newsViewController.

Контроллеры в ваших модулях должны наследоваться от базового абстрактного класса simpleController. Каждый контроллер должен реализовывать защищённый (protected) метод getView().

Таким образом, простейшим примером может выступать следующий контроллер, возвращающий клиенту строку Hello, world!:

class helloWorldController extends simpleController
{
    protected function getView()
    {
        return 'Hello, world!';
    }
}

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

4.3.2 Базовые методы и свойства

Для упрощения процесса разработки базовый класс simpleController предоставляет ряд методов и свойств.

Методы:

Метод Аргументы Описание
setPager
  • mapper $mapper - маппер, к которому будет применена пагинация;
  • [int $per_page = 20] - число элементов на странице;
  • [bool $reverse = false] - выводить записи в обратном порядке;
  • [int $round_items = 2] - число страниц, выводимых слева и справа от текущей.
Присоединяет класс пагинации к мапперу, который выбирает данные на страницу. Также в smarty автоматически передаётся инстанция объекта-пейджера с именем $pager (todo ссылка на описание класса), который можно отобразить в произвольном месте.
Пример использования: Выберем список новостей, по 10 новостей на странице. И отобразим их в обратном порядке:
newsListController.php:
$this->setPager($newsFolderMapper, 10, true); // устанавливаем pager
$this->smarty->assign('news', $newsFolderMapper->getItems($newsFolder)); // ищем новости
list.tpl:
<div class="pages">{$pager->toString()}</div>
fetch
  • string $path - имя файла шаблона.
Запуск на исполнение указанного шаблона.
return $this->fetch('news/list.tpl');
redirect
  • string $url - адрес, на который будет перенаправлен пользователь;
  • [int $code = 302] - код http-ответа.
Перенаправление пользователя по указанному урлу.
Пример использования: Перенеправление на главную страницу приложения:
$url = new url('default');
return $this->redirect($url->get());
forward
  • string $moduleName - имя модуля;
  • string $actionName - имя действия.
Передача управления другому контроллеру.
Пример использования: Вызов из текущего контроллера действия dashboard модуля admin:
return $this->forward('admin', 'dashboard');

Свойства:

Свойство Тип Описание
$toolkit stdToolkit Объект toolkit (todo ссылка на описание)
$id = $this->toolkit->getMapper('news', 'news');
$request httpRequest Объект запроса (todo ссылка на описание)
$path = $this->request->getString('path');
$response httpResponse Объект ответа (todo ссылка на описание)
$this->response->setCookie('cookie_name', 'cookie_data');
$smarty mzzSmarty Smarty (подробнее)
$this->smarty->assign('news', $newsFolderMapper->getItems($newsFolder));
$this->smarty->assign('folderPath', $newsFolder->getTreePath());

4.3.3 Служебные

 

Часть 5 ORM

5.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 лежат в подкаталоге models каждого модуля, мапперы - в подкаталоге mappers.

5.2 Мапперы

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

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

<?php
/**
 * $URL: svn://svn.mzz.ru/mzz/trunk/system/modules/news/mappers/newsMapper.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: newsMapper.php 4267 2010-07-09 14:34:11Z desperado $
 */
 
fileLoader::load('news/models/news');
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/newsFolder'
        ),
        'title' => array(
            'accessor' => 'getTitle',
            'mutator' => 'setTitle',
            'options' => array(
                'i18n',
            ),
        ),
        'editor' => array(
            'accessor' => 'getEditor',
            'mutator' => 'setEditor',
            'relation' => 'one',
            'foreign_key' => 'id',
            'mapper' => 'user/user'
        ),
        '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',
            ),
            'orderBy' => 1,
            'orderByDirection' => 'DESC',
        ),
        'updated' => array(
            'accessor' => 'getUpdated',
            'mutator' => 'setUpdated',
        ),
    );
 
    public function __construct($module)
    {
        parent::__construct($module);
        $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);
    }
}
 
?>

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

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

5.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 (метод для поиска пользователя по его логину) будет выглядеть следующим образом:

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

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

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

5.4 Хуки

В ORM mzz на основе паттерна The Observer реализована система хуков. Каждый из хуков вызывается после (или до) определённого действия в маппере. Также в хуки передаются данные, собственно с которыми код хука и должен работать. Полный список хуков может быть уточнён вами в файле orm/observer.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'); } } [...] } <

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

5.5 Плагины

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

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', 'plugin')
        );
    }
 
    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();
    }
 
    public function setObjId(entity $object, $id)
    {
        $object->merge(array($this->options['obj_id_field'] => $id));
        return $object;
    }
 
    public function getObjIdField()
    {
        return $this->options['obj_id_field'];
    }
}

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

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

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

mapper::plugins($name, array $options = array(), $newName = null);

или

mapper::attach(observer $observer, $name = null)

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

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

 

Часть 6 Шаблонизатор

6.1 Обзор

sdsdf

6.2 Помощники видов или хелперы

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

 

Часть 7 Внешние данные и валидация форм

7.1 Обзор

7.2 Валидаторы

 

Часть 8 ACL и система прав

8.1 Обзор

 

Часть 9 Локализация модулей и приложений

9.1 Обзор

 

Часть 10 Стандартные пакеты

10.1 Toolkit

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

$toolkit = systemToolkit::getInstance();

Также в контроллерах toolkit уже доступен через свойство toolkit. Пример получения текущего пользователя:

$this->toolkit->getUser();

10.2 Cache

10.2.1 Обзор

Пакет cache предназначен для кеширования данных.

10.2.2 Конфигурирование

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

Существует набор конфигураций по умолчанию:

Название Хранилище Опции Описание
fast file
'params' => array(
    'path' => systemConfig::$pathToTemp . DIRECTORY_SEPARATOR . 'cache',
    'prefix' => 'fast_',
    'expire' => 180)
Предназначен для хранения часто изменяющихся данных, которые по истечении очень короткого срока времени (180 секунд по умолчанию) должны будут устареть и заменены на свежие данные. Данные хранятся в поддиректории cache, временной директории проекта.
session session
'params' => array('expire' => 1800)
Предназначен для хранения данных, имеющих свою актуальность только в пределах пользовательской сессии. Стоит помнить, что данные, в этом случае, будут независимы для каждой из сессий и из одной сессии нельзя будет получить данные другой.
long file
'params' => array(
    'path' => systemConfig::$pathToTemp . DIRECTORY_SEPARATOR . 'cache',
    'prefix' => 'long_',
    'expire' => 86400)
Конфигурация аналогичная конфигурации fast, но предназначены наоборот - для хранения редко изменяющихся данных. Время хранения по умолчанию - сутки.
memory memory Предназначен для хранения данных, имеющих свою актуальность только в пределах вызова скрипта. Данные хранятся в оперативной памяти и уничтожаются после завершения выполнения скрипта.
default Конфигурация, которая будет использована в случае, если при получении объекта кэша не была указана любая из существующих конфигураций. По умолчанию является синонимом на конфигурацию fast.

В дополнение к этим конфигурациям вы можете добавить в своём приложении свои. Для этого в массив systemConfig::$cache просто необходимо добавить описание новой конфигурации. Ключ массива с конфигурацией - будет служить её именем.

Например, для добавления конфигурации кэша с именем myMemcacheConfig, которая бы хранила данные в memcache-хранилище нужно добавить следующие строки:

systemConfig::$cache['myMemcacheConfig'] = array(
    'backend' => 'memcache'
);

Если вам нужно изменить параметры стандартных конфигураций, то просто переопределите соответствующие ключи массива systemConfig::$cache.

Например, чтобы кэш fast начал работать с memcache вместо файлов, в конфигурационном файле нужно прописать:

systemConfig::$cache['fast'] = array(
    'backend' => 'memcache'
);

10.2.3 Использование

Работа с пакетом cache происходит единообразно для всех типов хранилищ и всех конфигураций. Для начала работы необходимо получить объект с нужной конфигурацией из toolkit'а:

$cache = $this->toolkit->getCache('fast');

В результате будет возвращён объект типа cache, реализующий интерфейс:

Метод Аргументы Возвращаемое значение Описание
get
  • string $key - извлекаемый ключ;
  • [mixed &$result = null] - извлекаемое значение.
Boolean:
  • true - искомое значение найдено;
  • false - в противном случае.
Этот метод используется для получения значений, сохранённых в кэше.
if ($cache->get('key', $value)) {
    echo 'Найденное значение: ' . $value;
} else {
    echo 'Значение не найдено';
}
set
  • string $key - сохраняемый ключ;
  • mixed $val - сохраняемое значение;
  • [array $tags = array()] - массив тегов, проассоциированных с данным ключом;
  • [int $expire = null] - время жизни ключа, в секундах.
Mixed: возвращаемое значение зависит от используемого хранилища. Обычно - true/false. Этот метод используется для сохранения значений в кэше.
$data = array(1, 'some string');
$cache->set('key', $data);
Сохранение данных с пометкой их тегами "sample" и "documentation" и временем жизни в 1 минуту:
$data = 'some data to cache';
$cache->set('key', $data, array('sample', 'documentation'), 60);
С одним и тем же тегом может быть проассоциировано любое число ключей. И наоборот: не накладывается никаких разумных ограничений на число тегов, проассоциированных с ключом.
delete
  • string $key - удаляемый ключ.
Mixed: возвращаемое значение зависит от используемого хранилища. Обычно - true/false. Этот метод используется для удаления значений из кэша.
$cache->delete('key');
clear
  • string $tag - имя очищаемого тега.
Mixed: возвращаемое значение зависит от используемого хранилища. Обычно - true/false. Этот метод используется для удаления значений из кэша по связанному с ним тегу. Если ключей, ассоциированных с указанным тегом, несколько, тогда будут удалены все вхождения.
$data = 'some data to cache';
$cache->set('key', $data, array('sample', 'documentation'), 60);
 
$cache->clear('sample');
В примере был удалён установленный ранее ключ 'key', потому как при установке его значения одним из тегов был указан тег 'sample'.
flush void Этот метод используется для очистки содержимого всего хранилища.
$cache->flush();

В зависимости от типа хранилища, могут очищаться все данные в нём (как в memcache) или только относящиеся к данной конфигурации (как в file).

10.3 Db

10.3.1 Объект соединения с БД

10.3.2 Генератор запросов

Для генерации SQL-запросов в MZZ предназначен специальный класс simpleSelect. SimpleSelect собирает запрос из составных частей, которыми являются объекты классов criteria. Интерфейс у simpleSelect предельно простой: конструктор, принимающий объект класса criteria и метод simpleSelect::toString(), возвращающий сгенерированный запрос.

Рассмотрим пример генерации простейшего запроса:

$criteria = new criteria('table');
$select = new simpleSelect($criteria);
echo $select->toString(); // выведет "SELECT * FROM `table`"

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

10.3.3 Criteria

Класс criteria служит для указания параметров выборки: полей, условий, объединений таблиц, группировки, итд. Сам по себе этот класс не используется нигде в приложениях, но он необходим как набор правил для генерации запроса с помощью класса simpleSelect.

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

Интерфейс criteria:

.1 __construct

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

__construct([string $table = null [,string $alias = null]])

$table имя таблицы
$alias алиас

.2 select

Указывает выбираемые поля

criteria select(string $field [, string $alias = null])
$field имя поля
$alias алиас
$criteria = new criteria('table');
$criteria->select('field1');
$criteria->select('field2', 'alias');
$select = new simpleSelect($criteria);
echo $select->toString(); // выведет "SELECT `field1`, `field2` AS `alias` FROM `table`"

Потому как метод select возвращает ссылку на объект критерии - можно сократить данный код:

$criteria = new criteria('table');
$criteria->select('field1')->select('field2', 'alias');
$select = new simpleSelect($criteria);
echo $select->toString(); // выведет "SELECT `field1`, `field2` AS `alias` FROM `table`"

.3 where

Указывает условия выборки

criteria where(string|criterion $field [, mixed $value = null [, int $comparison = criteria::EQUAL]])
$field имя поля либо объект criterion (todo ссылка на criterion)
$value сравниваемое значение. В зависимости от условия тип может быть любым
$comparison тип сравнения
$criteria = new criteria('table');
$criteria->where('field', 'value', criteria::GREATER)->where('field2', 'value2');
$select = new simpleSelect($criteria);
echo $select->toString(); // "SELECT * FROM `table` WHERE `table`.`field` > 'value' AND `table`.`field2` = 'value2'"

Доступные для использования типы сравнений:

EQUAL =
NOT_EQUAL <>
GREATER >
LESS <
GREATER_EQUAL >=
LESS_EQUAL =<
IN IN
NOT_IN NOT IN
LIKE LIKE
NOT_LIKE NOT LIKE
BETWEEN BETWEEN
NOT_BETWEEN NOT BETWEEN
FULLTEXT MATCH (%s) AGAINST (%s)
FULLTEXT_BOOLEAN MATCH (%s) AGAINST (%s IN BOOLEAN MODE)
IS_NULL IS NULL
IS_NOT_NULL IS NOT NULL

Для сравнений, в которых операнд "значение" состоит фактически из нескольких значений (Например: IN (1, 2, 3)) необходимо передавать массив этих значений.
Пример работы с такими сравнениями:

$criteria->where('field', array(1, 2, 3), criteria::IN); // `field` IN (1, 2, 3)
$criteria->where('field', array(1, 2), criteria::BETWEEN); // `field` BETWEEN 1 AND 2
$criteria->where(array('field1', 'field2'), 'value', criteria::FULLTEXT); // MATCH(`field1`, `field2`) AGAINST ('value')
$criteria->where('field', null, criteria::IS_NULL); // `field` IS NULL

.4 orderByAsc, orderByDesc

Добавляет сортировку в запрос

criteria orderByAsc(string $field [, boolean $alias = true]), criteria orderByDesc(string $field [, boolean $alias = true])
$field имя поля
$alias алиас
$criteria = new criteria('table');
$criteria->select('field1');
$criteria->orderByAsc('field1');
$select = new simpleSelect($criteria);
$select->toString(); // "SELECT `field1` FROM `table` ORDER BY `table`.`field1` ASC"

 

Часть 11 Готовые модули

11.1 Страницы

11.2 Меню

11.3 Файловый менеджер

11.4 Комментарии

11.4.1 Обзор

11.4.2 commentsPlugin

 

Часть 12 Расширение MZZ

12.1 Системные файлы

12.2 Расширение модулей

12.2.1 Mappers

Часто бывает, что необходимо поменять некоторые вещи в коробочных модулях MZZ, например, модифицировать $map (todo: ссылка туда, где знают, что такое MAP объекта) или же просто добавить недостающий метод в маппер. В таком случае подойдет способ, описанный в этой главе, но он не очень удобен по ряду причин.

MZZ предлагает немного иной подход к проблеме переопределения частей модуля в приложениях. Рассмотрим его на примере модуля user.

Мы имеем userMapper.php следующего содержания (тело класса скрыто, весь листинг вы можете посмотреть, открыв файл <MZZ>/system/modules/user/mappers/userMapper.php):

class userMapper extends mapper
{
 
    /* ... */
 
}
?>

И мы хотим переопределить этот маппер с целью добавить новый метод searchAllLastLoggedIn — метод, возвращающий нам всех пользователей, которые приходили на сайт не ранее заданной даты

Для реализации этой идеи достаточно создать файл в каталоге <project>/modules/user/mappers/appUserMapper.php, где <project> это путь до вашего приложения (опция systemConfig::$pathToApplication), а appUserMapper.php — новое имя маппера, которое было образовано путем добавления приставки app к базовому имени маппера. Префикс app в данном случае является стандартным и одинаковым для всех переопределяемых мапперов.

appUserMapper.php будет выглядеть примерно так:

fileLoader::load('modules/user/mappers/userMapper');
 
class appUserMapper extends userMapper
{
    public function searchAllLastLoggedIn($time)
    {
        $criteria = new criteria;
        $criteria->where('last_login', $time, criteria::GREATER);
 
        return $this->searchAllByCriteria($criteria);
    }
 
}

Несложно догадаться, что теперь можно добавлять, переопределять любые методы и свойства базового маппера userMapper

 

Часть 13 Создание проекта на MZZ

13.1 Введение

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

Основные особенности MZZ:

13.2 Архитектура MVC

13.2.1 Model

Компонент Model парадигмы MVC в MZZ представлен собственной реализацией паттерна проектирования The Data Mapper Pattern. Этот паттерн подразумевает два набора классов: мапперы - классы, которые хранят метаописание сущностей, отношения между сущностями, выполняют работу по извлечению из БД и сохранению данных в БД, итд; объекты - контейнеры, которые хранят данные, предоставляют интерфейс для доступа и модификации их.

13.2.2 View

Компонент View определяет отображение данных веб-клиенту. В MZZ для реализации этого компонента используется Smarty 2.6.26. Чаще всего, View хранится в файлах шаблонов, которые содержат HTML и Smarty код.

13.2.3 Controller

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

13.3 Создание блога

13.3.1 Введение

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

В процессе создания приложения будут созданы две сущности - post, postComment. Из стандартных модулей мы будем использовать только модуль user. Во фреймворке уже имеется готовый модуль комментариев, но, так, как цель данного руководства показать весь процесс написания приложения, мы реализуем комментарии еще раз, с более простой функциональностью.

Наш блог будет обладать следующими действиями: список записей (list), создание записи (create), редактирование записи (edit), удаление записи (delete), создание комментария (comment), список комментариев (comments), удаление комментария (commentDelete).

13.3.2 Установка

Процесс установки самого фреймворка очень прост. Для это необходимость скачать MZZ с Download раздела на официальном сайте и распаковать его в любую директорию.
Код является кроссплатформеным и работает под всеми операционными системами и веб серверами.

13.3.3 Создание Блог приложения

Скачайте dummy (пустой) проект с Download и распакайте его в любую директорию, которая доступна веб-серверу.

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

Имя файла Описание
db/ дампы базы данных приложения
files/ файлы, хранимые модулем fileManager
modules/ модули приложения
routes/ роуты приложения
templates/ шаблоны
tmp/ временные файлы
www/ корневая директория приложения
application.php класс приложения
config.php основная конфигурация приложения

Корневая директория приложения (www/) - это директория доступная для чтения веб-браузером. В ней хранятся все js/css-файлы, картинки и прочий общедоступный контент. Структура этой директории выглядит так:

Имя файла Описание
css/ файлы стилей
images/ файлы изображений
js/ файлы javascript
index.php точка входа в приложение, именно этому файлу передаётся управление

Мы установили сам фреймворк в директорию /home/MZZ, приложение будет установлено в /home/MZZ/app, веб-сервер настроен на директорию - /home/MZZ/app/www. Адрес нашего проекта будет http://MZZ.blog/.

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

define('SYSTEM_PATH', realpath(dirname(__FILE__) . '/../system/'));

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

define('DEBUG_MODE', true);
Если вы устанавливаете приложение на Unix-сервере, возможно, понадобится установить права для записи в tmp директорию.

chown <user>:<group> -R /home/MZZ/app/tmp
chmod 755 -R /home/MZZ/app/tmp

Уточните у вашего системного администратора какие права необходимы чтобы разрешить веб-серверу запись в директорию.

.5 Конфигурация базы данных

Откроем db/mzz_dummy.sql и изменим имя базы данных.

/*!40000 DROP DATABASE IF EXISTS `MZZ_blog`*/;
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `MZZ_blog` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `MZZ_blog`;

Импортируем измененный дамп dummy-базы в MySQL:

mysql -u user -ppassword < /home/MZZ/app/db/mzz_dummy.sql

И изменим настройки базы данных все в том же config.php.

systemConfig::$db['default']['dsn']  = 'mysql:host=localhost;dbname=MZZ_blog';
systemConfig::$db['default']['user'] = 'root';
systemConfig::$db['default']['password'] = '';

Если все было сделано правильно и dummy приложение установлено, по адресу http://MZZ.blog/ мы увидим некоторый hello текст.

Hello, world!
Привет, мир!
 
Модуль: dummy
Экшн: hello

13.3.4 Создание моделей

Теперь нам необходимо создать модуль блог, структуру его директорий и, собственно, сами модели. Для этих целей в MZZ реализован web-интерфейс для управления модулями. Зайти в него можно через административный интерфейс по адресу http://MZZ.blog/admin/ используя стандартый логин и пароль (admin / test). Далее необходимо выбрать пункт меню Администрирование -> Утилиты разработчика. В дальнейшем под словом devToolbar мы будем подразумевать именно этот интерфейс.

Для создания модуля надо нажать на иконку рядом с "Модули и классы". Имя для нового модуля: blog. Каталог генерации: каталог проекта (по умолчанию). Сохраним изменения и, если все успешно, мы увидим текст "Модуль blog успешно создан". Перейдем к моделям.

Модель Post будет содержать 5 свойств: идентификатор (name), заголовок (title), содержимое (content) и дату создания (created_at). Выполним запрос в mysql для создания таблицы:

CREATE TABLE `MZZ_blog`.`blog_post` (
    `name` VARCHAR(255) NOT NULL,
    `title` VARCHAR(255) NOT NULL,
    `content` TEXT NOT NULL,
    `created_at` DATETIME NOT NULL,
    PRIMARY KEY (`name`)
) ENGINE = MyISAM;

В devToolbar, в секции модуля blog, создадим класс post. Имя таблицы blog_post.

В MZZ, действие (action) должно принадлежать какому-либо определенному объекту. Но что делать с действиями list и create? Для этого мы создаем фейковый объект и называем его, как правило, именем модуля или <имяМодуля>Folder. Создавать таблицу в базе данных необходимости нет, поэтому просто создадим класс postFolder для blog и укажем несуществующую таблицу blog_postFolder.

Для комментариев создадим только один объект postComment. Они тоже имеют действия, не связанные с конкретным комментарием, это comments и comment, но в нашем случае для этих целей вполне подойдет объект post, тем более, что комментирование относится к постам в блоге. Начем с создания таблицы в базе данных:

CREATE TABLE `MZZ_blog`.`postComment` (
    `id` INT NOT NULL ,
    `post_id` INT NOT NULL ,
    `author_name` VARCHAR( 255 ) NOT NULL ,
    `content` TEXT NOT NULL ,
    `created_at` INT NOT NULL ,
    PRIMARY KEY ( `id` ) ,
    INDEX ( `post_id` )
) ENGINE = MYISAM ;

Создаем класс postComment для модуля blog в devToolbar указав имя таблицы blog_postComment.

13.3.5 Создание записи в блоге

В MZZ встроен механизм скаффолдинга, который позволяет генерировать готовый к использованию код для стандартных действией над объектом (CRUD - Create, Retrive, Update, Delete). Действия Create и Update чаще всего очень похожи, поэтому мы их объединили в одно Save действие. Здесь надо заметить, что использование CRUD подходит только в довольно общих и простых случаях, и получившийся код в любом случае необходимо дорабатывать под себя.

Первое действие, которое мы создадим, будет создание записи в блоге (create). Как было написано ранее, действие должно принадлежать существующему объекту, поэтому оно будет принадлежать классу postFolder. Создаем это действие в devToolbar указав CRUD: save и CRUD class: post (т.к. на самом деле это действие для создания объекта типа post)

По адресу http://MZZ.blog/blog/create находится автоматически сгенерированная форма для добавления записи. Отредактируем ее чуть-чуть.

Откроем файл шаблона modules/blog/templates/create.tpl и приведем нашу формочку в более приятный вид. В MZZ есть генератор форм, который генерирует HTML-код для различных элементов форм, выставляет дефолтные значения и восстанавливает введеные пользоваталем данные в форме в случае ошибки заполнения формы и отображает ее:

<h3>{if $isEdit}Edit{else}Create{/if} a post</h3>
 
{form action=$form_action method="post"}
    <table width="100%" border="0" cellpadding="5" cellspacing="0" align="center">
        <tr>
            <td>{form->caption name="post[name]" value="Name"}</td>
            <td>
                {form->text name="post[name]" size="30" value=$post->getName()}
                {if $validator->isFieldError('post[name]')}<div class="error">{$validator->getFieldError('post[name]')}</div>{/if}
            </td>
        </tr>
        <tr>
            <td>{form->caption name="post[title]" value="Title"}</td>
            <td>
                {form->text name="post[title]" size="30" value=$post->getTitle()}
                {if $validator->isFieldError('post[title]')}<div class="error">{$validator->getFieldError('post[title]')}</div>{/if}
            </td>
        </tr>
        {if !$isEdit}
        <tr>
            <td>{form->caption name="post[created_at]" value="Created at"}</td>
            <td>
                {form->text name="post[created_at]" size="30" value=$smarty.now|date_format:"%H:%M:%S %d/%m/%Y"}
                {if $validator->isFieldError('post[created_at]')}<div class="error">{$validator->getFieldError('post[created_at]')}</div>{/if}
            </td>
        </tr>
        {/if}
        <tr>
            <td valign="top">{form->caption name="post[content]" value="Content"}</td>
            <td>
                {form->textarea name="post[content]" rows="10" cols="45" value=$post->getContent()}
                {if $validator->isFieldError('post[content]')}<div class="error">{$validator->getFieldError('post[content]')}</div>{/if}
            </td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>{form->submit name="submit" value="_ simple/save"}</td>
        </tr>
    </table>
</form>

Сделаем пару изменений в контроллере modules/blog/controllers/blogCreateController.php. Здесь мы можем изменить валидаторы для нашей формы. Сейчас они выглядит так:

$validator->rule('required', 'post[title]', 'Field title is required');
$validator->rule('length', 'post[title]', 'Field title is out of length', array(0, 255));
$validator->rule('required', 'post[content]', 'Field content is required');
$validator->rule('required', 'post[created_at]', 'Field created_at is required');

Нам они не очень походят, поэтому заменим их на:

$validator->rule('required', 'post[title]', 'Field title is required');
$validator->rule('required', 'post[name]', 'Field name is required');
$validator->rule('required', 'post[content]', 'Field content is required');
$validator->rule('required', 'post[created_at]', 'Field created_at is required');
$validator->rule('regex', 'post[name]', 'Field name has illegal characters', '#^[a-z\d-_]+$#i');
$validator->rule('length', 'post[name]', 'Field name is out of length', array(0, 255));
$validator->rule('length', 'post[title]', 'Field title is out of length', array(0, 255));
$validator->rule('date', 'post[created_at]', 'Invalid creation date', array('regex' => 'time_date'));
$validator->rule('callback', 'post[name]', 'Post name is not unique', array(array($this, 'checkUniquePostName'), $post));

Перечисляя по порядку, валидаторы определяют следующее: поля title, name, content и created_at обязательны для заполнения, name может содержать только символы a-z, A-Z, 0-9, - и _ (так как это идентификатор), name и title имеют длину не более чем 255 символов, created_at имеет формат вида 14:43:32 20/12/2009 (вместо значения time_date может быть любое указано регулярное выражение для валидации формата даты, мы будем использовать стандарный вид даты для русской локали). Последний валидатор является callback функцией, для валидации значения будет вызван метод checkUniquePostName у текущего контроллера. Используется он для проверки того, что запись с таким же именем уже не существует. Добавим этот метод в blogCreateController.

public function checkUniquePostName($name, $post)
{
    if ($name == $post->getName()) {
        return true;
    }
    $postMapper = $this->toolkit->getMapper('blog', 'post');
 
    return is_null($postMapper->searchByKey($name));
}

Добавим установку имени для записи:

$post->setName($data['name']);
$post->setTitle($data['title']);
$post->setContent($data['content']);
$post->setCreatedAt($data['created_at']);

Также изменим способ редиректа после сохранения записи.

return jipTools::redirect();

Наше действие не будет выполняться как JIP-действие (об этом мы расскажем позже) поэтому заменим на:

$url = new url('withAnyParam');
$url->add('name', $post->getName());
$url->add('action', 'view');
return $this->redirect($url->get());

Класс url генерирует готовые ссылки используя роутер withAnyParam и переданные в него значения. Метод $this->redirect() выполняет HTTP-редирект пользователя на указанную страницу (просмотр созданной записи).

Чтобы сохранить дату создания в правильном формате, добавим метод в modules/blog/models/post.php

public function setCreatedAt($value)
{
    $datetime = explode(' ', $value);
    $date = explode('/', $datetime[1]);
    $datetime = implode('-', array_reverse($date)) . ' ' . $datetime[0];
    parent::__call('setCreatedAt', array($datetime));
}

Creating Post Form

13.3.6 Routers

Маршрутизация (Routing) - это процесс поиска подходящего роута и превращения с его помощью запрошенного пути в обычный массив. Правила маршрутизации хранятся в файле <project_folder>/routers/default.php и могут быть переопределены. Так же у модулей могут быть указаны свои правила маршрутизации, которые могут быть описаны в классе модуля в файле modules/<module>/<module>Module.php.

В modules/blog/blogModule.php мы добавим простой роут обработки путей для конкретной записи в блоге:

public function getRoutes()
{
    return array(
        array(),
        array(
            'blogPost' => new requestRoute('blog/:name/:action', array(
                'module' => 'blog',
                'action' => 'view'), array(
                'name' => '.*?',
                'action' => '(?:create)'))));
}

Этот метод возвращает массив из двух элементов: первый содержит роуты, которые будут вызываны после других и второй - перед остальными. Этот простой роут определяет правила для разбора пути вида "blog/first_blog_entry". В MZZ по умолчанию определены некоторые стандартные роуты, в том числе и для обработки путей вида module/name/action (withAnyParam). Смысл создания нашего в том, чтобы роутер знал что "blog/create" это создание новой записи, а не просмотр записи с именем "create".

13.3.7 Просмотр записи в блоге

Если создание записи прошло успешно (для примера, мы создали запись с именем "welcome"), то после добавления мы увидим исключение Unknown action view in module blog. Создадим в devToolbar действие view для класса post выбрав CRUD: view.

По адресу http://MZZ.blog/ru/blog/welcome мы увидим 404 ошибку. Произошло это из-за того, что по умолчанию контроллер blogViewController сгенерировался для работы с числовым идентификатором (id):

$id = $this->request->getInteger('id');
$post = $postMapper->searchByKey($id);

В нашем же случае используется строковое имя для идентификации объекта (name). Исправим это:

$name = $this->request->getString('name');
$post = $postMapper->searchByKey($name);