Давайте создадим дополнение

Для некоторых людей увязнуть прямо в проекте - лучший способ учиться, и цель состоит в том, чтобы в следующих разделах вы научились создавать дополнения с нуля. Будь готов; это не простая демонстрация типа «Hello world». На самом деле это довольно полнофункциональное демо-дополнение, которое охватывает ряд концепций XF2.

Дополнение, которую мы собираемся создать, позволит пользователям с соответствующими разрешениями «добавлять» темы и отображать на новой странице. Мы даже настроим процесс, который автоматически включает темы на определенных форумах. Мы будем использовать новый маршрут для этого с именем portal и в конечном итоге установим его в качестве маршрута индексной страницы и установим вкладку "Home", которая будет выбираться при просмотре этой страницы.

Создать дополнение

На протяжении всего дополнения мы будем использовать идентификатор Demo/Portal. Первое, что нам нужно сделать, это создать дополнение, для этого нам нужно открыть окно командной строки / оболочки / терминала, изменить каталог на корневой каталог установки XF (где находится cmd.php) и запустить следующую команду и введите ответы, показанные ниже:

Terminal

$ php cmd.php xf-addon:create

Enter an ID for this add-on: Demo/Portal

Enter a title: Demo - Portal

Enter a version ID: This integer will be used for internal variable comparisons.
Each release of your addon should increase this number: 1000010

Version string set to: 1.0.0 Alpha

Does this add-on supersede a XenForo 1 add-on? (y/n) n

The addon.json file was successfully written out to /var/www/src/addons/Demo/Portal/addon.json

Does your add-on need a Setup file? (y/n) y

Does your Setup need to support running multiple steps? (y/n) y

The Setup.php file was successfully written out to /var/www/src/addons/Demo/Portal/Setup.php

Теперь дополнение создано, теперь вы обнаружите, что у вас есть новый каталог в каталоге src/addons, и вы найдете дополнение в списке "Installed add-ons" в Admin CP.

Один из созданных файлов - это файл addon.json, который в настоящее время выглядит следующим образом:

{
    "legacy_addon_id": "",
    "title": "Demo - Portal",
    "description": "",
    "version_id": 1000010,
    "version_string": "1.0.0 Alpha",
    "dev": "",
    "dev_url": "",
    "faq_url": "",
    "support_url": "",
    "extra_urls": [],
    "require": [],
    "icon": ""
}

Давайте заполним некоторые из этих деталей:

{
    "legacy_addon_id": "",
    "title": "Demo - Portal",
    "description": "Add-on which will display featured threads on the forum home page.",
    "version_id": 1000010,
    "version_string": "1.0.0 Alpha",
    "dev": "You!",
    "dev_url": "",
    "faq_url": "",
    "support_url": "",
    "extra_urls": [],
    "require": [],
    "icon": "fa-home"
}

Теперь мы добавили description, имя разработчика (dev) и указали, что мы хотим отображать иконку (icon). Иконка может быть либо путем (относительно корня дополнения), либо именем Иконки Font Awesome, как мы сделали здесь.

Поскольку мы не заменяем дополнение XenForo 1, мы можем игнорировать legacy_addon_id. Для полного объяснения всех свойств в файле addon.json, обратитесь к разделу структуры дополнения.

Создание класса установки

Ну, строго говоря, класс уже создан и записан в Setup.php, но сейчас он ничего не делает. По сути, у нас есть класс скелета, который выглядит так:

<?php

namespace Demo\Portal;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\AddOn\StepRunnerUpgradeTrait;

class Setup extends AbstractSetup
{
        use StepRunnerInstallTrait;
        use StepRunnerUpgradeTrait;
        use StepRunnerUninstallTrait;
}

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

Начнем с импорта некоторых полезных классов схемы. Если вы хотите узнать о них больше, вы можете обратиться к разделу Управление схемой.

Сразу после последнего объявления use добавьте следующие строки:

use XF\Db\Schema\Alter;
use XF\Db\Schema\Create;

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

<?php

namespace Demo\Portal;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\AddOn\StepRunnerUpgradeTrait;

use XF\Db\Schema\Alter;
use XF\Db\Schema\Create;

class Setup extends \XF\AddOn\AbstractSetup
{
    use StepRunnerInstallTrait;
    use StepRunnerUpgradeTrait;
    use StepRunnerUninstallTrait;

    public function installStep1()
    {
        $this->schemaManager()->alterTable('xf_forum', function(Alter $table)
        {
            $table->addColumn('demo_portal_auto_feature', 'tinyint')->setDefault(0);
        });
    }
}

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

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

public function installStep2()
{
    $this->schemaManager()->alterTable('xf_thread', function(Alter $table)
    {
        $table->addColumn('demo_portal_featured', 'tinyint')->setDefault(0);
    });
}

Этот шаг, аналогичный шагу выше, на этот раз добавит новый столбец в таблицу xf_thread. Мы будем использовать этот столбец в качестве кэшированного значения, чтобы быстро определить, представлена ли тема или нет, без необходимости выполнять дополнительные запросы или поиск в таблице xf_demo_portal_featured_thread.

Говоря об этом, мы должны добавить эту таблицу сейчас. На этот раз прямо под installStep2():

public function installStep3()
{
    $this->schemaManager()->createTable('xf_demo_portal_featured_thread', function(Create $table)
    {
        $table->addColumn('thread_id', 'int');
        $table->addColumn('featured_date', 'int');
        $table->addPrimaryKey('thread_id');
    });
}

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

Те же принципы применимы и к именованию. Существенное отличие состоит в том, что все таблицы должны иметь префикс xf_. Причина этого в том, что если выполняется чистая установка XF, мы можем удалить все таблицы с префиксом xf_, включая таблицы, созданные дополнения.

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

Terminal

$ php cmd.php xf-addon:install-step Demo/Portal 1 $ php cmd.php xf-addon:install-step Demo/Portal 2 $ php cmd.php xf-addon:install-step Demo/Portal 3

Расширение сущности форума

Пока мы добавили столбец в таблицу xf_forum, теперь пора расширить структуру сущности Forum. Нам нужно сделать это, чтобы объект знал о нашем новом столбце, и чтобы данные можно было читать и записывать в него через объект.

Note

Для следующих шагов потребуется включить Режим разработки. Не забудьте установить Demo/Portal в качестве значения defaultAddOn в config.php.

Первым шагом в этом процессе является создание "Code event listener". Это можно сделать в Admin CP в разделе «Разработка», нажмите на ссылку "Code event listeners" и нажмите кнопку "Add code event listener".

Нам нужно прослушивать событие entity_structure. Мы будем использовать это, чтобы изменить структуру сущности форума по умолчанию, чтобы добавить наш недавно созданный столбец demo_portal_auto_feature.

