Универсализация классов сущностей в CMS 1C-Bitrix

в 12:04, , рубрики: 1С-Битрикс, php, метки: , ,

Я веб-разработчик и так сложилось, что я работаю именно на Битриксе. Свое нытье и недовольство в адрес этой CMS я опущу, т.к. об этом уже написано достаточно. Здесь я хочу поделиться решением одной проблемы, которую встретил на своем пути, работая с сущностями в Битриксе, а именно с неуниверсальностью кода.

image

Объясню в чем суть. В битриксе на каждую сущность (элемент инфоблока, раздел, заказ, свойство заказа и тп) есть свой класс (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();
    }
    

    Базовый DBManager

    namespace 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 = []);
    }
    

    Базовый ItemFinder

    namespace 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);
    }
    

    Базовый DataManager

    namespace 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);

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

Автор: рекрут

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js