Новости

Пишем первый аддон для XenForo. MVC, шаблоны, стили, настройки, навигация...

10.10.2010 | FractalizeR

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

В первую очередь надо перевести форум в режим отладки. Это можно сделать добавив в файл /library/config.php строчку:

  1. $config['debug'] = true;

Войдите в панель управления администратора и перейдите на появившуюся закладку Разработка. Выберите там "Создать дополнение". В качестве ID нам надо указать любую уникальную строку. Пусть это будет "LikeReviewRus" (без кавычек, конечно). В поле заголовка введите любой поясняющий текст. Он ни на что не влияет, просто будет отображаться в панели управления. Сохраните. У нас получилось пустое дополнение.

Теперь давайте создадим опцию для лимита количества сообщений на страницу. Перейдите на вкладку Главная и зайдите в Настройки. Добавьте группу настроек. ID группы пусть будет LikeReviewRusSettingsGroup. Заголовок можете указать произвольный. Этот текст вы увидите в числе других групп настроек. Не делайте его слишком длинным. Имейте ввиду, что это текст для отображения на "Master language"-языке форума. Фактически, таким языком является английский и наш аддон в идеале должен бы быть изначально тоже на английском. Затем мы могли бы его перевести и распространять русскую локализацию вместе с файлом аддона. Но для простоты давайте пока все строки указывать на родном языке. В качестве дополнения выберите наш аддон и сохраните.

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

  1. min=1
  2. step=5
  3. max=100

Это означает, что минимальное значение настройки - 1, при каждом нажатии на стрелку будет прибавляться / отниматься 5, а максимальное число, которое можно туда ввести - 100. Тип данных пусть будет "Беззнаковое целое". Значение по умолчанию - 30. Остальные настройки пока оставим как есть. Сохраните. Вы сразу же сможете проверить, что у вас получилось.

Теперь давайте создадим настройку стиля. Перейдите на вкладку Внешний вид и щелкните по Настройкам стиля. Создайте там новую группу. Пусть ее ID будет LikeReviewRusStyle. Все остальные параметры, включая пункт Дополнение, вы можете выбрать самостоятельно. Сохраните, а затем зайдите в эту группу. Вы сразу попадете в диалог создания нового свойства стиля. Имя свойства - LikeReviewRusAvatarBorder. Укажите Дополнение, заголовок и описание. В качестве типа свойства укажите Скаляр и выберите цвет. Возможно, вам также придется снова вручную выбрать группу. Это баг и его исправят. Сохраните и посмотрите, что у вас получилось. Класс, правда? :)

Теперь нам нужно создать немного фраз, чтобы аддон можно было переводить на другие языки. Щелкните по ссылке Фразы на странице Внешний вид. Создайте фразы с заголовками LikeReviewRus_Header (для отображения в самом верху страницы) и LikeReviewRus_Description для отображения ниже в качестве описания. В качестве текста для этой последней фразы введите "Это {numPosts} больше всего понравившихся пользователям постов". Ниже вы увидите, как в шаблоне вместо {numPosts} будет вставлено количество.

Теперь нам нужно создать шаблон для страницы, которую мы будем показывать. Щелкните по разделу Шаблоны на вкладке Внешний вид. Теперь создайте новый шаблон. В качестве имени шаблона укажите likereviewrus.css и вставьте следующее содержимое:

  1. .mostLikedPosts {}
  2. .mostLikedPosts .avatar {
  3.     float: left;
  4.     margin-right: 10px;
  5. }
  6. .mostLikedPosts .avatar img {
  7.     width: 64px;
  8.     height: 64px;
  9.     border: 3px solid {xen:property LikeReviewRusAvatarBorder};
  10. }
  11. .mostLikedPosts .likedPost {
  12.     position: relative;
  13. }
  14. .mostLikedPosts .primaryContent {
  15.     padding: 0;
  16.     padding-top: 10px;
  17. }
  18. .mostLikedPosts h3 {
  19.     font-size: 12pt;
  20.     margin-bottom: 5px;
  21. }
  22. .mostLikedPosts .likes {
  23.     display: block;
  24.     position: absolute;
  25.     right: 0px;
  26.     top: 10px;
  27.     width: 24px;
  28.     height: 24px;
  29.     line-height: 24px;
  30.     text-align: center;
  31.     border-radius: 13px;
  32.     font-weight: bold;
  33.     background: {xen:property primaryLighterStill};
  34.     border: 1px solid {xen:property primaryLighter};
  35. }
  36. .mostLikedPosts .likes:hover {
  37.     background-color: {xen:property secondaryLightest};
  38.     border-color: {xen:property secondaryLighter};
  39.     color: {xen:property secondaryDark};
  40.     text-decoration: none;
  41.     box-shadow: 0 0 10px {xen:property secondaryMedium};
  42. }
  43. .mostLikedPosts .meta {
  44.     font-size: 11px;
  45.     padding-top: 5px;
  46.     padding-bottom: 5px;
  47.     margin-left: 80px;
  48.     margin-bottom: -1px;
  49.     margin-top: 10px;
  50.     border: 1px solid {xen:property primaryLighterStill};
  51.     border-right: none;
  52.     border-top-left-radius: 10px;
  53. }
  54. .mostLikedPosts .meta dd {
  55.     margin-right: 10px;
  56. }
  57. .mostLikedPosts .meta dd strong {
  58.     font-weight: bold;
  59. }

