Skip to content

Модуль новостей. Детальная страница раздела

Содержание


Формулировка задачи

Согласно спецификации необходимо вывести данные раздела новостей Параметры запроса:

  • Уникальный символьный код раздела code

Вывести следующие поля:

  • Название
  • Описание
  • Детальная картинка
  • Свойство типа список выбора "Тема"
  • Множественное свойство типа привязка к элементам "Баннер" - вывести картинку для анонса, название и ссылку
  • Ссылка на детальную страницу раздела. Ссылка на детальную страницу строится из символьных кодов родительских разделов + символьный код раздела
  • Хлебные крошки
  • Количество активных элементов в разделе
  • Метатеги - title, h1, description, keywords

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

Создание контроллера

Расположим контроллер в модуле News в пространстве Section. Т.к. контролер будет выводить детальную инофрмацию о сущности, то так и назовем контроллер \Realweb\Api\Module\News\Controller\Section\EntityController

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{
	public function get()
	{

	}
}

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

Согласно документации перед вызовом метода get будет вызван метод checkGetParams, если он реализован. После этого, если в основном компоненте будут содержаться ошибки, метод get выполняться не будет, а ошибки выведутся в браузер. В любом контроллере объект основного компонента realweb.api доступен через вызов метода getComponent.

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{

	public function checkGetParams(): void
	{
		$strCode = $this->getRequestParam('code','');
		if(strlen($strCode) == 0){
			$this->getComponent()->addErrorNoParams();
		}
	}

	public function get()
	{

	}
}

Модель разделов инфоблока

В пространстве Model\Section создадим модель для работы с разделом с методом получения раздела по символьному коду и получением тега для кеша

php
<?php

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

/**
 * Class \Realweb\Api\Module\News\Model\Section\Database
 */
class Database
{
	public static function getByCode($strCode): array
	{

	}

	public static function getCacheTag(): string
	{
		return "iblock_id_" . IBLOCK_CONTENT_NEWS;
	}
}

Реализация основного метода контроллера

Для реализации основного метода сразу будем использовать кеширование данных

Для передачи символьного кода в коллбек воспользуемся объектом пагинации/параметров \Realweb\Api\Model\Main\Pagination

Отметим, что помимо прочих, у объекта пагинации/параметров есть удобный метод getParamString который приводит переменную к нужному типу

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

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

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{
	public function checkGetParams(): void
	{
		$strCode = $this->getRequestParam('code', '');
		if (strlen($strCode) == 0) {
			$this->getComponent()->addErrorNoParams();
		}
	}

	public function get()
	{
		$obNav = $this->getPagination();
		$obNav->setParam('code', $this->getRequestParam('code', ''));

		$arCacheResult = Cache::getInstance()
			// Установка уникального ID кеша, объект $obNav сериализуется и хешируется
			->setId($obNav)
			// Установка директории хранения кеша
			->setDir(__CLASS__)
			// Добавление тега кеша для сброса, как только произойдут изменения в ИБ - кеш сбросится
			->addTag(News\Model\Section\Database::getCacheTag())
			// Вызов коллбека, возвращающий данные
			->get(fn() => $this->_getEntity($obNav));

		return $arCacheResult['entity'];
	}

	private function _getEntity(Pagination $obNav): array
	{
		//Получение массива с данными
		if ($arSection = News\Model\Section\Database::getByCode($obNav->getParamString('code', ''))) {
			return array(
				'entity' => $arSection,
			);
		}
		Cache::getInstance()->abortCache();
		return array(
			'entity' => null,
		);
	}
}

Основной запрос в БД

Составим основной запрос для получения раздела по символьному коду. Подробнее о составлении запроса.

php
<?php

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

use Bitrix\Main\DB\SqlExpression;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Fields\ExpressionField;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Database
 */
