Я веб-разработчик и так сложилось, что я работаю именно на Битриксе. Свое нытье и недовольство в адрес этой CMS я опущу, т.к. об этом уже написано достаточно. Здесь я хочу поделиться решением одной проблемы, которую встретил на своем пути, работая с сущностями в Битриксе, а именно с неуниверсальностью кода.
Объясню в чем суть. В битриксе на каждую сущность (элемент инфоблока, раздел, заказ, свойство заказа и тп) есть свой класс (CIBlockElement, CIBlockSection, CSaleOrder, CSaleOrderProp и тп). Так вот эти классы не имеют общего предка, строго регламентированных методов с одинаковыми параметрами, хотя все они являются однотипными классами, которые имееют список общих методов (выборка, добавление, удаление и тп). Каждый из этих классов живет сам по себе и из-за этого возникают неудобства.
Например, самый распространенный метод GetList хоть и выполняет одну и ту же задачу, но его параметры могут отличаться у некоторых классов, иногда просто изменен их порядок. Еще интересен пример с методом GetByID где-то он возвращает массив, а где-то объект CDBResult, поэтому поведение методов неочевидное и часто тратится время на проверку.
Знаний и опыта не хватало (до этого 1.5 года был джуниором на Java), но было ясное понимание, что так нельзя, поэтому я потихоньку начал писать свои классы-обертки для каждой сущности с общей структурой, наследованием и абстракцией. Я их несколько раз переписывал и в итоге у меня получилось 4 подмодуля:
- dbmanager — слой-обертка над стандартными классами сущностей (CIBlockElement, CIBlockSection и тп). Необходим для изоляция классов битрикса от нашего кода.
IDBManager
interface IDBManager { /** @return BaseDBResult */ public function select($params = []); public function add($fields); public function update($id, $fields); public function delete($id); public function getPrimaryFieldName(); public function getSlugFieldName(); }
Базовый DBManagernamespace OdEntityDBManager; use OdEntityDBResultOldDBResult; abstract class OldDBManager implements IDBManager { protected $oldEntityInstance; public function __construct() { $this->oldEntityInstance = $this->getOldClassInstance(); } public function select($params = []) { $bxParams = $this->makeBXParams($params); $dbRes = $this->getBXDBResult($bxParams); return $this->convertDBResult($dbRes, $params); } public function add($fields) { $instance = $this->oldEntityInstance; return $instance && method_exists($instance, 'Add') ? $instance->Add($fields) : false; } public function update($id, $fields) { $instance = $this->oldEntityInstance; return $instance && method_exists($instance, 'Update') ? $instance->Update($id, $fields) : false; } public function delete($id) { $instance = $this->oldEntityInstance; return $instance && method_exists($instance, 'Delete') ? $instance->Delete($id) : false; } public function convertDBResult($bxDBResult, $params) { return $bxDBResult ? new OldDBResult($bxDBResult, $params) : null; } public function makeBXParams($params = []) { return [ 'filter' => array_change_key_case((array)$params['filter'], CASE_UPPER), 'select' => array_map('strtoupper', (array)$params['select']), 'order' => (array)$params['order'], 'group' => $params['group'] ? $params['group'] : false, 'nav_params' => $params['limit'] ? ['nTopCount' => $params['limit']] : false ]; } public function getPrimaryFieldName() { return 'id'; } public function getSlugFieldName() { return 'code'; } public function getDateFieldNames() { return []; } /** @return CAllDBResult */ abstract protected function getBXDBResult($bxParams = []); abstract protected function getOldClassInstance(); }
- itemfinder — классы отвечающие за выборку сущностей. Заранее прошу прощение за неканоническое именование методов, в силу своей тяги к минимализму решил убрать приставку «find» в названиях.
IItemFinder
interface IItemFinder { public function item($filter = [], $fields = [], $orderBy = []); public function items($filter = [], $fields = [], $orderBy = [], $extendedParams = []); public function map($filter = [], $fields = [], $orderBy = [], $extendedParams = []); public function limited($limit, $filter = [], $fields = [], $orderBy = [], $offset = null); public function grouped($groupBy, $filter = [], $orderBy = [], $limit = null); public function exists($filter = []); public function count($filter = []); public function id($filter = [], $orderBy = []); public function ids($filter = [], $orderBy = [], $limit = null); /** @return BaseDBResult */ public function select($filter = [], $fields = [], $orderBy = [], $extendedParams = []); }
Базовый ItemFindernamespace OdEntityFinder; use OdEntityDBManagerIDBManager; use OdEntityDBResultBaseDBResult; use OdEntityUtilsArrayUtils; use OdEntityUtilsDateUtils; class ItemFinder implements IItemFinder { protected $dbManager; protected $idFieldName; protected $slugFieldName; protected $dateFieldNames; protected $lowercaseFields = true; private $cacheEnabled = false; private $defaultParams = []; private static $_cache = []; public function __construct(IDBManager $dbManager) { $this->dbManager = $dbManager; $this->idFieldName = $dbManager->getPrimaryFieldName(); $this->slugFieldName = $dbManager->getSlugFieldName(); $this->dateFieldNames = $dbManager->getDateFieldNames(); } public function item($filter = [], $fields = [], $orderBy = [], $offset = null) { $items = $this->limited(1, $filter, $fields, $orderBy, $offset); return is_array($items) && count($items) > 0 ? array_shift($items) : []; } public function items($filter = [], $fields = [], $orderBy = [], $extendedParams = []) { return $this->selectAsArray($filter, $fields, $orderBy, $extendedParams); } public function map($filter = [], $fields = [], $orderBy = [], $extendedParams = []) { if (!empty($fields)) { $fields = (array)$fields; $fields[] = $this->idFieldName; } $items = $this->items($filter, $fields, $orderBy, $extendedParams); $map = []; foreach ($items as $item) { $map[$item[$this->idFieldName]] = $item; } return $map; } public function limited($limit, $filter = [], $fields = [], $orderBy = [], $offset = null) { return $this->selectAsArray($filter, $fields, $orderBy, ['limit' => $limit, 'offset' => $offset]); } public function grouped($groupBy, $filter = [], $orderBy = [], $limit = null) { return $this->selectAsArray($filter, $groupBy, $orderBy, ['group' => $groupBy, 'limit' => $limit]); } public function exists($filter = []) { return $this->count($filter) > 0; } public function count($filter = []) { return count($this->ids($filter)); } public function id($filter = [], $orderBy = []) { $item = $this->item($filter, [$this->idFieldName], $orderBy); return $item ? $item[$this->idFieldName] : null; } public function ids($filter = [], $orderBy = [], $limit = null) { $items = $this->selectAsArray($filter, [$this->idFieldName], $orderBy, ['limit' => $limit]); $ids = $this->_mapIds($items); return array_map('intval', $ids); } public function select($filter = [], $fields = [], $orderBy = [], $extendedParams = []) { $params = $this->makeParams($filter, $fields, $orderBy, $extendedParams); $this->modifyParams($params); if (!$this->validateParams($params)) { return new BaseDBResult(); } $dbRes = $this->dbManager->select($params); $dbRes->setLowercaseKeys($this->lowercaseFields); if (is_array($params['select']) && ArrayUtils::isAssoc($params['select'])) { $dbRes->setAliasMap($params['select']); } return $dbRes; } /* ------------ internal ------------ */ protected function selectAsArray($filter = [], $fields = [], $orderBy = [], $extendedParams = []) { $cacheKey = serialize(func_get_args()); $cacheRes = $this->_cacheDbRes($cacheKey); if (!is_null($cacheRes)) { return $cacheRes; } $items = $this->select($filter, $fields, $orderBy, $extendedParams)->fetchAll(); $this->_cacheDbRes($cacheKey, $items); return $items; } protected function modifyParams(&$params) { $filter = $params['filter']; $isFilterByPrimary = !empty($filter) && !empty($filter[$this->idFieldName]); if ($isFilterByPrimary) { $limit = is_array($filter[$this->idFieldName]) ? count($filter[$this->idFieldName]) : 1; $params['limit'] = min($limit, $params['limit']); } } private function makeParams($filter = [], $fields = [], $orderBy = [], $extendedParams = []) { $params = array_filter([ 'filter' => $filter, 'select' => $fields, 'order' => $orderBy, ]); $params += (array)$extendedParams; $params['filter'] = $this->makeFilter($params['filter']); $params['select'] = $this->makeSelect($params['select']); $params['order'] = $this->makeOrder($params['order']); if (!empty($this->defaultParams['filter'])) { $params['filter'] = (array)$params['filter'] + $this->defaultParams['filter']; } if (!empty($this->defaultParams['select'])) { $params['select'] = array_merge($this->defaultParams['select'], (array)$params['select']); } if (!empty($this->defaultParams['order'])) { $params['order'] = array_merge($this->defaultParams['order'], (array)$params['order']); } return $params; } final protected function makeFilter($filter = null) { if (empty($filter)) { return $filter; } if ($this->idFieldName) { if (is_numeric($filter) && $filter > 0) { $filter = [$this->idFieldName => $filter]; } elseif (is_array($filter) && !ArrayUtils::isAssoc($filter)) { $ids = array_filter($filter, 'is_numeric'); if (count($ids) === count($filter)) { $filter = [$this->idFieldName => $filter]; } } } // if its symbolic string if ($this->slugFieldName && is_string($filter)) { $filter = [$this->slugFieldName => $filter]; } if (is_array($filter) && !empty($filter)) { foreach ($filter as $field => $value) { $fieldName = str_replace(['>', '<', '>=', '<=', '><', '!><', '=', '%', '?'], '', $field); if (in_array($fieldName, $this->dateFieldNames) && $value) { $filter[$field] = DateUtils::toFilterDate($value); } } } return $filter; } final protected function makeSelect($select = null) { return (array)$select; } final protected function makeOrder($order = null) { if (is_string($order) && strlen($order) > 0) { $order = [$order => 'asc']; } return (array)$order; } protected function validateParams($params = []) { return true; } private function _cacheDbRes($params, $value = null) { $cacheId = md5(serialize($params)); if (isset($value)) { if ($this->cacheEnabled) { self::$_cache[$cacheId] = $value; } return $this->cacheEnabled; } if ($res = self::$_cache[$cacheId]) { return $res; } return null; } /* ------------ settings ------------ */ public function setDefaultParamValue($paramName, $value) { $this->defaultParams[$paramName] = $value; } public function addDefaultParamValue($paramName, $value) { if (!$this->defaultParams[$paramName]) { $this->defaultParams[$paramName] = []; } $this->defaultParams[$paramName] = array_merge($this->defaultParams[$paramName], (array)$value); } public function setCacheEnabled($value) { $this->cacheEnabled = !!$value; } public function setLowercaseFields($value) { $this->lowercaseFields = $value; } /* ------------ utils ------------ */ protected function _mapIds($array) { return ArrayUtils::mapField($array, $this->idFieldName); } }
- datamanager — классы отвечающие за редактирование сущностей(обновление, удаление, создание)
IDataManager
interface IDataManager { public function add(array $fields); public function addItems(array $fieldsList); public function addOrUpdate($filter, array $fields, $fieldsToUpdate = []); public function addUnique($filter, array $fields); public function updateById($id, array $fields); public function updateItem($filter, array $fields); public function updateItems($filter, array $fields); public function deleteById($id); public function deleteItem($filter); public function deleteItems($filter); }
Базовый DataManagernamespace OdEntityDataManager; use OdEntityDBManagerIDBManager; use OdEntityFinderIItemFinder; class DataManager implements IDataManager { /** @var IDBManager */ protected $dbManager; /** @var IItemFinder */ protected $finder; protected $primaryFieldName; public function __construct(IDBManager $dbManager, IItemFinder $finder = null) { $this->finder = $finder; $this->dbManager = $dbManager; $this->primaryFieldName = $this->dbManager->getPrimaryFieldName(); } /* ------------ ADD ------------ */ public function add(array $fields) { return $this->dbManager->add($fields); } public function addItems(array $fieldsList = []) { $addedItemsIds = []; foreach ($fieldsList as $fields) { $addedItemsIds[] = $this->add($fields); } return $addedItemsIds; } public function addOrUpdate($filter, array $fields, $fieldsToUpdate = []) { if (!$this->finder) { return false; } if (!$id = $this->finder->id($filter)) { return $this->add($fields); } if (empty($fieldsToUpdate)) { $fieldsToUpdate = $fields; } return $this->updateById($id, $fieldsToUpdate); } public function addUnique($filter, array $fields) { if (!$this->finder) { return false; } if (!$id = $this->finder->id($filter)) { return $this->add($fields); } return $id; } /* ------------ UPDATE ------------ */ public function updateById($id, array $fields) { $res = $this->dbManager->update($id, $fields); return $res ? $id : false; } public function updateItems($filter, array $fields) { if (!$this->finder) { return false; } $ids = $this->finder->ids($filter); $updatedIds = []; foreach ($ids as $id) { $updatedIds[] = $this->updateItem($id, $fields); } return array_filter($updatedIds); } public function updateItem($filter, array $fields) { if (!$this->finder) { return false; } $id = $this->finder->Id($filter); return $id ? $this->updateById($id, $fields) : false; } /* ------------ DELETE ------------ */ public function deleteById($id) { return $this->dbManager->delete($id); } public function deleteItem($filter) { if (!$this->finder) { return false; } $id = $this->finder->id($filter); return $this->deleteById($id); } public function deleteItems($filter) { if (!$this->finder) { return false; } $ids = $this->finder->ids($filter); $deletedIds = []; foreach ($ids as $id) { $deletedIds[] = $this->deleteById($id); } return array_filter($deletedIds); } }
- itemmanager — это слой-обертка над itemmanger и datamanager для более удобного использования (не уверен, что это правильно).
Все это оформлено в виде битрикс-модуля. В каждом подмодуле должен быть класс для каждой сущности.
Самый полезный из подмодулей — это itemmanager. Этот дополнительный слой позволил добавить дополнительную логику работы с выборкой, доп. параметры, предобработки, постобобработки. Например, в качестве фильтра можно задавать просто ID, массив из ID, символьный код; дату в фильтре можно указывать в стандартном формате даты и времени; возможно кеширование результатов и тд.
Я пока не использовал в этом коде D7 классы сущностей, т.к. логика работы с D7 и старыми классами иногда сильно отличается и не вся старая реализация перенесена на D7, при желании старые классы можно заменить на D7 аналоги.
Примеры использования:
Пример 1. Самая распространенная задача — получить элемент инфоблока по его символьному коду или ID:
до:
$dbRes = CIBlockElement::GetList([], ['CODE' => 'element_code']);
$elem = $dbRes->Fetch();
после:
$elem = IBElement::find('element_code');
Пример 2. Создать раздел инфоблока с кодом 'section_code' или обновить, если он уже существует:
до:
$fields = ['CODE' => 'section_code', 'NAME' => '...', 'SECTION_ID' => '...', ...];
$dbRes = CIBlockSection::GetList([], ['CODE' => 'section_code']);
if ($section = $dbRes->Fetch()) {
CIBlockSection::Update($section['ID'], $fields);
} else {
CIBlockSection::Add($fields);
}
после:
$fields = ['CODE' => 'section_code', 'NAME' => '...', 'SECTION_ID' => '...', ...];
IBSection::addOrUpdate('section_code', $fields);
Пример 3. Найти элементы инфоблока созданные за последнюю неделю и получить список наименований их главных разделов.
до:
$dateCreate = date($DB->DateFormatToPHP(CLang::GetDateFormat("SHORT")), strtotime('week ago'));
$dbRes = CIBlockElement::GetList(
[],
['IBLOCK_ID' => '5', '>DATE_CREATE' => $dateCreate],
['IBLOCK_SECTION_ID']
);
$sectionIds = [];
while($arFields = $dbRes->GetNext()) {
$sectionIds[] = $arFields['IBLOCK_SECTION_ID'];
}
$sectDbRes = CIBlockSection::GetList([], ['ID' => $sectionIds]);
$parentNames = [];
while($arRes = $sectDbRes->Fetch()) {
$parentDbRes = CIBlockSection::GetList(
["SORT"=>"ASC"],
["IBLOCK_ID"=>$arRes["IBLOCK_ID"], "<=LEFT_BORDER" => $arRes["LEFT_MARGIN"], ">=RIGHT_BORDER" => $arRes["RIGHT_MARGIN"], "DEPTH_LEVEL" => 1],
false
);
if ($parent = $parentDbRes->GetNext()) {
$parentNames[] = $parent['NAME'];
}
}
var_dump($parentNames);
после:
$products = IBElement::findItems(
['iblock_id' => 5, '>=date_create' => 'week ago'],
['iblock_section_id']
);
$sectionIds = ArrayUtils::mapField($products, 'iblock_section_id');
$parents = IBSection::getFinder()->mainParents($sectionIds, [], ['name']);
$parentNames = ArrayUtils::mapField($parents, 'name');
var_dump ($parentNames);
Думаю, новичкам в битриксе этот опыт будет полезен. Код лежит здесь. Я недавно это все переписывал, поэтому там пока только реализация для элементов и разделов инфоблока. В ближайшее время постараюсь перенести остальное.
Автор: рекрут