Выберите дополнение, сохраните и выйдите. Мы просто набили список CSS стилей, которыми просто будем пользоваться в шаблоне. Тегами {xen:property} обозначены места, в которые будут вставлены данные из основных настроек стиля (найдите их, кстати). Обратите внимание, что мы вставили туда и настройку стиля, которую создали сами, исправляя ошибку автора (хоть слегка и безвкусно).

Теперь создайте еще один шаблон и назовите его likereviewrus_index. Вставьте в него следующее содержимое:

  1. <xen:title>{xen:phrase LikeReviewRus_Header}</xen:title>
  2.  
  3. <xen:navigation>
  4.     <xen:breadcrumb href="{xen:link likes-review}">{xen:phrase LikeReviewRus_Header}</xen:breadcrumb>
  5. </xen:navigation>
  6.  
  7. <xen:require css="likereviewrus.css" />
  8.  
  9. <div class="sectionMain mostLikedPosts">
  10.     <h2 class="subHeading">{xen:phrase LikeReviewRus_Description, 'numPosts={xen:count $likedPosts}'}</h2>
  11.  
  12.     <ol>
  13.         <xen:foreach loop="$likedPosts" value="$post">
  14.  
  15.             <li class="likedPost">
  16.                 <div class="primaryContent">
  17.                     <xen:avatar user="$post" size="m" img="true" />
  18.                     <h3><a href="{xen:link posts, $post}">{$post.title}</a></h3>
  19.                     <div class="muted">{xen:helper wordTrim, $post.message, 140}</div>
  20.                     <a href="{xen:link posts/likes, $post}" class="likes OverlayTrigger"><strong>{xen:number $post.likes}</strong></a>
  21.  
  22.                     <dl class="secondaryContent pairsInline meta">
  23.                         <dt>{xen:phrase posted_by}</dt> <dd><a href="{xen:link members, $post}" class="username">{$post.username}</a></dd>
  24.                         <dt>{xen:phrase date}</dt> <dd><xen:datetime time="$post.post_date" /></dd>
  25.                         <dt>{xen:phrase likes}</dt> <dd><a href="{xen:link posts/likes, $post}" class="OverlayTrigger">{xen:number $post.likes}</a></dd>
  26.                     </dl>
  27.                 </div>
  28.             </li>
  29.  
  30.         </xen:foreach>
  31.     </ol>
  32.  
  33.     <div class="sectionFooter">{xen:phrase showing_x_posts, 'numPosts={xen:count $likedPosts}'}</div>
  34. </div>

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

  • <xen:title> - тег указывает заголовок страницы.</xen:title>
  • <xen:require> - подключает указанный CSS файл.</xen:require>
  • <xen:navigation> и <xen:breadcrumb> - определяют, что будет показано в виде цепочки навигации. В данном случае это просто одна ссылка. Более сложные варианты мы здесь пока рассматривать не будем </xen:breadcrumb></xen:navigation>
  • {xen:link} - строит URL с использованием параметров (likes-review) согласно правилам роутинга форума. О роутинге немного позже.
  • {xen:phrase} просто вставляет значение фразы на языке, который пользователь указал в своих настройках. Если перевода для фразы на этот язык нет, будет показан оригинальный вариант на основном языке. В одинарных кавычках может следовать список параметров для фразы. Параметры внутри фразы определяются, как вы уже заметили, фигурными скобками.
  • {xen:count} - возвращает количество записей, полученных в результате запроса к базе данных. Сам запрос мы определим позже.
  • <xen:foreach> - определяет foreach цикл, как и в PHP</xen:foreach>
  • <xen:avatar> - вставляет аватар пользователя, определяя его по данным, указанном в параметре user.</xen:avatar>
  • {xen:helper} - просто стандартная функция внутри шаблона для обрезания длинного текста. Такие функции можно определять и самому.
  • {xen:number} и <xen:datetime> - форматируют переданные данные, согласно настройкам, заданным в текущем языке (на котором пользователь видит страницу)</xen:datetime>
  • Конструкции вроде {$post.username} - просто вставка значения с ключом username из массива в переменной шаблона $post. Как передавать переменные в шаблон я покажу чуть позже.

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