class Database
{
	public static function getByCode(string $strCode): ?array
	{
		//Подзапрос для получения количества элементов
		$obSubQueryCount = \Realweb\Api\Model\Iblock\Element\Table::query()
			->setSelect(array('ELEMENTS_COUNT'))
			->where("IBLOCK_SECTION_ID", '=', new SqlExpression('%s'))
			->where('ACTIVE', '=', 'Y')
			->registerRuntimeField(
				new ExpressionField('ELEMENTS_COUNT', 'COUNT(%s)', array('ID'))
			)
			->setTableAliasPostfix('_count');

		$obQuery = \Realweb\Api\Model\Iblock\Section\Table::queryIblock(IBLOCK_CONTENT_NEWS);
		$obQuery
			->setSelect(array("ID", "CODE", "NAME", "DESCRIPTION", "UF_BANNER"))
			->addSelect("IBLOCK.SECTION_PAGE_URL", "SECTION_PAGE_URL_TEMPLATE")
			->where("ACTIVE", true)
			->where("CODE", "=", $strCode)
			->registerDetailPicture()
			->registerSectionCodePath()
			->registerSectionNamePath()
			->registerSectionEnumField("UF_THEME")
			->registerRuntimeField(
				new ExpressionField(
					"ELEMENTS_COUNT",
					'(' . $obSubQueryCount->getQuery() . ')',
					array('ID')
				)
			)
			->addSelect('ELEMENTS_COUNT')
			;
		$rsResult = $obQuery->exec();
		if ($arSection = $rsResult->fetch()) {
			return $arSection;
		}

		return null;
	}

	public static function getCacheTag(): string
	{
		return "iblock_id_" . IBLOCK_CONTENT_NEWS;
	}
}
php
array(15)
[
    "ID"                        => string(3) "118"
    "CODE"                      => string(9) "iyul-2023"
    "NAME"                      => string(13) "Июль 2023"
    "DESCRIPTION"               => string(31) "Описание раздела"
    "UF_BANNER"                 =>  array(2)
        [
            0 => int(5041)
            1 => int(5040)
        ]
    "SECTION_PAGE_URL_TEMPLATE" => string(36) "#SITE_DIR#/news/#SECTION_CODE_PATH#/"
    "DETAIL_PICTURE_SRC"        => string(55) "/upload/iblock/1d9/sf3ah97d22v2rkon2bnln9iqb85ih008.png"
    "SECTION_CODE_PATH"         => string(4) "2023"
    "SECTION_NAME_PATH"         => string(4) "2023"
    "UF_THEME_ID"               => string(2) "10"
    "UF_THEME_USER_FIELD_ID"    => string(2) "45"
    "UF_THEME_VALUE"            => string(22) "Пресса о нас"
    "UF_THEME_DEF"              => string(1) "N"
    "UF_THEME_SORT"             => string(3) "500"
    "UF_THEME_XML_ID"           => string(32) "8e3b4fd44a61a744120cd0c299f9cdb2"
    "ELEMENTS_COUNT"            => string(1) "3"
]

Как мы видим, в результирующем массиве имеются лишние данные (UF_THEME_ID, UF_THEME_USER_FIELD_ID, UF_THEME_DEF, UF_THEME_SORT, UF_THEME_XML_ID, SECTION_CODE_PATH, SECTION_NAME_PATH, SECTION_PAGE_URL_TEMPLATE) а также отсутствуют необходимые поля - ссылки на файлы баннеров, ссылка на детальную страницу, хлебные крошки.

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

Сущность раздела \Realweb\Api\Model\Iblock\Section\Entity

Создадим в пространстве имен Section класс для сущности раздела и класс для коллекции таких сущностей. Используем уже подготовленные классы \Realweb\Api\Model\Iblock\Section\Entity и \Realweb\Api\Model\Iblock\Section\Collection - они обладают многими полезными методами.

php
<?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
{
	protected static function _getDatabase(): string
	{
		return Database::class;
	}
}
php
<?php

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

/**
 * Class \Realweb\Api\Module\News\Model\Section\Collection
 */
class Collection extends \Realweb\Api\Model\Iblock\Section\Collection
{
	/**
	 * @param string $strKey
	 * @return \Realweb\Api\Module\News\Model\Section\Entity|null
	 */
	public function getByKey($strKey): ?Entity
	{
		return parent::getByKey($strKey);
	}

	/**
	 * @return Entity[]
	 */
	public function getCollection(): array
	{
		return parent::getCollection();
	}
}

В коллекции мы объявили 2 метода getByKey и getCollection - они необходимы для того, чтобы IDE "знала" с какими сущностями мы работаем в рамках этой коллекции.

При попытке создать экземпляр класса \Realweb\Api\Module\News\Model\Section\Entity мы увидим исключение

php
$rsResult = $obQuery->exec();
if ($arSection = $rsResult->fetch()) {
    $obSection = new Entity($arSection);
    return $obSection;
}
text
[Error]
Call to undefined method Realweb\Api\Module\News\Model\Section\Database::getOrmEntity() (0)