В поле "Event hint" мы введем имя расширяемого класса, например: XF\Entity\Forum. Это гарантирует, что наш слушатель будет выполняться только в сущности форума.

Для класса "Execute callback" введите Demo\Portal\Listener, а для метода введите forumEntityStructure.

Стоит добавить описание, чтобы объяснить, для чего предназначен этот слушатель, так как это поможет легче идентифицировать слушателя в списке слушателей событий кода. "Расширение структуры XF\Entity\Forum" должно быть достаточно. Наконец, убедитесь, что выбрано дополнение "Demo - Portal".

Прежде чем мы нажмем "Save", нам нужно создать класс Listener. Итак, создайте новый файл с именем Listener.php в src/addons/Demo/Portal. Первоначально содержимое этого файла должно быть следующим. Мы знаем, какие аргументы требуются этой функции, из документации под селектором событий кода.

<?php

namespace Demo\Portal;

use XF\Mvc\Entity\Entity;

class Listener
{
    public static function forumEntityStructure(\XF\Mvc\Entity\Manager $em, \XF\Mvc\Entity\Structure &$structure)
    {

    }
}

Обратите внимание на объявление use между именем namespace и class. Мы будем ссылаться на класс, объявленный здесь, более одного раза, поэтому его объявление здесь позволяет нам ссылаться на него по гораздо более короткому псевдониму, в данном случае Entity.

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

Прежде чем мы добавим функциональный код в нашу новую функцию, возможно, сейчас самое время посмотреть, как работает система вывода при разработке. Проверьте новые каталоги и файлы, добавленные в каталог дополнения. В частности, в каталоге _output/code_event_listeners есть новый файл JSON, который должен выглядеть следующим образом:

{
    "event_id": "entity_structure",
    "execute_order": 10,
    "callback_class": "Demo\\Portal\\Listener",
    "callback_method": "forumEntityStructure",
    "active": true,
    "hint": "XF\\Entity\\Forum",
    "description": "Extends the XF\\Entity\\Forum structure"
}

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

Хорошо, давайте добавим еще код. Вернувшись в класс Listener, добавьте в функцию forumEntityStructure следующее:

$structure->columns['demo_portal_auto_feature'] = ['type' => Entity::BOOL, 'default' => false];

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

Расширение сущности темы

Опять же, поскольку мы добавили новый столбец в таблицу xf_thread table, мы должны сделать так, чтобы объект Thread знал об этом. Это очень похоже на то, что мы сделали выше.

Вернитесь к "Add code event listener" и снова прослушайте entity_structure. "Event hint" на этот раз будет XF\Entity\Thread. Мы можем использовать тот же класс обратного вызова, что и раньше (Demo\Portal\Listener), но на этот раз метод будет называться threadEntityStructure. Добавьте описание, аналогичное предыдущему. Перед сохранением мы должны добавить код непосредственно под функцией forumEntityStructure:

public static function threadEntityStructure(\XF\Mvc\Entity\Manager $em, \XF\Mvc\Entity\Structure &$structure)
{
    $structure->columns['demo_portal_featured'] = ['type' => Entity::BOOL, 'default' => false];
}

Этот код почти идентичен тому, что мы добавили в структуру сущности форума; на самом деле единственная разница - это имя столбца. Но нам нужно добавить еще кое-что. Мы должны создать отношение сущностей, чтобы в дальнейшем, если нам понадобится доступ к указанной сущности темы (которую мы создаем в следующем разделе), мы могли бы легко сделать это с помощью запроса файндера. Ниже строки $structure->columns добавьте:

$structure->relations['FeaturedThread'] = [
    'entity' => 'Demo\Portal:FeaturedThread',
    'type' => Entity::TO_ONE,
    'conditions' => 'thread_id',
    'primary' => true
];

Смотрите Отношения для получения дополнительной информации об отношениях. Нажмите "Save", чтобы сохранить слушателя.

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

Выше в installStep3() мы создали новую таблицу. Нам нужно будет создать объект для взаимодействия и создания новых записей в этой таблице. Поскольку это совершенно новый объект, нам не нужно ничего делать, кроме создания класса внутри src/addons/Demo/Portal/Entity/FeaturedThread.php, каркас которого будет выглядеть следующим образом:

<?php

namespace Demo\Portal\Entity;

use XF\Mvc\Entity\Structure;

class FeaturedThread extends \XF\Mvc\Entity\Entity
{

}

Нам нужно использовать это для определения структуры сущности, которая представляет нашу новую таблицу xf_demo_portal_featured_thread, которую мы создали ранее. Структура этого объекта должна выглядеть так:

public static function getStructure(Structure $structure)
{
    $structure->table = 'xf_demo_portal_featured_thread';
    $structure->shortName = 'Demo\Portal:FeaturedThread';
    $structure->primaryKey = 'thread_id';
    $structure->columns = [
        'thread_id' => ['type' => self::UINT, 'required' => true],
        'featured_date' => ['type' => self::UINT, 'default' => time()]
    ];
    $structure->getters = [];
    $structure->relations = [
        'Thread' => [
            'entity' => 'XF:Thread',
            'type' => self::TO_ONE,
            'conditions' => 'thread_id',
            'primary' => true
        ],
    ];

    return $structure;
}

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

Изменение формы редактирования форума

Теперь нам нужен способ изменить шаблон forum_edit, чтобы добавить туда новый флажок, который в конечном итоге может записывать обратно в новый столбец, который мы теперь создали. Мы сделаем это, создав модификацию шаблона. Это делается на панели администратора в разделе "Appearance", а затем нажмите "Template modifications". Нажмите вкладку "Admin", а затем кнопку "Add template modification".

В поле "Template", введите "forum_edit". Это шаблон, который нам нужно изменить.

В поле "Modification key" введите "demo_portal_forum_edit". Это уникальный ключ, который определяет модификацию Вашего шаблона. Предпочтительным условием для этого является, как минимум, упоминание дополнения с последующим изменением имени шаблона.

Поле "Description" должно содержать некоторый текст, который поможет вам определить цель этой модификации при просмотре списка модификаций шаблона. Что-то вроде "Adds auto feature checkbox to the forum_edit template" должно быть достаточно.

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

Самый простой способ установить флажок в этом разделе - это просто заменить верхний флажок, поэтому в поле "Find" добавьте:

<xf:option name="allow_posting"

И в поле замены:

<xf:option name="demo_portal_auto_feature" selected="$forum.demo_portal_auto_feature"
    label="Автоматически добавлять темы в этом форуме"
    hint="Если этот флажок установлен, любые новые темы, опубликованные на этом форуме, будут автоматически отмечены." />
$0

Нам не нужно беспокоиться о создании фраз, но мы сможем подобрать их позже. Обратите внимание, что атрибут name совпадает с именем столбца, который мы создали ранее, и, что более важно, отмеченное состояние строки флажка также считывает вновь добавленный столбец из объекта форума.

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