Теперь немного о том, как XenForo обрабатывает запросы. Если вы знакомы с MVC моделью, скорее всего, вы хорошо это представляете. Если нет, вкратце это выглядит примерно так. XenForo разбирает пришедший ему HTTP запрос и по правилам роутинга определяет, какому контроллеру передать управление для его обработки. Контроллер принимает запрос, создает модель данных (которая, например, просто читает данные из БД запросом), затем строит вид (фактически, превращает шаблоны в HTML код) и передает модель с данными ему. Получившееся месиво отправляется в браузер пользователя.

Для того, чтобы XenForo понял, на какой запрос нужно показывать нашу страницу со списком самых классных на форуме сообщений, нам нужно перво-наперво создать правило роутинга. Перейдите на вкладку Разработка и щелкните по ссылке Префиксы роутинга. Создайте новый публичный префикс. В качестве строки префикса укажите likes-review. В настройке использования класса для генерации ссылки выберите Никогда, в качестве имени класса укажите LikeReviewRus_Route_Prefix_LikesReview. Не забудьте верно выбрать дополнение.

Настройка в панели управления закончена. Теперь мы приступаем к кодингу. В первую очередь накодим наш класс для обработки префикса роутинга. Перейдите в папку Library и создайте там подкаталог LikeReviewRus, в ней папку Route, внутри Prefix. Вот уж воистину смерть кащеева у нас получилась. Теперь создавайте файл LikesReview.php его содержимое:

  1. <?php
  2.  
  3. class LikeReviewRus_Route_Prefix_LikesReview implements XenForo_Route_Interface
  4. {
  5.     /**
  6.     * Match a specific route for an already matched prefix.
  7.     *
  8.     * @see XenForo_Route_Interface::match()
  9.     */
  10.     public function match($routePath, Zend_Controller_Request_Http $request, XenForo_Router $router)
  11.     {
  12.         return $router->getRouteMatch('LikeReviewRus_ControllerPublic_Index', 'index', 'likes-review');
  13.     }
  14. }

В данном случае мы говорим, что при обнаружении префикса likes-review (мы его в админке настроили), нужно передавать управление контроллеру LikeReview_ControllerPublic_Index, передать ему в качестве действия "index", а в качестве majorsection - likes-review. Вторая ошибка Кира :)

Теперь создайте путь \library\LikeReviewRus\ControllerPublic, зайдите туда и создайте там файл Index.php. Его содержимое:

  1. <?php
  2.  
  3. class LikeReviewRus_ControllerPublic_Index extends XenForo_ControllerPublic_Abstract
  4. {
  5.     public function actionIndex()
  6.     {
  7.         $maxResults = XenForo_Application::get('options')->LikeReviewRusMaxToDisplay;
  8.  
  9.         $likedPosts = $this->_getLikeReviewModel()->getMostLikedPosts($maxResults);
  10.  
  11.         $viewParams = array(
  12.             'likedPosts' => $likedPosts
  13.         );
  14.  
  15.         return $this->responseView('LikeReviewRus_ViewPublic_Index', 'likereviewrus_index', $viewParams);
  16.     }
  17.  
  18.     /**
  19.      * @return LikeReview_Model_LikeReview
  20.      */
  21.     protected function _getLikeReviewModel()
  22.     {
  23.         return $this->getModelFromCache('LikeReviewRus_Model_LikeReview');
  24.     }
  25. }

Что тут происходит? У данного контроллера определено только одно действие (index, в названии метода использован camelCase). В этом действии извлекается содержимое настройки максимального количества результатов (помните, мы ее создавали). Затем создается модель данных для списка самых крутых постов, затем создаются параметры для передачи в вид (просто параметры для шаблона, $viewParams) и после этого строится вид (рендерится шаблон likereview_index, responseView) и ему передаются параметры. Вспомогательный метод _getLikeReviewModel просто достает модель из общего кэша, если она была закэширована.