Так происходит потому, что модель БД для раздела "не знает" с какими сущностями она работает. Добавим описание сущностей в модель и расширим ее от базового класса \Realweb\Api\Model\Iblock\Section\Database. Нам необходимо реализовать следующие обязательные методы:

  • getIblockId - получение ID ИБ для основного запроса
  • getCollection - получение класса коллекции
  • getEntity - получение класса сущности
  • getQuery - если требуется возвращение объекта основного запроса

Теперь модель БД не нуждается в методе getByCode - основной класс "умеет" получать объекты сущностей методом getObject

php
<?php

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

use Bitrix\Main\DB\SqlExpression;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Realweb\Api\Model\Iblock\Section\Query;
use Realweb\Api\Model\Main\Pagination;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Database
 */
class Database extends \Realweb\Api\Model\Iblock\Section\Database
{
	public static array $arFields = array(
		'ID',
		'IBLOCK_ID',
		'IBLOCK_SECTION_ID',
		'LEFT_MARGIN',
		'RIGHT_MARGIN',
		'DEPTH_LEVEL',
		'NAME',
		'CODE',
		'DESCRIPTION',
		'UF_BANNER'
	);
	protected const QUERY_WITH_UF = true;

	public static function getFields(): array
	{
		return static::$arFields;
	}

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

	/**
	 * @return Collection|string
	 */
	public static function getCollection(): string
	{
		return Collection::class;
	}

	/**
	 * @return Entity|string
	 */
	public static function getEntity(): string
	{
		return Entity::class;
	}

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

	public static function getQuery(?Pagination $obNav = null): Query
	{
		$obQuery = parent::getQuery($obNav)
			->addSelect('IBLOCK.SECTION_PAGE_URL', 'SECTION_PAGE_URL_TEMPLATE')
			->where("ACTIVE", true)
			->registerDetailPicture()
			->registerSectionCodePath()
			->registerSectionEnumField("UF_THEME")
		;
		$obSubQueryCount = \Realweb\Api\Model\Iblock\Element\Table::query()
			->setSelect(array('ELEMENTS_COUNT'))
			->where("IBLOCK_SECTION_ID", '=', new SqlExpression('%s'))
			->where('ACTIVE', '=', 'Y')
			->registerRuntimeField(
				new ExpressionField('ELEMENTS_COUNT', 'COUNT(%s)', array('ID'))
			)
			->setTableAliasPostfix('_count')
		;

		$obQuery
			->registerRuntimeField(
				new ExpressionField(
					"ELEMENTS_COUNT",
					'(' . $obSubQueryCount->getQuery() . ')',
					array('ID')
				)
			)
			->addSelect('ELEMENTS_COUNT')
		;

		return $obQuery;
	}
}

Условие ->where("CODE", "=", $strCode) писать не требуется, оно уже есть в базовом классе \Realweb\Api\Model\Iblock\Section\Database. Такэе обратим внимание и на константу QUERY_WITH_UF - по умолчанию она false и пользовательские поля не выбираются.

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

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

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

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{
	public function checkGetParams(): void
	{
		$strCode = $this->getRequestParam('code', '');
		if (strlen($strCode) == 0) {
			$this->getComponent()->addErrorNoParams();
		}
	}

	public function get(): ?array
	{
		$obNav = $this->getPagination();
		$obNav->setParam('code', $this->getRequestParam('code', ''));

		$arCacheResult = Cache::getInstance()
			// Установка уникального ID кеша, объект $obNav сериализуется и хешируется
			->setId($obNav)
			// Установка директории хранения кеша
			->setDir(__CLASS__)
			// Добавление тега кеша для сброса, как только произойдут изменения в ИБ - кеш сбросится
			->addTag(News\Model\Section\Database::getCacheTag())
			// Вызов коллбека, возвращающий данные
			->get(fn () => $this->_getEntity($obNav))
		;

		/** @var News\Model\Section\Entity $obSection */
		if ($obSection = $arCacheResult['entity']) {
			return $obSection->toJson();
		}

		return null;
	}

	private function _getEntity(Pagination $obNav): array
	{
		// Получение сущности раздела
		$obSection = News\Model\Section\Database::getObject($obNav);

		if ($obSection->isExist()) {
			return array(
				'entity' => $obSection,
			);
		}
		Cache::getInstance()->abortCache();

		return array(
			'entity' => null,
		);
	}
}