Мы можем использовать кнопку "Test", чтобы проверить, работает ли замена должным образом. При нажатии кнопки тестирования появится наложение с измененным шаблоном. Если все пойдет хорошо, должна быть выделена зеленая область с новым кодом, который мы хотим добавить.

Note

Это довольно простая замена. Для более продвинутого сопоставления можно также использовать тип "Regular expression". Подробное объяснение работы с регулярными выражениями выходит за рамки этого руководства, но в Интернете есть много ресурсов, которые могут помочь.

Наконец, нажмите «Сохранить», чтобы сохранить модификацию шаблона. Если все прошло успешно, когда вы вернетесь к списку модификаций шаблона, вы увидите, что в сводке журнала отображается 1 / 0 / 0 , что означает, что модификация успешно применена один раз. Еще лучший индикатор того, что он работал, как планировалось, - это перейти на страницу "Nodes", указанную в разделе "Forums" в Admin CP, и отредактировать существующий форум. Теперь должна появиться наша недавно добавленная модификация шаблона.

Расширение процесса сохранения форума

У нас есть столбец, у нас есть пользовательский интерфейс для передачи ввода в этот столбец, теперь нам нужно обработать сохранение данных в этот столбец. Мы сделаем это, расширив контроллер Forum и расширив специальный метод, который вызывается при сохранении узла и его данных. Во-первых, давайте создадим "Class extension", которое можно найти в разделе "Development" в Admin CP. Нажмите "Add class extension".

Здесь нам нужно указать "Base class name", которое является именем класса, который мы расширяем, в данном случае это будет XF\Admin\Controller\Forum. И нам нужно указать "Extension class name", которое является классом, который будет расширять базовый класс. Введите его как Demo\Portal\XF\Admin\Controller\Forum. Мы должны создать этот класс перед тем, как нажать «Сохранить».

Создайте новый файл в src/addons/Demo/Portal/XF/Admin/Controller с именем Forum.php. Это может показаться довольно длинным путем, но мы рекомендуем такой путь для расширенных классов. Это позволяет вам более легко идентифицировать файлы, представляющие расширенные классы, благодаря тому факту, что они находятся в каталоге с тем же именем, что и расширенный идентификатор «дополнения» (в данном случае XF). Это также дает понять, какой именно класс был расширен, поскольку структура каталогов следует по тому же пути, что и класс по умолчанию. На данный момент содержимое файла должно выглядеть так:

<?php

namespace Demo\Portal\XF\Admin\Controller;

class Forum extends XFCP_Forum
{

}

Смотрите Расширение классов и Тип подсказки для получения дополнительной информации.

Нажмите «Сохранить», чтобы сохранить "Class extension". Теперь мы можем добавить код. Конкретный метод, который нам нужно расширить, - это защищенная функция под названием saveTypeData. При расширении любого существующего метода в любом классе важно проверить исходный метод по нескольким причинам. Во-первых, мы хотим убедиться, что аргументы, которые мы используем в расширенном методе, совпадают с аргументами расширяемого метода. Во-вторых, нам нужно знать, что на самом деле делает этот метод. Например, должен ли метод возвращать что-то определенного типа или определенный объект? Это, безусловно, относится к большинству действий контроллера, как мы упоминали в разделе Изменение ответа действия контроллера (должным образом). Однако, хотя этот метод находится внутри контроллера, на самом деле это не действие самого контроллера. Фактически, этот конкретный метод является "void" методом; от него ничего не ожидается. Однако мы всегда должны гарантировать, что вызываем родительский метод в нашем расширенном методе, поэтому давайте просто добавим сам новый метод без нового кода, который нам нужно добавить:

protected function saveTypeData(FormAction $form, \XF\Entity\Node $node, \XF\Entity\AbstractNode $data)
{
    parent::saveTypeData($form, $node, $data);
}

Warning

Список аргументов этого конкретного метода предполагает, что у нас есть объявление use, которое заменяет полный класс \XF\Mvc\FormAction на просто FormAction. Поэтому вам нужно будет добавить это объявление использования самостоятельно. Добавьте use XF\Mvc\FormAction; между строками namespace и class.

Итак, прямо сейчас мы расширили этот метод, и наше расширение должно быть вызвано, но сейчас оно не делает ничего, кроме вызова своего родительского метода. Теперь нам нужно получить значение ввода со страницы редактирования форума и применить его к объекту $data (которым в данном случае является объект Forum).

protected function saveTypeData(FormAction $form, \XF\Entity\Node $node, \XF\Entity\AbstractNode $data)
{
    parent::saveTypeData($form, $node, $data);

    $form->setup(function() use ($data)
    {
        $data->demo_portal_auto_feature = $this->filter('demo_portal_auto_feature', 'bool');
    });
}

Использование объекта FormAction позволяет нам иметь различные точки расширения в процессе, который выполняется в ходе обычной отправки формы. Он доступен не для всех действий контроллера. Это гораздо более распространено, например, в Admin CP, которые часто следуют простой модели CRUD (создание, чтение, обновление, удаление). Многие другие процессы в XF происходят внутри объекта службы, который обычно имеет определенные точки расширения, связанные с запущенной службой. Это конкретное использование объекта FormAction несколько отличается от того, с чем вы обычно сталкиваетесь. Сохранение узла - это несколько другой процесс, потому что, помимо работы с сущностью узла, вы также будете работать с узлом связанного типа, например субъект форума. Однако у нас есть доступ к объекту действия формы в этом методе, поэтому мы должны его использовать. Мы использовали его здесь, чтобы добавить определенное поведение к фазе "setup" процесса. А именно, когда вызывается метод run() объекта FormAction, он будет проходить различные фазы в определенном порядке. Неважно, в каком порядке эти поведения были добавлены к объекту, они все равно будут выполняться в порядке setup, validate, apply, complete.

Код, который мы добавили выше, позволяет нам установить для нашего столбца demo_portal_auto_feature в сущности форума любое значение, сохраненное для ввода demo_portal_auto_feature, который мы добавили на страницу редактирования форума. Теперь должно быть возможно проверить, что все это работает. Просто отредактируйте форум по своему выбору и установите флажок. вы должны уметь наблюдать две вещи. Во-первых, когда вы вернетесь к редактированию этого форума, этот флажок должен быть установлен. Во-вторых, если вы посмотрите в таблице xf_forum форум, который вы только что отредактировали, поле demo_portal_auto_feature теперь должно быть установлено на 1. Оставьте это значение включенным для этого форума, так как в конечном итоге мы будем автоматически отображать темы с этого форума.

Настройка автоматического включения темы

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

