5.2 Написание модуля "Комментарии"
- 1. Введение
- 2. Планирование
- 3. Структура БД
- 4. Общий вид урлов
- 5. Создание структуры каталогов
- 6. Создание сущностей
- 7. Регистрация модуля в системе
- 8. Программирование действий
- 9. Подведение итогов
Ввиду появления модуля devToolbar, предназначенного для упрощения рутинной работы при создании модулей, этот раздел несколько потерял актуальность в том плане - что большая часть работы теперь возложена на модуль. Однако прочтение главы полезно с точки зрения понимания принципов работы и философии mzz. Также отметим, что некоторые утверждения могут быть несколько ложными ввиду динамичного развития проекта. Заранее приносим вам свои извинения.
В момент прочтения вами этой статьи, реальный код из репозитория и коды примеров в этой главе могут различаться как логикой, так и реализацией.
В этой главе будем рассмотрен процесс создания нового модуля. В качестве примера будет рассмотрен новый модуль Comments, которого ещё нет в демо-приложении "лента новостей", работающего под управлением mzz.
Для начала давайте определимся, что же мы хотим получить в результате. Модуль Comments будет служить для добавления возможности комментирования любых объектов приложения. Это значит, что его легко и непринуждённо можно будет подключить как к модулю News, так и к Page и к любому произвольному модулю системы. Приступим.
Определимся с набором возможных действий модуля.
- list - экшн, который будет возвращать список комментариев для данного объекта;
- post - экшн для добавления нового комментария;
- edit - экшн для исправления уже существующего комментария;
- delete - экшн для удаления комментария.
В систему будут добавлены следующие сущности:
- comment - сущность, обозначающая непосредственно комментарий;
- commentsFolder - сущность скорее носящая служебный характер. Причины введения этой сущности будут поясняться в процессе этой главы.
Для таблицы с комментариями нам нужны следующие поля:
- id - первичный ключ таблицы, идентификатор комментария;
- obj_id - системный идентификатор объекта в пределах всего приложения (ссылку);
- text - текст комментария;
- author - автор сообщения, это поле будет ссылаться на таблицу с пользователями;
- time - время в формате unix timestamp;
- folder_id - идентификатор папки (commentsFolder), в которой находится данный комментарий.
Для описанной структуры дамп будет следующим:
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 таблица БД будет следующей:
- id - первичный ключ таблицы, идентификатор папки с комментариями;
- obj_id - системный идентификатор объекта в пределах всего приложения (ссылку);
- parent_id - системный идентификатор комментируемого объекта.
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"
В соответствии с нашими потребностями определим вид ссылок (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+')));Для создания структуры каталогов в командной строке перейдите в каталог 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
Для создания сущностей используется генератор, подобный использованному в предыдущей главе. Для создания наших двух запланированных сущностей перейдите в только что созданный каталог модуля и выполните в командной строке:
- Для сущности
comments:generateDO.bat comments
Результат успешного выполнения команды:
File created successfully: - comments/comments.php - comments/mappers/commentsMapper.php - comments/actions/comments.ini - comments/maps/comments.map.ini ALL OPERATIONS COMPLETED SUCCESSFULLY
- Для сущности
commentsFolder:generateDO.bat commentsFolder
Результат успешного выполнения команды:
File created successfully: - comments/commentsFolder.php - comments/mappers/commentsFolderMapper.php - comments/actions/commentsFolder.ini - comments/maps/commentsFolder.map.ini ALL OPERATIONS COMPLETED SUCCESSFULLY
Зарегистрируем модуль Comments в приложении.
Из всех определённых нами необходимых экшнов в системе уже зарегистрированы 3: list, edit, delete. Соответственно в таблицу `sys_actions` добавляем только одну запись со значением post. Для удобства выпишем идентификаторы всех используемых далее экшнов:
- list - 5
- edit - 1
- delete - 2
- post - 19
- editACL - 9 (это служебное действие, оно должно быть у каждого объекта в приложении, построенном на mzz)
Естественно идентификаторы ваших экшнов (и последующие идентификаторы) могут отличаться от приведённых в документации.
В таблицу `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_id | action_id |
| 10 | 1 |
| 10 | 2 |
| 10 | 9 |
| 11 | 5 |
| 11 | 19 |
| 11 | 9 |
В таблицу `sys_sections` добавляем имя раздела, в котором будет "располагаться" новый модуль - comments. Получаем id = 8.
Через таблицу `sys_classes_sections` связываем сущности нового модуля и раздел. Добавляем 2 записи с `section_id` = 8 и `class_id` = 10 и 11 соответственно. Этим записям будут присвоены id = 10 (для comments) и 11 (для commentsFolder). Совпадение случайное - у вас они могут быть другими.
В первую очередь давайте создадим действие 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.
Давайте подведём итог проделанной работы. В течение нескольких часов мы написали достаточно универсальный модуль, который теперь без лишних трудозатрад можно запустить в контексте другого модуля, таким образом расширив функциональность, не прибегая к каким либо модификациям кода модуля. Продемонстрируем это на практике: добавьте в шаблон отображения конкретной страницы (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, можно управлять правами - кому можно добавлять комментарии для данного объекта, а кому - нет, а также устанавливать права по умолчанию для вновь создаваемых комментариев.