Можно заметить, что метод getObject всегда возвращает объект раздела, даже если он не найден в БД. Так сложилось исторически и по аналогии с методом getObjectCollection. По этой причине стоит проверка существования раздела isExist.

Реализуем метод toJson сущности раздела

php
<?php

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

/**
 * Class \Realweb\Api\Module\News\Model\Section\Entity
 *
 * @method string getDescription()
 * @method string getUfThemeValue()
 * @method int getElementsCount()
 */
class Entity extends \Realweb\Api\Model\Iblock\Section\Entity
{
	public function toJson(): array
	{
		return array(
			'id' => $this->getId(),
			'name' => $this->getName(),
			'description' => (string)$this->getDescription(),
			'image' => $this->toJsonDetailPicture(),
			'theme' => (string)$this->getUfThemeValue(),
			'link' => $this->getUrl(),
			'elementsCount' => $this->getElementsCount(),
		);
	}

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

Для доступа к некоторым нестандартным полям объявили магические методы в аннотации. Подробнее про магические методы.

json
{
  "id": 118,
  "name": "Июль 2023",
  "description": "Описание раздела",
  "image": {
    "src": "/upload/iblock/1d9/sf3ah97d22v2rkon2bnln9iqb85ih008.png",
    "alt": "Июль 2023"
  },
  "theme": "Пресса о нас",
  "link": "/news/2023/iyul-2023/",
  "elementsCount": "3"
}

Все еще не хватает хлебных крошек и баннеров.

Получение хлебных крошек

У класса \Realweb\Api\Model\Iblock\Section\Entity есть встроенные методы:

  • processChain - построение хлебных крошек
  • getChain - получение коллекции родительских разделов, без предварительного вызова processChain вернет пустую коллекцию

"Посчитаем" хлебные крошки и выведем их в итоговый json.

Для получения всего "пути" хлебных крошек также понадобится и объект Инфоблока. Получим его с помощью встроенного в ядро модуля Iblock

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

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

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{
	public function checkGetParams(): void
	{
            ...
	}

	public function get(): ?array
	{
            ...
	}

	private function _getEntity(Pagination $obNav): array
	{
		// Получение сущности раздела
		$obSection = News\Model\Section\Database::getObject($obNav);

		if ($obSection->isExist()) {
		  //Считаем хлебные крошки
			$obSection->processChain();
			return array(
				'entity' => $obSection,
			);
		}
		Cache::getInstance()->abortCache();

		return array(
			'entity' => null,
		);
	}
}
php
<?php

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

use Realweb\Api\Module\Iblock;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Entity
 *
 * @method string getDescription()
 * @method string getUfThemeValue()
 * @method int getElementsCount()
 */
class Entity extends \Realweb\Api\Model\Iblock\Section\Entity
{
	public function toJson(): array
	{
		return array(
			'id' => $this->getId(),
			'name' => $this->getName(),
			'description' => (string)$this->getDescription(),
			'image' => $this->toJsonDetailPicture(),
			'theme' => (string)$this->getUfThemeValue(),
			'link' => $this->getUrl(),
			'elementsCount' => $this->getElementsCount(),
			'breadcrumbs' => $this->toJsonBreadcrumbs(),
		);
	}

	public function toJsonBreadcrumbs(): array
	{
		$arResult = array();
		$arResult[] = array(
			'name' => 'Главная',
			'link' => SITE_DIR,
		);

		// Получение объекта инфоблока для разводящей страницы
		if ($obIblock = (new Iblock\Controller\IndexController())->setParam('id', $this->getIblockId())->getEntity()) {
			$arResult[] = array(
				'name' => $obIblock->getName(),
				'link' => $obIblock->getUrl(),
			);
		}
		// Цепочка родительских разделов
		foreach ($this->getChain()->getCollection() as $obSection) {
			$arResult[] = array(
				'name' => $obSection->getName(),
				'link' => $obSection->getUrl(),
			);
		}

		// Сам раздел
		$arResult[] = array(
			'name' => $this->getName(),
			'link' => $this->getUrl(),
		);

		return $arResult;
	}