В XF2 мы активно используем служебные объекты. Обычно они используют подход типа "setup and go"; вы настраиваете свою конфигурацию, а затем вызываете метод для завершения действия. Мы используем служебный объект для настройки и создания темы, так что это идеальное место для добавления необходимого кода. Все начинается с другого расширения класса, поэтому перейдите на страницу "Add class extension".

На этот раз базовым классом будет XF\Service\Thread\Creator, а классом расширения будет Demo\Portal\XF\Service\Thread\Creator, и, как обычно, этот новый класс будет выглядеть примерно как код ниже. Создайте этот код по пути src/addons/Demo/Portal/XF/Service/Thread/Creator.php, затем нажмите «Сохранить», чтобы создать расширение.

<?php

namespace Demo\Portal\XF\Service\Thread;

class Creator extends XFCP_Creator
{

}

Пока мы здесь, мы также создадим еще одно расширение. Базой будет XF\Pub\Controller\Forum, а классом расширения будет Demo\Portal\XF\Pub\Controller\Forum. Создайте следующий код в пути src/addons/Demo/Portal/XF/Pub/Controller/Forum.php и нажмите «Сохранить»:

<?php

namespace Demo\Portal\XF\Pub\Controller;

class Forum extends XFCP_Forum
{

}

В конечном итоге мы собираемся расширить метод _save() в нашем расширенном объекте-создателе темы, чтобы мы могли добавить нашу тему после того, как он был создан. Чтобы соответствовать подходу "setup and go", мы создадим метод, который можно использовать, чтобы указать, следует ли создавать тему как избранный или нет. Для этого нам понадобятся две вещи; свойство класса для хранения значения (по умолчанию - null) и общедоступный метод, позволяющий установить это свойство.

protected $featureThread;

public function setFeatureThread($featureThread)
{
    $this->featureThread = $featureThread;
}

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

protected function setupThreadCreate(\XF\Entity\Forum $forum)
{
    /** @var \Demo\Portal\XF\Service\Thread\Creator $creator */
    $creator = parent::setupThreadCreate($forum);

    return $creator;
}

Как и ожидалось, на самом деле это ничего не делает; вызывается расширенный код, но все, что он делает, это возвращает то, что было возвращено родительским вызовом. Теперь мы должны изменить $creator, чтобы настроить отображение, если оно применимо к форуму, с которым мы сейчас работаем.

Между строками $creator и return добавьте:

if ($forum->demo_portal_auto_feature)
{
    $creator->setFeatureThread(true);
}

Теперь мы можем добавить метод _save() к расширенному классу создателя:

protected function _save()
{
    $thread = parent::_save();

    return $thread;
}

Чтобы убедиться, что эта тема будет представлен, между строками $thread и return нам просто нужно добавить:

if ($this->featureThread && $thread->discussion_state == 'visible')
{
    /** @var \Demo\Portal\Entity\FeaturedThread $featuredThread */
    $featuredThread = $thread->getRelationOrDefault('FeaturedThread');
    $featuredThread->save();

    $thread->fastUpdate('demo_portal_featured', true);
}

Поскольку мы ранее создали отношение FeaturedThread для сущности темы, мы можем использовать это отношение и для создания! Здесь мы используем метод getRelationOrDefault(). Это будет видеть, действительно ли это отношение возвращает существующую запись, а если нет, оно создаст объект и установит для него любые значения по умолчанию, даже идентификатор темы! Это означает, что на самом деле нам нужно сделать немного больше, чем получить отношение по умолчанию и сохранить его, чтобы вставить в базу данных.

Кроме того, мы должны установить для поля demo_portal_featured значение true. Поскольку объект темы уже был сохранен (когда исходный класс сохранил объект), мы можем использовать метод fastUpdate() для быстрого обновления этого поля.

Теперь нам нужно все это попробовать и убедиться, что это работает. Перейдите на форум, на котором вы включили опцию demo_portal_auto_feature ранее, и создайте новую тему. Единственный способ узнать, работает ли он прямо сейчас, - это проверить таблицу xf_demo_portal_featured_thread, и при этом мы должны увидеть там новую запись!

Создайте страницу портала

Еще предстоит проделать значительный объем работы, прежде чем мы закончим, но теперь у нас есть возможность отображать темы, было бы неплохо, если бы мы могли где-то их отображать, поэтому давайте начнем создавать нашу страницу портала.

Для этого нам нужен новый общедоступный маршрут. Перейдите в Admin CP и в разделе "Development" нажмите "Routes", затем нажмите "Add route: Public". Пока мы будем делать это довольно просто. Префиксом маршрута будет "portal", контекстом раздела будет "home", а контроллером - "Demo\Portal:Portal".

Теперь мы должны создать этот контроллер по пути src/addons/Demo/Portal/Pub/Controller/Portal.php со следующим основным содержимым:

<?php

namespace Demo\Portal\Pub\Controller;

class Portal extends \XF\Pub\Controller\AbstractController
{

}

Мы хотим, чтобы наш портал отображался для людей, когда они посещают страницу портала index.php?portal. В этом URL-адресе нет части "action" - только только что созданный префикс маршрута. Имея это в виду, код, который нам нужно добавить для отображения страницы портала, должен находиться в методе actionIndex(). Вот основной код, который нам понадобится:

public function actionIndex()
{
    $viewParams = [];
    return $this->view('Demo\Portal:View', 'demo_portal_view', $viewParams);
}

Это пока не совсем сработает, потому что мы еще не создали шаблон, но пока этого достаточно, чтобы хотя бы продемонстрировать, что наш маршрут и контроллер общаются друг с другом. Таким образом, посещение портала index.php?portal должно как минимум отображать 'Template error'.

Как было упомянуто в разделе View reply, первый аргумент - это класс представления, но нам не нужно создавать этот класс. При необходимости этот класс может быть расширен другими дополнениями, даже если он не существует. Второй аргумент - это шаблон, который нам нужно создать сейчас по пути src/addons/Demo/Portal/_output/templates/public/demo_portal_view.html. На данный момент этот шаблон должен просто содержать следующее:

<xf:title>Portal</xf:title>

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

Теперь пора начать добавлять код, который будет отображать список избранных тем. Первым шагом к этому является создание репозитория для некоторых из наших общих базовых запросов поиска. Итак, создайте новый файл по пути src/addons/Demo/Portal/Repository/FeaturedThread.php и добавьте следующий код:

<?php

namespace Demo\Portal\Repository;

use XF\Mvc\Entity\Finder;
use XF\Mvc\Entity\Repository;

