Pull to refresh

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

Я веб-разработчик и так сложилось, что я работаю именно на Битриксе. Свое нытье и недовольство в адрес этой 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 Od\Entity\DBManager;
    
    use Od\Entity\DBResult\OldDBResult;
    
    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 Od\Entity\Finder;
    
    use Od\Entity\DBManager\IDBManager;
    use Od\Entity\DBResult\BaseDBResult;
    use Od\Entity\Utils\ArrayUtils;
    use Od\Entity\Utils\DateUtils;
    
    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 Od\Entity\DataManager;
    
    use Od\Entity\DBManager\IDBManager;
    use Od\Entity\Finder\IItemFinder;
    
    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);


Думаю, новичкам в битриксе этот опыт будет полезен. Код лежит здесь. Я недавно это все переписывал, поэтому там пока только реализация для элементов и разделов инфоблока. В ближайшее время постараюсь перенести остальное.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.