	protected static function _getDatabase(): string
	{
		return Database::class;
	}
}
json
{
  "id": 118,
  "name": "Июль 2023",
  "description": "Описание раздела",
  "image": {
    "src": "/upload/iblock/1d9/sf3ah97d22v2rkon2bnln9iqb85ih008.png",
    "alt": "Июль 2023"
  },
  "theme": "Пресса о нас",
  "link": "/news/2023/iyul-2023/",
  "elementsCount": "3",
  "breadcrumbs": [
    {
      "name": "Главная",
      "link": "/"
    },
    {
      "name": "Новости",
      "link": "/news/"
    },
    {
      "name": "2023",
      "link": "/news/2023/"
    },
    {
      "name": "Июль 2023",
      "link": "/news/2023/iyul-2023/"
    }
  ]
}

Получение множественных свойств типа "Привязка к элементу"

Для получения коллекции привязанных элементов необходимо предварительно реализовать модуль баннеров. Пример реализации такого модуля

Пусть такой модуль реализован в пространстве Catalog\Banner

php
<?php

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

use Realweb\Api\Module\Iblock;
use Realweb\Api\Module\Catalog;

/**
 * Class \Realweb\Api\Module\News\Model\Section\Entity
 *
 * @method string getDescription()
 * @method string getUfThemeValue()
 * @method int getElementsCount()
 * @method array getUfBanner()
 */
class Entity extends \Realweb\Api\Model\Iblock\Section\Entity
{
	private ?Catalog\Model\Banner\Collection $_banners = null;

	public function toJson(): array
	{

		return array(
			'id' => $this->getId(),
			'name' => $this->getName(),
			'description' => (string)$this->getDescription(),
			'image' => $this->toJsonDetailPicture(),
			'theme' => (string)$this->getUfThemeValue(),
			'link' => $this->getUrl(),
			'elementsCount' => $this->getElementsCount(),
			'breadcrumbs' => $this->toJsonBreadcrumbs(),
			'banners' => $this->getBanners()->toJson(),
		);
	}

	public function toJsonBreadcrumbs(): array
	{
		$arResult = array();
		$arResult[] = array(
			'name' => 'Главная',
			'link' => SITE_DIR,
		);

		// Получение объекта инфоблока для разводящей страницы
		if ($obIblock = $this->getIblock()) {
			$arResult[] = array(
				'name' => $obIblock->getName(),
				'link' => $obIblock->getUrl(),
			);
		}
		// Цепочка родительских разделов
		foreach ($this->getChain()->getCollection() as $obSection) {
			$arResult[] = array(
				'name' => $obSection->getName(),
				'link' => $obSection->getUrl(),
			);
		}

		// Сам раздел
		$arResult[] = array(
			'name' => $this->getName(),
			'link' => $this->getUrl(),
		);

		return $arResult;
	}

	public function getBanners(): Catalog\Model\Banner\Collection
	{
		if ($this->_banners === null) {
			$arBannerIds = $this->getUfBanner();
			if (is_array($arBannerIds) && count($arBannerIds) > 0) {
				$this->_banners = (new Catalog\Controller\Backend\Banner\CollectionController())
					->setParam('ids', $arBannerIds)
					->getCollection();
			} else {
				$this->_banners = new Catalog\Model\Banner\Collection();
			}
		}

		return $this->_banners;
	}