class FeaturedThread extends Repository
{
    /**
     * @return Finder
     */
    public function findFeaturedThreadsForPortalView()
    {
        $visitor = \XF::visitor();

        $finder = $this->finder('Demo\Portal:FeaturedThread');
        $finder
            ->setDefaultOrder('featured_date', 'DESC')
            ->with('Thread', true)
            ->with('Thread.User')
            ->with('Thread.Forum', true)
            ->with('Thread.Forum.Node.Permissions|' . $visitor->permission_combination_id)
            ->with('Thread.FirstPost', true)
            ->with('Thread.FirstPost.User')
            ->where('Thread.discussion_type', '<>', 'redirect')
            ->where('Thread.discussion_state', 'visible');

        return $finder;
    }
}

Здесь мы используем средство поиска для запроса всех избранных тем в обратном порядке featured_date и присоединяемся к таблице xf_thread и из этой таблицы присоединяемся к таблице xf_user для создателя темы, таблица xf_forum и таблицаxf_post оттуда снова присоединиться к таблице xf_user для создателя сообщения. Мы утверждали, что ветка, форум и первое сообщение должны существовать, указав для этого аргумента true, поэтому они будут выполняться как INNER JOIN, тогда как пользовательские запросы будут выполняться с LEFT JOIN. Возможно, что автор некоторых тем и сообщений может не существовать (например, если они были опубликованы автоматически системой подачи RSS или опубликованы гостями).

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

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

Давайте теперь воспользуемся этим в нашем методе actionIndex() внутри нашего контроллера портала. Измените существующую строку $viewParams = []; на следующую:

/** @var \Demo\Portal\Repository\FeaturedThread $repo */
$repo = $this->repository('Demo\Portal:FeaturedThread');

$finder = $repo->findFeaturedThreadsForPortalView();

$viewParams = [
    'featuredThreads' => $finder->fetch()
];

На этом этапе мы не будем беспокоиться об изменении базового средства поиска, которое мы получили из репозитория. Вместо этого давайте начнем фактически видеть некоторые результаты и обновим шаблон demo_portal_view следующим образом (после тегов <xf:title>):

<xf:if is="$featuredThreads is not empty">
    <xf:foreach loop="$featuredThreads" value="$featuredThread">
        <xf:macro name="thread_block"
            arg-thread="{$featuredThread.Thread}"
            arg-post="{$featuredThread.Thread.FirstPost}"
            arg-featuredThread="{$featuredThread}"
        />
    </xf:foreach>
<xf:else />
    <div class="blockMessage">Пока нет ни одной темы.</div>
</xf:if>

<xf:macro name="thread_block" arg-thread="!" arg-post="!" arg-featuredThread="!">
    <xf:css src="message.less" />

    <div class="block">
        <div class="block-container" data-xf-init="lightbox">
            <h4 class="block-header"><a href="{{ link('threads', $thread) }}">{$thread.title}</a></h4>
            <div class="block-body">
                <xf:macro name="message"
                    arg-post="{$post}"
                    arg-thread="{$thread}"
                    arg-featuredThread="{$featuredThread}"
                />
            </div>
            <div class="block-footer">
                <a href="{{ link('threads', $thread) }}">Продолжить чтение...</a>
            </div>
        </div>
    </div>
</xf:macro>

<xf:macro name="message" arg-post="!" arg-thread="!" arg-featuredThread="!">
    <div class="message message--post message--simple">
        <div class="message-inner">
            <div class="message-cell message-cell--main">
                <div class="message-content js-messageContent">
                    <div class="message-attribution">
                        <div class="contentRow contentRow--alignMiddle">
                            <div class="contentRow-figure">
                                <xf:avatar user="{$post.User}" size="xxs" defaultname="{$post.username}" href="" />
                            </div>
                            <div class="contentRow-main contentRow-main--close">
                                <ul class="listInline listInline--bullet u-muted">
                                    <li><xf:username user="{$thread.User}" /></li>
                                    <li><xf:date time="{$featuredThread.featured_date}" /></li>
                                    <li><a href="{{ link('forums', $thread.Forum) }}">{$thread.Forum.title}</a></li>
                                    <li>{{ phrase('replies:') }} {$thread.reply_count|number}</li>
                                </ul>
                            </div>
                        </div>
                    </div>
                    <div class="message-userContent lbContainer js-lbContainer"
                         data-lb-id="post-{$post.post_id}"
                         data-lb-caption-desc="{{ $post.User ? $post.User.username : $post.username }} &middot; {{ date_time($post.post_date) }}"
                    >
                        <blockquote class="message-body">
                            {{ bb_code($post.message, 'post', $post.User, {
                                'attachments': $post.attach_count ? $post.Attachments : [],
                                'viewAttachments': $thread.canViewAttachments()
                            }) }}
                        </blockquote>
                    </div>
                </div>
            </div>
        </div>
    </div>
</xf:macro>

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

Мы начинаем шаблон с условия, которое читается как <xf:if is="$featuredThreads is not empty">. Это необходимо для проверки того, что объект, возвращаемый средством поиска, действительно содержит записи избранной темы. Если этого не произошло, мы выводим соответствующее сообщение.

Если у нас есть какие-то записи, нам нужно перебрать каждую, чтобы отобразить ее. Для каждой записи мы вызываем macro. Макросы - это многоразовые части кода шаблона, которые самодокументируются (в том смысле, что вы видите, какие аргументы поддерживаются) и поддерживают свою собственную область видимости, которая не может быть загрязнена аргументами в шаблоне, вызывающем макрос; Это означает, что макросам известны только явно переданные аргументы и глобальный параметр $xf.

Макрос блока темы отображает базовый блок для избранной темы, а затем вызывает другой макрос для отображения каждого сообщения.

Реализация вкладки навигации

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

Для этого мы должны использовать прослушиватель событий кода, чтобы изменить URL-адрес на URL-адрес нашего портала. В административной панели в разделе Development нажмите "Code event listeners" и нажмите "Add code event listener". Прослушайте событие home_page_url, класс обратного вызова снова будет Demo\Portal\Listener, и на этот раз метод будет называться homePageUrl.

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

public static function homePageUrl(&$homePageUrl, \XF\Mvc\Router $router)
{
    $homePageUrl = $router->buildLink('canonical:portal');
}

Наконец, нам следует подумать об изменении маршрута индексной страницы к странице нашего портала. Перейдите в Admin CP и в разделе "Setup" нажмите "Options", а затем "Basic board information". Измените параметр "Index page route" на portal/.

Пока вы находитесь в Admin CP, давайте посмотрим, что происходит теперь, когда вы нажимаете на заголовок Board в заголовке. Это приведет вас к вашей индексной странице. Все в порядке, эта индексная страница теперь должна быть вашим порталом! В дополнение к этому, вкладка Home должна быть видна и выбрана.

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

Добавление (или удаление) тем вручную

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

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

Первый шаг - это новая модификация шаблона. Поэтому перейдите в "Add template modification" (убедитесь, что в списке "Template modifications" выбрана вкладка "Public"). На этот раз мы модифицируем шаблон helper_thread_options, мы будем использовать demo_portal_helper_thread_options в качестве ключа, и вы можете написать разумное описание. На самом деле мы можем сделать здесь "Simple replacement", поэтому оставьте этот переключатель выбранным и в поле "Find" добавьте:

