Skip to content

Создание новостного раздела

Цель

Необходимо на сайте создать раздел с новостями с разделением на подразделы. На странице списка новостей необходимо отобразить:

  • список с разделами
  • сортировку по популярности, по названию
  • фильтр по дате
  • список с элементами
  • пагинацию

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

На детальной странице необходимо отобразить:

  • детальное описание
  • блок с другими статьями

Реализация

Создание ИБ "Новости"

Для создания ИБ необходимо использовать модуль миграций В админ панели /bitrix/admin/rw_migration.php?lang=ru&q=create необходимо создать миграцию.

Пример миграции
php
class rw_m_1737362725_develop_create_ib_content_news extends \Realweb\Api\Module\Migration\Model\Scenario\Script
{
	public static function name(): string
	{
		return 'create_ib_content_news';
	}

	public static function hash(): string
	{
		return '663894aca543f15851945a981d63c3bb44e9ba0c';
	}

	public static function priority(): string
	{
		return self::PRIORITY_HIGH;
    }

	public static function approximatelyTime(): int
	{
		return 1;
    }

	public function commit(): void
	{
        (new \Realweb\Api\Module\Migration\Model\Builder\Iblock())
            ->save('content', 'news', function (\Realweb\Api\Module\Migration\Model\Entity\Iblock $rsIblock) {
                $rsIblock
                    ->name("Новости")
                    ->listPageUrl("#SITE_DIR#/news/")
                    ->sectionPageUrl("#SITE_DIR#/news/#SECTION_CODE#/")
                    ->detailPageUrl("#SITE_DIR#/news/#SECTION_CODE#/#ELEMENT_CODE#/")
                    ->requiredCode()
                    ->requiredSectionCode()
                    ->textHtml()
                    ->setIpropertyTemplates();

                $obProp = $rsIblock
                    ->saveProperty("TYPE")
                    ->name("Тип")
                    ->typeDropdown();
                $obProp->saveEnum('Новость')->xmlId('news')->sort(10)->byDefault();
                $obProp->saveEnum('Статья')->xmlId('article')->sort(20);

                $rsIblock->saveElementFormInterface(0)
                    ->setTabs(
                        (new \Realweb\Api\Module\Migration\Model\Entity\Option\Tab\Collection())
                            ->addItem(
                                (new \Realweb\Api\Module\Migration\Model\Entity\Option\Tab\Entity('Элемент'))
                                    ->addItem('ID')
                                    ->addItem('NAME')
                                    ->addItem('CODE')
                                    ->addItem('SORT')
                                    ->addItem('PROPERTY_TYPE')
                                    ->addItem('PREVIEW_PICTURE')
                                    ->addItem('PREVIEW_TEXT')
                                    ->addItem('DETAIL_TEXT')
                                )
                        );

                $rsIblock->saveSectionFormInterface(0)
                    ->setTabs(
                        (new \Realweb\Api\Module\Migration\Model\Entity\Option\Tab\Collection())
                            ->addItem(
                                (new \Realweb\Api\Module\Migration\Model\Entity\Option\Tab\Entity('Раздел'))
                                    ->addItem('ID')
                                    ->addItem('NAME')
                                    ->addItem('SORT')
                                    ->addItem('CODE')
                                )
                        );
            });
	}

	public function rollback(): void
	{

	}
}

После написания миграции необходимо ее выполнить, можно также это сделать через админ панель bitrix/admin/rw_migration.php?lang=ru&q=main

Создание модуля "Новости"

Для реализации основного функционала необходимо в рамках модуля realweb.api.module создать подмодуль news со следующей структурой.

Структура модуля

Контроллеры

С помощью контроллеров будет возвращаться JSON-объект в ответе на эндпойнт для фронта. Подробнее про контроллеры можно прочитать тут

ListController.php
php
namespace Realweb\Api\Module\News\Controller;

use Realweb\Api\Module\News\Model\Element;

/**
 * Class \Realweb\Api\Module\News\Controller\ListController
 */
class ListController extends \Realweb\Api\Controller\CollectionController
{
    protected function _getDataBase(): string
    {
        return Element\Database::class;
    }

    protected function _initParams(): void
    {
        $this
            ->getPagination()
            ->setParam('section_id', (int)$this->getRequestParam('category_id') ?: null)
            ->setParam('date_from', (string)$this->getRequestParam('date_from') ?: null)
            ->setParam('date_to', (string)$this->getRequestParam('date_to') ?: null)
            ->setParam('order', (string)$this->getRequestParam('order') ?: null)
            ->setPageSize((int)$this->getRequestParam('limit', 12))
            ->setCurrentPage((int)$this->getRequestParam('page', 1));
    }