	protected static function _getDatabase(): string
	{
		return Database::class;
	}
}
json
{
  "id": 118,
  "name": "Июль 2023",
  "description": "Описание раздела",
  "image": {
    "src": "/upload/iblock/1d9/sf3ah97d22v2rkon2bnln9iqb85ih008.png",
    "alt": "Июль 2023"
  },
  "theme": "Пресса о нас",
  "link": "/news/2023/iyul-2023/",
  "elementsCount": "3",
  "breadcrumbs": [
    {
      "name": "Главная",
      "link": "/"
    },
    {
      "name": "Новости",
      "link": "/news/"
    },
    {
      "name": "2023",
      "link": "/news/2023/"
    },
    {
      "name": "Июль 2023",
      "link": "/news/2023/iyul-2023/"
    }
  ],
  "banners": [
    {
      "title": "Наращиваем волосы в нашей студии",
      "link": {
        "link": "/uslugi/narashchivanie-volos/kapsulnoe-narashchivanie/",
        "label": "Узнать подробнее"
      },
      "image": {
        "src": "/upload/iblock/15f/frvw1169vxcy8egvsj6ww5nfltj0vklh.png",
        "alt": "Наращиваем волосы в нашей студии"
      }
    },
    {
      "title": "2",
      "link": {
        "link": "/test/",
        "label": "test"
      },
      "image": {
        "src": "/upload/iblock/33c/mbsm0j8qplp0kszwd76v80zvi6zakt9l.png",
        "alt": "2"
      }
    }
  ]
}

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

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

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

use Realweb\Api\Model\Main\Cache;
use Realweb\Api\Model\Main\Pagination;
use Realweb\Api\Module\News;
use Realweb\Api\Module\Catalog;

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{
	public function checkGetParams(): void
	{
		...
	}

	public function get(): ?array
	{
		$obNav = $this->getPagination();
		$obNav->setParam('code', $this->getRequestParam('code', ''));

		$arCacheResult = Cache::getInstance()
			// Установка уникального ID кеша, объект $obNav сериализуется и хешируется
			->setId($obNav)
			// Установка директории хранения кеша
			->setDir(__CLASS__)
			// Добавление тега кеша для сброса, как только произойдут изменения в ИБ - кеш сбросится
			->addTag(News\Model\Section\Database::getCacheTag())
			->addTag(Catalog\Model\Banner\Database::getCacheTag())
			// Вызов коллбека, возвращающий данные
			->get(fn () => $this->_getEntity($obNav))
		;

		/** @var News\Model\Section\Entity $obSection */
		if ($obSection = $arCacheResult['entity']) {
			return $obSection->toJson();
		}

		return null;
	}

        private function _getEntity(Pagination $obNav): array
	{
		// Получение сущности раздела
		$obSection = News\Model\Section\Database::getObject($obNav);

		if ($obSection->isExist()) {
			$obSection->processChain();
			$obSection->getBanners(); //Получение баннеров внутри кеша
			return array(
				'entity' => $obSection,
			);
		}
		Cache::getInstance()->abortCache();

		return array(
			'entity' => null,
		);
	}
}

Получение метатегов

Для получения метатегов существует метод сущности раздела processMetaValues

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

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

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends \Realweb\Api\Controller
{
	public function checkGetParams(): void
	{
            ...
	}

	public function get(): ?array
	{
            ...
	}

	private function _getEntity(Pagination $obNav): array
	{
		// Получение сущности раздела
		$obSection = News\Model\Section\Database::getObject($obNav);

		if ($obSection->isExist()) {
		        //Считаем хлебные крошки
			$obSection->processChain();
			//Получаем метатеги
			$obSection->processMetaValues();
			return array(
				'entity' => $obSection,
			);
		}
		Cache::getInstance()->abortCache();

		return array(
			'entity' => null,
		);
	}
}
php
<?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 toJsonDetail(): array
	{
		return array(
			'id' => $this->getId(),
			'name' => $this->getName(),
			'description' => (string)$this->getDescription(),
			'image' => $this->toJsonDetailPicture(),
			'theme' => (string)$this->getUfThemeValue(),
			'link' => $this->getUrl(),
			'elementsCount' => $this->getElementsCount(),
			'breadcrumbs' => $this->toJsonBreadcrumbs(),
			'banners' => $this->getBanners()->toJson(),
			'meta' => $this->getMetaValues()->toJsonMeta(),
		);
	}

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

Внимание! Если не выполнить метод processMetaValues - $this->getMetaValues() вернет пустую коллекцию.

Объединение контроллеров

В рамках одного проекта потребуется выводить разные данные - детальные страницы элементов и разделов разных ИБ. По сути функциональность большинства контроллеров сводится к следующему:

  1. Создать объект Пагинации/Фильтра и установить параметры из запроса (ID или CODE)
  2. Вызвать метод получения коллекции соответствующей модели
  3. Опционально провести некоторые манипуляции с коллекцией
  4. Получить первый элемент коллекции (Т.к. параметр ID или CODE уникальны)
  5. Закешировать результат
  6. Отдать результат в браузер методом toJsonDetail (Метод toJson зарезервирован для списков)

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

  • Каждый контроллер имеет свою модель
  • Каждый контроллер имеет свою обработку и установку входящих параметров
  • Каждый контроллер может иметь свои манипуляции с коллекцией
  • Каждый контроллер может иметь свои манипуляции с сущностью
php
<?php

namespace Realweb\Api\Controller;

use Realweb\Api\Controller;
use Realweb\Api\Model\Data\Collection;
use Realweb\Api\Model\Data\Entity;
use Realweb\Api\Model\Main\Cache;
use Realweb\Api\Model\Main\Pagination;

/**
 * Class \Realweb\Api\Module\Controller\EntityController
 */
abstract class EntityController extends Controller
{
	/**
	 * Для каждого контроллера будет своя модель
	 * @return \Realweb\Api\Model\Data\Database
	 */
	abstract protected function _getDatabase(): string;