<xf:if is="$thread.canLockUnlock()">

В поле "Replace" добавьте:

<xf:if is="($thread.isInsert() AND !$thread.Forum.demo_portal_auto_feature AND $thread.canFeatureUnfeature())
    OR ($thread.isUpdate() && $thread.canFeatureUnfeature())"
>
    <xf:option label="{{ phrase('demo_portal_featured') }}" name="featured" value="1" selected="{$thread.demo_portal_featured}">
        <xf:hint>{{ phrase('demo_portal_featured_hint') }}</xf:hint>
        <xf:afterhtml>
            <xf:hiddenval name="_xfSet[featured]" value="1" />
        </xf:afterhtml>
    </xf:option>
</xf:if>
$0

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

Быстрый "Test" должен показать, что этот дополнительный код будет вставлен чуть выше существующего флажка "Open" в существующем <xf:checkboxrow>. Если все в порядке, нажмите "Save".

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

В разделе "Appearance" перейдите в "Phrases" и нажмите "Add phrase". Убедитесь, что ваше дополнение выбрано. "Title" первой фразы будет "demo_portal_featured", а текст будет просто "Featured". Нажмите "Save and Exit". Снова нажмите "Add phrase". "Title" для второй фразы будет "demo_portal_featured_hint", а текст будет "Featured threads will appear on the Portal page."

Вернемся к коду шаблона, который мы только что добавили в модификацию; возможно, вы что-то заметили. Мы вызвали метод для сущности темы, canFeatureUnfeature(), но этот метод еще не существует. В конечном итоге мы собираемся использовать это для проверки разрешений, которая будет определять, может ли пользователь вручную включать тему или нет.

Чтобы добавить этот метод, нам нужно новое расширение класса для сущности XF\Entity\Thread. Итак, сделайте это сейчас, как мы это делали раньше. Расширенный класс будет Demo\Portal\XF\Entity\Thread, поэтому создайте его по пути src/addons/Demo/Portal/XF/Entity/Thread.php с содержимым:

<?php

namespace Demo\Portal\XF\Entity;

class Thread extends XFCP_Thread
{
    public function canFeatureUnfeature()
    {
        return true;
    }
}

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

Чтобы проверить, работает ли это на данный момент, откройте одну из ранее представленных тем и выберите "Edit thread" в меню инструментов. Мы должны увидеть, что в строке флажка "Set thread status" есть добавленный флажок "Featured", и он должен быть отмечен, что указывает на то, что эта тема действительно включена в список избранных.

Теперь мы можем перейти к изменению службы редактора тем, чтобы найти это значение и функцию или ее отсутствие соответственно. Для этого нам понадобятся два новых расширения класса. Вернитесь на страницу "Add class extensions". Первый будет иметь базовый класс XF\Pub\Controller\Thread и класс расширения Demo\Portal\XF\Pub\Controller\Thread. У второго будет базовый класс XF\Service\Thread\Editor и класс расширения Demo\Portal\XF\Service\Thread\Editor.

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

<?php

namespace Demo\Portal\XF\Service\Thread;

class Editor extends XFCP_Editor
{
    protected $featureThread;

    public function setFeatureThread($featureThread)
    {
        $this->featureThread = $featureThread;
    }

    protected function _save()
    {
        $thread = parent::_save();

        if ($this->featureThread !== null && $thread->discussion_state == 'visible')
        {
            /** @var \Demo\Portal\Entity\FeaturedThread $featuredThread */
            $featuredThread = $thread->getRelationOrDefault('FeaturedThread', false);

            if ($this->featureThread)
            {
                if (!$featuredThread->exists())
                {
                    $featuredThread->save();
                    $thread->fastUpdate('demo_portal_featured', true);
                }
            }
            else
            {
                if ($featuredThread->exists())
                {
                    $featuredThread->delete();
                    $thread->fastUpdate('demo_portal_featured', false);
                }
            }
        }

        return $thread;
    }
}

Это немного сложнее, чем код в службе создания. Например, могут быть ситуации, когда тема редактируется, а у пользователя нет разрешения на редактирование темы, и поэтому мы не показываем флажки. В этих случаях мы не хотим автоматически предполагать, что тема должна быть лишена функций. Поскольку свойство класса $featureThread по умолчанию имеет значение null, мы можем использовать это так, чтобы, по сути, свойство имело три состояния. В этом случае null будет означать "no change", true будет означать, что мы включили тему, а false будет означать, что мы удалили его.

В случае отсутствия функций мы фактически просто удаляем указанный объект темы, вызывая метод delete(). В обоих случаях мы снова используем метод fastUpdate(), чтобы обновить кешированное значение в сущности темы, чтобы представить текущее выделенное состояние.

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

<?php

namespace Demo\Portal\XF\Pub\Controller;

class Thread extends XFCP_Thread
{
    public function setupThreadEdit(\XF\Entity\Thread $thread)
    {
        /** @var \Demo\Portal\XF\Service\Thread\Editor $editor */
        $editor = parent::setupThreadEdit($thread);

        $canFeatureUnfeature = $thread->canFeatureUnfeature();
        if ($canFeatureUnfeature)
        {
            $editor->setFeatureThread($this->filter('featured', 'bool'));
        }

        return $editor;
    }
}

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

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

Нам просто нужно добавить следующий код под методом setupThreadEdit(), который мы добавили выше:

public function finalizeThreadReply(\XF\Service\Thread\Replier $replier)
{
    parent::finalizeThreadReply($replier);

    $setOptions = $this->filter('_xfSet', 'array-bool');
    if ($setOptions)
    {
        $thread = $replier->getThread();

        if ($thread->canFeatureUnfeature() && isset($setOptions['featured']))
        {
            $replier->setFeatureThread($this->filter('featured', 'bool'));
        }
    }
}

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

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

if ($forum->demo_portal_auto_feature)
{
    $creator->setFeatureThread(true);
}

Этим:

if ($forum->demo_portal_auto_feature)
{
    $creator->setFeatureThread(true);
}
else
{
    $setOptions = $this->filter('_xfSet', 'array-bool');
    if ($setOptions)
    {
        $thread = $creator->getThread();

        if ($thread->canFeatureUnfeature() && isset($setOptions['featured']))
        {
            $creator->setFeatureThread($this->filter('featured', 'bool'));
        }
    }
}

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

Теперь мы должны протестировать создание 3 тем, чтобы убедиться, что это работает должным образом. Сначала на форуме с включенным автоматическим включением, чтобы убедиться, что он все еще работает, затем на форуме без включенного автоматического включения, с установленным флажком "Featured" и снова с ним. Предполагая, что все работает, идем дальше.