    /**
     * @param Element\Collection $obCollection
     * @return void
     */
    protected function _process(\Realweb\Api\Model\Data\Collection $obCollection): void
    {
        $obCollection->processSections();
    }
}
Контроллер детальной элемента ItemController.php
php
namespace Realweb\Api\Module\News\Controller;

use Realweb\Api\Module\News\Model\Element;

/**
 * Class \Realweb\Api\Module\News\Controller\ItemController
 *
 * @method Element\Entity getEntity()
 */
class ItemController extends \Realweb\Api\Controller\EntityController
{
    public function checkGetParams(): void
    {
        if (!(int)$this->getRequestParam('id')) {
            $this->_addErrorParams();
        }
    }

    protected function _getDatabase(): string
    {
        return Element\Database::class;
    }

    protected function _initParams(): void
    {
        $this
            ->getPagination()
            ->setParam('id', (int)$this->getRequestParam('id'));
    }

    /**
     * @param Element\Collection $obCollection
     * @return void
     */
    protected function _processCollection(\Realweb\Api\Model\Data\Collection $obCollection): void
    {
        $obCollection->processSections();
    }

    /**
     * @param Element\Entity $obEntity
     */
    protected function _processEntity(\Realweb\Api\Model\Data\Entity $obEntity): void
    {
        $obEntity->processMetaValues();
    }
}
Контроллер списка разделов CategoriesController.php
php
namespace Realweb\Api\Module\News\Controller;

use Realweb\Api\Module\News\Model\Section;

/**
 * Class \Realweb\Api\Module\News\Controller\CategoriesController
 */
class CategoriesController extends \Realweb\Api\Controller\CollectionController
{
    protected function _getDataBase(): string
    {
        return Section\Database::class;
    }

    protected function _initParams(): void
    {
        $this
            ->getPagination()
            ->setAllRecords();
    }

    public function get(): ?array
    {
        $arResult = parent::get();
        if ($this->isReturnJsonData()) {
            return $arResult['items'];
        }

        return null;
    }
}
Контроллер детальной раздела Section/EntityController.php
php
namespace Realweb\Api\Module\News\Controller\Section;

use Realweb\Api\Module\News\Model\Section;

/**
 * Class  \Realweb\Api\Module\News\Controller\Section\EntityController
 *
 * @method Section\Entity getEntity()
 */
class EntityController extends \Realweb\Api\Controller\EntityController
{
	protected function _getDatabase(): string
	{
		return Section\Database::class;
	}

	protected function _initParams(): void
	{
		$this->getPagination()
			->setParam('id', $this->getParamNumber('id'))
			->setParam('code', $this->getParamString('code'));
	}

	/**
	 * @param Section\Entity $obEntity
	 */
	protected function _processEntity(\Realweb\Api\Model\Data\Entity $obEntity): void
	{
		$obEntity->processMetaValues();
	}
}
Список похожих новостей RelatedController
php
namespace Realweb\Api\Module\News\Controller;

/**
 * Class \Realweb\Api\Module\News\Controller\RelatedController
 */
class RelatedController extends \Realweb\Api\Controller
{
    public function checkGetParams(): void
    {
        if (!$this->getRequestParam('id')) {
            $this->_addErrorParams();
        }
    }

    public function get(): ?array
    {
        $obEntity = (new ItemController())
            ->setParam('id', (int)$this->getRequestParam('id'))
            ->getEntity();
        if ($obEntity) {
            $obCollection = (new ListController())
                ->setParam('section_id', $obEntity->getIblockSectionId())
                ->setParam('limit', 3)
                ->getCollection();
            $obCollection->deleteByKey($obEntity->getId());

            return array(
                'items' => $obCollection->toJson()
            );
        }

        return null;
    }
}

Модели

Модель предназначена для получения данных и работы с ними. Подробнее про работу с ИБ можно прочитать тут

Элемент
Коллекция элементов Element\Collection
php
namespace Realweb\Api\Module\News\Model\Element;

use Realweb\Api\Model\Main\Pagination;
use Realweb\Api\Module\News\Model\Section;

/**
 * Class \Realweb\Api\Module\News\Model\Element\Collection
 *
 * @method Entity|null getByKey(int $iValue)
 * @method Entity[] getCollection()
 */