	// Объект сущности для всех общий, удобно получать данные на бекенде
	protected ?Entity $_entity = null;

	// Метод получения данных для бекенда
	public function getEntity(): ?Entity
	{
		if ($this->_entity === null) {
			$this->setReturnJsonData(false);
			$this->get();
		}

		return $this->_entity;
	}

	// Основной метод получения данных общий для всех
	public function get(): ?array
	{
		$this->_initParams();
		$obCache = Cache::getInstance()
			->setId($this->getPagination())
			// Время кеширования может отличаться
			->setTime($this->_getCacheTime())
			->setDir(__CLASS__)
		;
		// Теги для сброса кеша разные у всех, по умолчанию тег инфоблока, но можно и переопределить, если требуется
		if ($mTag = $this->_getCacheTag()) {
			if (is_array($mTag)) {
				foreach ($mTag as $strTag) {
					$obCache->addTag($strTag);
				}
			} else {
				$obCache->addTag($mTag);
			}
		}

		// Получение закешированного результата
		$arCacheResult = $obCache->get(fn () => $this->_getEntity($this->getPagination()));
		if (array_key_exists('entity', $arCacheResult)) {
			$obEntity = $arCacheResult['entity'];
			$this->_setEntity($obEntity);

			// Если требуется отдать данные в браузер - отдаем
			if ($this->isReturnJsonData()) {
				return $obEntity->toJsonDetail();
			}
		} else {
			$this->getComponent()->addErrorNoEntity();
		}

		return null;
	}

	// Основной запрос данных из модели
	protected function _getEntity(Pagination $obNav): array
	{
		$obCollection = $this->_getDatabase()::getObjectCollection($obNav);

		// Если нужны какие-то манипуляции с коллекцией - делаем их
		$this->_processCollection($obCollection);
		if ($obEntity = $obCollection->current()) {

			// Если нужны какие-то манипуляции с сущностью - делаем их
			$this->_processEntity($obEntity);

			return array(
				'entity' => $obEntity,
			);
		}

		return array();

	}

	// Установка сущности в экземпляр класса контроллера
	protected function _setEntity(Entity $obEntity): void
	{
		$this->_entity = $obEntity;
	}

	// Инициализация параметров, можно переопределить в контроллере проекта
	protected function _initParams(): void
	{
		$this->getPagination();
	}

	protected function _getCacheTag(): array | string | null
	{
		return $this->_getDatabase()::getCacheTag();
	}

	protected function _getCacheTime(): int
	{
		return Cache::DURATION;
	}

	protected function _processCollection(Collection $obCollection): void
	{
	}

	protected function _processEntity(Entity $obEntity): void
	{
	}
}

Таким образом контроллер получения раздела принимает вид:

php
<?php

namespace Realweb\Api\Module\News\Controller\Section;

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

/**
 * Class \Realweb\Api\Module\News\Controller\Section\EntityController
 */
class EntityController extends Controller\EntityController
{
	public function checkGetParams(): bool
	{
		$strCode = $this->getRequestParam('code', $this->getParamString('code',''));
		if (strlen($strCode) == 0) {
			$this->getComponent()->addErrorNoParams();

			return false;
		}

		return true;
	}

	/**
	 * @return Section\Entity
	 */
	public function getEntity(): ?\Realweb\Api\Model\Data\Entity
	{
		return parent::getEntity();
	}

	protected function _getDataBase(): string
	{
		return Section\Database::class;
	}

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

	/**
	 * @param Section\Entity $obEntity
	 */
	protected function _processEntity(\Realweb\Api\Model\Data\Entity $obEntity): void
	{
		$obEntity->processChain();
	}
}

Контроллер CollectionController уже создан в ядре realweb.api

Метод getEntity необходим для получения раздела на бекенде, например

php
use Realweb\Api\Module\News;
$obSection = (new News\Controller\Section\EntityController())->setParam('code','iyul-2023')->getEntity();
//Теперь можно работать с этим разделом

Пример кода

Пример кода можно найти по ссылке