Улучшение страницы портала

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

Сначала мы должны настроить наш код так, чтобы отображались только X избранных тем, а также мы должны добавить некоторую навигацию по страницам. На этом этапе, если вы еще этого не сделали, возможно, стоит указать еще несколько тем, чтобы мы могли действительно протестировать разбиение на страницы!

Для начала нам нужно вернуться к нашему контроллеру портала и добавить код в начало метода actionIndex():

$page = $this->filterPage();
$perPage = 5;

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

Следующее, что нужно сделать, это изменить эту строку:

$finder = $repo->findFeaturedThreadsForPortalView();

К этому:

$finder = $repo->findFeaturedThreadsForPortalView()
    ->limitByPage($page, $perPage);

Это изменяет наш запрос так, что он будет ограничиваться значениями страниц / страниц, которые мы определили выше. Это автоматически вычислит правильный предел ($perPage) и смещение (($page - 1) * $perPage) для текущей страницы. Затем нам нужно передать еще несколько параметров в параметры нашего представления, поэтому измените их:

$viewParams = [
    'featuredThreads' => $finder->fetch()
];

На:

$viewParams = [
    'featuredThreads' => $finder->fetch(),
    'total' => $finder->total(),
    'page' => $page,
    'perPage' => $perPage
];

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

Если вы вернетесь на портал, вы увидите только 5 избранных тем. Однако теперь нам нужно добавить навигацию по страницам. Итак, откройте шаблон demo_portal_view и сразу после закрывающего тега </xf:foreach> добавьте следующее:

<xf:pagenav page="{$page}" perpage="{$perPage}" total="{$total}" link="portal" wrapperclass="block" />

Перезагрузив страницу портала на этом этапе, если у вас есть более 5 избранных тем, вы теперь увидите навигацию по страницам в нижней части списка избранных тем.

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

Позиции виджетов добавляются в Admin CP в разделе "Development". Перейдите на страницу "Widget positions" и нажмите "Add widget position". Введите "Position ID" для demo_portal_view_sidebar, "Title" для Demo portal view: Sidebar и соответствующее описание. Убедившись, что позиция включена и выбран правильный идентификатор дополнения, нажмите "Save".

Чтобы добавить эту позицию в шаблон, просто добавьте следующее под тегом <xf:title>:

<xf:widgetpos id="demo_portal_view_sidebar" position="sidebar" />

Конечно, мы по-прежнему не увидим боковую панель, пока не добавим к ней несколько виджетов. Сами виджеты не назначаются дополнениями, поэтому виджеты, которые вы создаете для этой позиции, если вы хотите поставить некоторые настроенные виджеты по умолчанию, необходимо будет добавить в класс Setup.

Для простоты мы просто продублируем виджеты, которые в настоящее время назначены на позицию forum_list_sidebar (по умолчанию). Итак, мы добавим их в новый метод installStep4() в класс Setup:

public function installStep4()
{
    $this->createWidget('demo_portal_view_members_online', 'members_online', [
        'positions' => ['demo_portal_view_sidebar' => 10]
    ]);

    $this->createWidget('demo_portal_view_new_posts', 'new_posts', [
        'positions' => ['demo_portal_view_sidebar' => 20]
    ]);

    $this->createWidget('demo_portal_view_new_profile_posts', 'new_profile_posts', [
        'positions' => ['demo_portal_view_sidebar' => 30]
    ]);

    $this->createWidget('demo_portal_view_forum_statistics', 'forum_statistics', [
        'positions' => ['demo_portal_view_sidebar' => 40]
    ]);

    $this->createWidget('demo_portal_view_share_page', 'share_page', [
        'positions' => ['demo_portal_view_sidebar' => 50]
    ]);
}

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

Terminal

$ php cmd.php xf-addon:install-step Demo/Portal 4

Реализация разрешений и оптимизации

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

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

Для начала перейдите к контроллеру портала и измените эту строку:

->limitByPage($page, $perPage);

На:

->limit($perPage * 3);

И ниже добавьте:

$featuredThreads = $finder->fetch()
    ->filter(function(\Demo\Portal\Entity\FeaturedThread $featuredThread)
    {
        return ($featuredThread->Thread->canView());
    })
    ->sliceToPage($page, $perPage);

Наконец измените:

'featuredThreads' => $finder->fetch(),

На:

'featuredThreads' => $featuredThreads,

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

'attachments': $post.attach_count ? $post.Attachments : [],

Прямо сейчас это будет генерировать дополнительный запрос для каждого сообщения. Поэтому вместо этого мы должны попытаться выполнить единый запрос для всех отображаемых сообщений и заранее добавить их к сообщениям. Возможно, это звучит сложнее, чем есть на самом деле. Просто добавьте приведенный ниже код под строкой ->slice(0, $perPage, true);.

$threads = $featuredThreads->pluckNamed('Thread');
$posts = $threads->pluckNamed('FirstPost', 'first_post_id');

/** @var \XF\Repository\Attachment $attachRepo */
$attachRepo = $this->repository('XF:Attachment');
$attachRepo->addAttachmentsToContent($posts, 'post');

Сначала мы используем метод pluckNamed(), чтобы получить коллекцию тем, а затем снова, чтобы получить коллекцию сообщений (с ключом по идентификатору сообщения) из тем. Когда у нас есть сообщения, мы можем просто передать их специальному методу в репозитории вложений, который выполняет один запрос и «гидратирует» отношение вложений для каждого сообщения.

Последняя вещь, связанная с разрешениями, которую нужно завершить, - это создать новое разрешение для управления тем, кто может вручную включать / отключать темы. Для этого в Admin CP в разделе "Development" нажмите "Permission definitions" и нажмите "Add permission". "Permission group" будет "forum", "Permission ID" будет demoPortalFeature, "Title" должно быть Can feature / unfeature threads, установите "Interface group" на Forum moderator permissions и после выбора подходящего порядок отображения и, убедившись, что ваше дополнение выбрано, нажмите "Save".

Чтобы действительно использовать это разрешение, нам нужно вернуться к нашей расширенной сущности темы, чтобы изменить метод canFeatureUnfeature(). Замените return true; на:

return \XF::visitor()->hasNodePermission($this->node_id, 'demoPortalFeature');

На этом этапе, поскольку разрешения не имеют значений по умолчанию, если вы перейдете к редактированию какой-либо цепочки, вы должны обнаружить, что флажок "Featured" отсутствует. Но если вы дадите себе это разрешение, флажок вернется. Итак, это должно продемонстрировать, что разрешение работает должным образом!

Создание некоторых опций

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

На панели администратора в разделе Setup выберите Options и нажмите кнопку "Add option group". Мы просто назовем "Group ID" demoPortal и дадим ему название "Demo - Portal options". Присвойте ему соответствующие ̀"Description" и "Display order" и нажмите "Save".