Теперь переходим к нашей модели данных. Создайте путь library\LikeReviewRus\Model и войдите туда. Создайте файл LikeReview. Его содержимое:

  1. <?php
  2.  
  3. class LikeReviewRus_Model_LikeReview extends XenForo_Model
  4. {
  5.     /**
  6.      * Gets the most liked posts in descending order
  7.      *
  8.      * @param integer Maximum posts to fetch
  9.      *
  10.      * @return array
  11.      */
  12.     public function getMostLikedPosts($limit)
  13.     {
  14.         // Сначала выбираем данные о супер-постах напрямую из таблицы xf_liked_content,
  15.         // ограничивая количество переданным параметром
  16.         $limitedSql = $this->limitQueryResults("
  17.             SELECT content_id, COUNT(*) AS likes
  18.             FROM xf_liked_content
  19.             WHERE content_type = 'post'
  20.             GROUP BY content_id
  21.             ORDER BY likes DESC
  22.         ", $limit);
  23.  
  24.         // Получаем в массив содержимое первой колонки датасета (fetchCol) с ID номерами нужных нам постов
  25.         $postIds = $this->_getDb()->fetchCol($limitedSql);
  26.  
  27.         // Используем модель данных постов на форуме для получения списка постов по их ID номерам
  28.         // Указываем, что нам надо соединить посты с соответствующими им темами и пользователями
  29.         $postResults = $this->_getPostModel()->getPostsByIds($postIds, array
  30.         (
  31.             'join' => XenForo_Model_Post::FETCH_THREAD | XenForo_Model_Post::FETCH_USER
  32.         ));
  33.  
  34.         // Строим массив с результатом, где в качестве индекса использовано количество "лайков" поста
  35.         $posts = array();
  36.         foreach ($postResults AS $post)
  37.         {
  38.             $posts["$post[likes].$post[post_date]"] = $post;
  39.         }
  40.  
  41.         // Сортируем
  42.         krsort($posts);
  43.  
  44.         // Возвращаем
  45.         return $posts;
  46.     }
  47.  
  48.     /**
  49.      * @return XenForo_Model_Post
  50.      */
  51.     protected function _getPostModel()
  52.     {
  53.         // Возвращаем модель данных о постах из кэша
  54.         return $this->getModelFromCache('XenForo_Model_Post');
  55.     }
  56. }

Теперь попробуйте перейти на страницу www.xenforo.local/likes-review. Вы должны увидеть список постов с благодарностями. Обратите внимание на толстую уродливую рамку аватаров :)

Теперь попробуем добавить закладку на эту страницу в основную навигацию. Перейдите в админку на вкладку Разработка и создайте новый обработчик события. Обрабатывать будем событие navigation_tabs. Имя класса обработчика LikeReviewRus_Tabs_MainTab, имя метода addTab.

Еще создайте шаблон likesreviewrus_links со следующим содержимым:

  1. <ul class="secondaryContent blockLinksList">
  2.     <li><a href="{xen:link likes-review}">{xen:phrase LikeReviewRus_Header}</a></li>
  3. </ul>

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

Создайте путь library\LikeReviewRus\Tabs, перейдите туда и создайте там файл MainTab.php. Его содержимое:

  1. <?php
  2. class LikeReviewRus_Tabs_MainTab
  3. {
  4.     public static function addTab(array &$extraTabs, $selected)
  5.     {
  6.         $extraTabs['likes-review'] =array(
  7.             'title' =>  new XenForo_Phrase('LikeReviewRus_Header'), // Название основной вкладки
  8.             'href'   =>  XenForo_Link::buildPublicLink('likes-review'), // Строим ее ссылку
  9.             'selected' => ($selected == 'likes-review'), // Определяем, на ней ли мы сейчас
  10.             'linksTemplate' => 'likesreviewrus_links', // Определяем шаблон для рендеринга под-ссылок
  11.         );
  12.     }
  13. }

Навигация готова. Для того, чтобы она красиво отображалась, вам стоит включить ЧПУ-ссылки в настройках SEO форума. Для того, чтобы посмотреть, какие запросы использует страница, откройте ее, добавив в конец /_debug=1. В частности, вы увидите, что вызов new XenForo_Phrase генерирует отдельный запрос, что не есть хорошо. Для того, чтобы этого избежать, откройте фразу LikeReviewRus_Header и поставьте галочку, чтобы она добавлялась в глобальный кэш.

Вот и все. Надеюсь, вам понравилось :) Если есть какие-то опечатки - сообщайте. Впрочем, думаю, после всего описанного вам не составит труда их исправить самостоятельно, если они есть.

Обсудить статью можно здесь.