class Collection extends \Realweb\Api\Model\Iblock\Element\Collection
{
	/**
	 * @param Entity $obItem
	 * @return $this
	 */
	public function addItem($obItem): self
	{
		if (!$this->getByKey($obItem->getId())) {
			$this->_collection[] = $obItem;
			$this->_keys[] = $obItem->getId();
		}

		return $this;
	}

    public function processSections(): void
    {
        if ($this->count()) {
            $arIds = array();
            foreach ($this->getCollection() as $obItem) {
                if ($iId = $obItem->getIblockSectionId()) {
                    $arIds[$iId] = $iId;
                }
            }
            if ($arIds) {
                $obCollection = Section\Database::getObjectCollection(
                    (new Pagination())->setParam('ids', $arIds)
                );
                foreach ($this->getCollection() as $obItem) {
                    if ($obSection = $obCollection->getByKey($obItem->getIblockSectionId())) {
                        $obItem->setSection($obSection);
                    }
                }
            }
        }
    }
}
Класс для получения элементов Element\Database
php
namespace Realweb\Api\Module\News\Model\Element;

use Bitrix\Main\Type\Date;
use Realweb\Api\Model\Main\Pagination;
use Realweb\Api\Model\Iblock\Element\Query;

/**
 * Class \Realweb\Api\Module\News\Model\Element\Database
 *
 * @method static Collection getObjectCollection(?Pagination $obNav = null)
 */
class Database extends \Realweb\Api\Model\Iblock\Element\Database
{
    public static function getIblockId(): int
    {
        return IBLOCK_CONTENT_NEWS;
    }

    public static function getFields(): array
    {
        return array(
            'ID',
            'IBLOCK_ID',
            'NAME',
            'CODE',
            'IBLOCK_SECTION_ID',
            'ACTIVE_FROM',
            "PREVIEW_TEXT",
            "DETAIL_TEXT",
        );
    }

    public static function getCollection(): string
    {
        return Collection::class;
    }

    public static function getEntity(): string
    {
        return Entity::class;
    }

    public static function getQuery(?Pagination $obNav = null): Query
    {
        $obQuery = parent::getQuery($obNav)
            ->setSelect(self::getFields())
            ->registerPreviewPicture()
            ->where('ACTIVE', '=', 'Y')
            ->addSelect('IBLOCK_SECTION.CODE', 'SECTION_CODE')
            ->registerDropdownField('TYPE', self::getIblockId())
            ->setOrder(array('SORT' => 'ASC', 'ACTIVE_FROM' => 'DESC'));

        if ($obNav !== null) {
            if ($strValue = $obNav->getParamString('date_from')) {
                $obQuery->where('ACTIVE_FROM', '>=', new Date($strValue));
            }
            if ($strValue = $obNav->getParamString('date_to')) {
                $obQuery->where('ACTIVE_FROM', '<=', new Date($strValue));
            }
            if ($strValue = $obNav->getParamString('order')) {
                $arValue = explode("-", $strValue);
                switch ($arValue[0]) {
                    case 'popular':
                        $obQuery->setOrder(array('SORT' => $arValue[1] == 'asc' ? 'ASC': 'DESC', 'ACTIVE_FROM' => 'DESC'));
                        break;
                    case 'name':
                        $obQuery->setOrder(array('NAME' => $arValue[1] == 'asc' ? 'ASC': 'DESC', 'ACTIVE_FROM' => 'DESC'));
                        break;
                }

            }
        }

        return $obQuery;
    }
}
Сущность элемента Element\Entity
php
namespace Realweb\Api\Module\News\Model\Element;

use Bitrix\Main\Type\DateTime;
use Realweb\Api\Module\News\Model\Section;
use Realweb\Api\Model\Iblock\Element\Property\Enum;

/**
 * Class \Realweb\Api\Module\News\Model\Element\Entity
 *
 * @method DateTime getActiveFrom()
 */
class Entity extends \Realweb\Api\Model\Iblock\Element\Entity
{
    private ?Section\Entity $_section = null;
    private ?Enum\Entity $_type = null;

    public function setData(?array $arData = null): self
    {
        $this->_type = new Enum\Entity(self::getByAlias($arData, 'TYPE_'));

        return parent::setData($arData);
    }

    public function getType(): Enum\Entity
    {
        return $this->_type;
    }

    public function getSection(): ?Section\Entity
    {
        return $this->_section;
    }

    public function setSection(Section\Entity $obEntity): self
    {
        $this->_section = $obEntity;

        return $this;
    }