Теперь нажмите "Add option". Установите для параметра "Option ID" значение demoPortalFeaturedPerPage, "Title" на Featured threads per page, измените формат на Spin box, "Data type" на Positive integer и "Default value" на 10. Нажмите "Save".

Чтобы реализовать это, вернитесь к контроллеру портала и измените:

$perPage = 5;

На:

$perPage = $this->options()->demoPortalFeaturedPerPage;

Наверное, не помешает добавить еще один вариант. Возможно, другой полезной опцией будет возможность изменить порядок сортировки по умолчанию с xf_demo_portal_featured_thread.feartured_date на xf_thread.post_date. Вернитесь в группу "Demo - Portal options" и нажмите "Add option".

Установите "Option ID" на demoPortalDefaultSort, "Title" на Default sort order и "Edit format" на Radio buttons. Для "Format parameters" установите их следующим образом:

plain featured_date={{ phrase('demo_portal_featured_date') }} post_date={{ phrase('demo_portal_post_date') }}

Наконец, установите "Default value" на featured_date и нажмите "Save".

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

Установите значение параметра на "Post date".

Строго говоря, мы могли бы просто обновить наш метод репозитория, чтобы использовать новую опцию напрямую, однако, возможно, стоит посмотреть, как работают пользовательские методы поиска. Создайте новый файл по пути src/addons/Demo/Portal/Finder/FeaturedThread.php с содержимым:

<?php

namespace Demo\Portal\Finder;

use XF\Mvc\Entity\Finder;

class FeaturedThread extends Finder
{
    public function applyFeaturedOrder($direction = 'ASC')
    {
        $options = \XF::options();

        if ($options->demoPortalDefaultSort == 'featured_date')
        {
            $this->setDefaultOrder('featured_date', $direction);
        }
        else
        {
            $this->setDefaultOrder('Thread.post_date', $direction);
        }

        return $this;
    }
}

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

В нашем репозитории избранных тем найдите:

->setDefaultOrder('featured_date', 'DESC')

И измените на:

->applyFeaturedOrder('DESC')

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

В шаблоне demo_portal_view меняем:

<li><xf:date time="{$featuredThread.featured_date}" /></li>

На:

<li>
    <xf:if is="$xf.options.demoPortalDefaultSort == 'featured_date'">
        <xf:date time="{$featuredThread.featured_date}" />
    <xf:else />
        <xf:date time="{$thread.post_date}" />
    </xf:if>
</li>

Невозможность изменения видимости

Чтобы приблизиться к этому, нам нужно будет снова изменить сущность Thread, но на этот раз мы сделаем это с помощью события entity_post_save. Как мы упоминали в Жизненный цикл объекта, метод `_postSave()- это то место, где действия могут выполняться в результате вставки объекта или обновлено. Первоначально мы отключим тему, когда она больше не будет видна.

Итак, вернитесь на страницу "Add code event listeners" и на этот раз послушайте событие entity_post_save. Подсказкой события на этот раз будет XF\Entity\Thread. Для обратного вызова execute мы будем использовать тот же класс, что и раньше (Demo\Portal\Listener), но мы добавим сюда новый метод с именем threadEntityPostSave. Давайте добавим этот метод сейчас, чтобы он был там, когда мы сохраняем слушателя:

public static function threadEntityPostSave(\XF\Mvc\Entity\Entity $entity)
{

}

Нажмите «Сохранить», чтобы сохранить слушателя.

Содержимое этой функции довольно простое, давайте посмотрим на это:

if ($entity->isUpdate())
{
    $visibilityChange = $entity->isStateChanged('discussion_state', 'visible');
    if ($visibilityChange == 'leave')
    {
        $featuredThread = $entity->FeaturedThread;
        if ($featuredThread)
        {
            $featuredThread->delete();
            $entity->fastUpdate('demo_portal_featured', false);
        }
    }
}

Раньше у нас были темы без признаков, но на этот раз мы хотим сделать это условным для состояния темы. Мы можем обнаружить изменения состояния, используя метод isStateChanged. Это вернет либо enter, либо leave для имени столбца и переданного значения. Например, если discussion_state изменится с visible на deleted, тогда метод вернет leave в приведенном выше примере.

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

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

Для этого нам понадобится другой слушатель, на этот раз для события entity_post_delete. Итак, добавьте это, используя тот же класс обратного вызова, и на этот раз имя метода threadEntityPostDelete. Добавьте следующий код в класс слушателя:

public static function threadEntityPostDelete(\XF\Mvc\Entity\Entity $entity)
{
    $featuredThread = $entity->FeaturedThread;
    if ($featuredThread)
    {
        $featuredThread->delete();
    }
}

После нажатия кнопки «Сохранить» для сохранения слушателя стоит провести тест. Чтобы проверить это, вам может быть лучше следить за таблицей xf_demo_portal_featured_thread, поскольку пока код уже не будет отображать невидимые темы, но всегда важно не оставлять потерянные данные. Все хорошо, мы почти закончили...

Последние штрихи

Говоря о потерянных данных, мы должны приводить в порядок базу данных всякий раз, когда дополнения удаляется. Мы можем сделать это в классе Setup, который мы создали ранее.

Мы собираемся создать 3 новых метода, которые соответствуют нашим первым трем шагам установки:

public function uninstallStep1()
{
    $this->schemaManager()->alterTable('xf_forum', function(Alter $table)
    {
        $table->dropColumns('demo_portal_auto_feature');
    });
}

public function uninstallStep2()
{
    $this->schemaManager()->alterTable('xf_thread', function(Alter $table)
    {
        $table->dropColumns('demo_portal_featured');
    });
}

public function uninstallStep3()
{
    $this->schemaManager()->dropTable('xf_demo_portal_featured_thread');
}

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

Сборка дополнения

Последний шаг для любого дополнения - его выпуск! Это включает извлечение файлов XML из базы данных (которые поставляются в пакете и используются для установки), вычисление хэша каждого файла и добавление его в наш hashes.json и упаковку только соответствующих файлов в ZIP-файл.

К счастью, это можно сделать с помощью одной команды CLI! Просто выполните команду ниже:

Terminal

$ php cmd.php xf-addon:build-release Demo/Portal

Performing add-on export.

Exporting data for Demo - Portal to ../src/addons/Demo/Portal/_data.

10/10 [============================] 100%

Written successfully.

Building release ZIP.

Writing release ZIP to ../src/addons/Demo/Portal/_releases.

Release written successfully.

Итак, на этом мы завершаем наше демонстрационное дополнение! Если вы хотите загрузить исходный код этого дополнения, созданный с использованием тех самых команд, которые были продемонстрированы выше, нажмите здесь: Demo-Portal-1.0.0 Alpha.zip.