    public function toJson(): array
    {
        return array(
            "id" => $this->getId(),
            "link" => $this->getUrl(),
            "title" => $this->getName(),
            "type" => $this->getType()->getValue(),
            "summary" => $this->getPreviewText(),
            "image" => $this->toJsonPreviewPicture(),
            "date" => $this->getActiveFrom()?->format('d.m.Y, H:i'),
        );
    }

    public function toJsonDetail(): array
    {
        return array(
            "id" => $this->getId(),
            "title" => $this->getName(),
            "type" => $this->getType()->getValue(),
            "summary" => $this->getDetailText(),
            "date" => $this->getActiveFrom()?->format('d.m.Y, H:i'),
        );
    }

    protected static function _getDatabase(): string
    {
        return Database::class;
    }
}
Раздел
Коллекция разделов Section\Collection
php
namespace Realweb\Api\Module\News\Model\Section;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Collection
 *
 * @method Entity|null getByKey(int $iValue)
 * @method Entity[] getCollection()
 */
class Collection extends \Realweb\Api\Model\Iblock\Section\Collection
{
	/**
	 * @param Entity $obItem
	 * @return $this
	 */
	public function addItem($obItem): self
	{
		if (!($this->getByKey($obItem->getId()))) {
			$this->_collection[] = $obItem;
			$this->_keys[] = $obItem->getId();
		}

		return $this;
	}
}
Класс для получения разделов Section\Database
php
namespace Realweb\Api\Module\News\Model\Section;

use Realweb\Api\Model\Main\Pagination;
use Realweb\Api\Model\Iblock\Section\Query;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Database
 */
class Database extends \Realweb\Api\Model\Iblock\Section\Database
{
	public static function getCollection(): string
	{
		return Collection::class;
	}

	public static function getEntity(): string
	{
		return Entity::class;
	}

	public static function getIblockId(): int
	{
		return IBLOCK_CONTENT_NEWS;
	}

    public static function getFields(): array
    {
        return array(
            'ID',
            'IBLOCK_ID',
            'NAME',
            'CODE',
        );
    }

	public static function getObjectCollection(?Pagination $obNav = null): Collection
	{
		return parent::getObjectCollection($obNav);
	}

	public static function getQuery(?Pagination $obNav = null): Query
	{
		$obSelect = parent::getQuery($obNav)
			->setSelect(self::getFields())
            ->where('ACTIVE', '=', 'Y')
			->setOrder(array('SORT' => 'ASC'));

		if ($obNav !== null) {

		}

		return $obSelect;
	}
}
Сущность разделов Section\Entity
php
namespace Realweb\Api\Module\News\Model\Section;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Entity
 */
class Entity extends \Realweb\Api\Model\Iblock\Section\Entity
{
	public function toJson(): array
	{
        return array(
            'id' => (int) $this->getId(),
            'link' => $this->getUrl(),
            'name' => $this->getName(),
        );
	}

	protected static function _getDatabase(): string
	{
		return Database::class;
	}
}

Настройка роутинга

Настройка роутинга осуществляется с помошью модуля роутинга. Конфигурация задается в классе \Realweb\Api\Module\Route\Model\Data.

Структура модуля роутинга

На событии onBuild добавляется роутинг для публичной части сайта.

php
\Realweb\Api\Module\Route\Model\Helper::getInstance()
    ->addRoute(new Page\News\Index(SITE_DIR . 'news/'))
    ->addRoute(new Page\News\Section(SITE_DIR . 'news/#section_code#/'))
    ->addRoute(new Page\News\Element(SITE_DIR . 'news/#section_code#/#element_code#/'))

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

На событии onApiBuild добавляется роутинг для АПИ методов.

php
\Realweb\Api\Module\Route\Model\Api\Helper::getInstance()
    ->addRoute('/news-categories/', Module\News\Controller\CategoriesController::class)
    ->addRoute('/news-list/', Module\News\Controller\ListController::class)
    ->addRoute('/news-item/', Module\News\Controller\ItemController::class)
    ->addRoute('/news-related/', Module\News\Controller\RelatedController::class)

Тут указывается связка эндпойнтов с классами-обработчиками этих эндпойнтов. Эти эндпойнты фронт будет использовать для получения данных для страниц.

Классы для получения данных страницы

Для фронтенда данные страницы (тип страницы, метатеги, ХК и другое) получаются с помощью эндпойнта /api/entity/, в который передается текущий путь страницы path, например, список новостей /api/entity/?path/news/.

Класс для разводящей страницы
Page\News\Index
php
namespace Realweb\Api\Module\Route\Model\Page\News;

use Realweb\Api\Module\Route;

/**
 * Class \Realweb\Api\Module\Route\Model\Page\News\Index
 */
class Index extends Route\Model\Page\Iblock
{
	public function getCode(): string
	{
		return 'page-news';
	}

    protected function _getIblockId(): int
    {
        return IBLOCK_CONTENT_NEWS;
    }
}
Класс для страницы категории
Page\News\Section
php
namespace Realweb\Api\Module\Route\Model\Page\News;

use Realweb\Api\Module\News;
use Realweb\Api\Module\Route;
use Realweb\Api\Model\Main\Meta;

/**
 * Class \Realweb\Api\Module\Route\Model\Page\News\Section
 */
class Section extends Route\Model\Page\Base
{
	public function getCode(): string
	{
		return 'page-news-category';
	}

	public function getEntity(): ?News\Model\Section\Entity
	{
		if ($strCode = $this->getPath()->getBySlugName('section_code')?->getValue()) {
			if (!array_key_exists($strCode, $this->_entity)) {
				$obEntity = (new News\Controller\Section\EntityController())
					->setParam('code', $strCode)
					->getEntity();
				if ($obEntity) {
                    $this->_entity[$strCode] = $obEntity;
				}
			}

			return $this->_entity[$strCode];
		}

		return null;
	}

    public function getBreadcrumbs(): ?Route\Model\Breadcrumb\Collection
    {
        $obEntity = $this->getEntity();
        $obCollection = Route\Model\Breadcrumb\Collection::getBase();
        $obCollection->addItem(new Route\Model\Breadcrumb\Entity(array(
            'link' => $obEntity->getIblock()->getUrl(),
            'text' => $obEntity->getIblock()->getName(),
        )));
        $obCollection->addItem(new Route\Model\Breadcrumb\Entity(array(
            'link' => $obEntity->getUrl(),
            'text' => $obEntity->getName(),
        )));

        return $obCollection;
    }

	public function getMeta(): ?Meta\Collection
	{
		return $this->getEntity()->getMetaValues();
	}

    public function toJsonEntity(): array
    {
        $arResult = parent::toJsonEntity();
        $arResult['id'] = $this->getEntity()->getId();

        return $arResult;
    }
}
Класс для детальной страницы новости
Page\News\Element
php
namespace Realweb\Api\Module\Route\Model\Page\News;

use Realweb\Api\Module\News;
use Realweb\Api\Module\Route;
use Realweb\Api\Model\Main\Meta;

/**
 * Class \Realweb\Api\Module\Route\Model\Page\News\Element
 */
class Element extends Route\Model\Page\Base
{
    public function getCode(): string
    {
        return 'page-news-item';
    }

    public function getEntity(): ?News\Model\Element\Entity
    {
        if ($strCode = $this->getPath()->getBySlugName('element_code')?->getValue()) {
            if (!array_key_exists($strCode, $this->_entity)) {
                $obEntity = (new News\Controller\ItemController())
                    ->setParam('code', $strCode)
                    ->getEntity();
                if ($obEntity?->getUrl() == $this->getPath()->toString()) {
                    $this->_entity[$strCode] = $obEntity;
                }
            }

            return $this->_entity[$strCode];
        }

        return null;
    }

    public function getBreadcrumbs(): Route\Model\Breadcrumb\Collection
    {
        $obEntity = $this->getEntity();
        $obCollection = Route\Model\Breadcrumb\Collection::getBase();
        $obCollection->addItem(new Route\Model\Breadcrumb\Entity(array(
            'link' => $obEntity->getIblock()->getUrl(),
            'text' => $obEntity->getIblock()->getName(),
        )));
        $obCollection->addItem(new Route\Model\Breadcrumb\Entity(array(
            'link' => $obEntity->getSection()->getUrl(),
            'text' => $obEntity->getSection()->getName(),
        )));
        $obCollection->addItem(new Route\Model\Breadcrumb\Entity(array(
            'link' => $obEntity->getUrl(),
            'text' => $obEntity->getName(),
        )));

        return $obCollection;
    }

    public function getMeta(): ?Meta\Collection
    {
        return $this->getEntity()->getMetaValues();
    }

    public function toJsonEntity(): array
    {
        $arResult = parent::toJsonEntity();
        $arResult['id'] = $this->getEntity()->getId();

        return $arResult;
    }
}