- PVSM.RU - https://www.pvsm.ru -
Я веб-разработчик и так сложилось, что я работаю именно на Битриксе. Свое нытье и недовольство в адрес этой CMS я опущу, т.к. об этом уже написано достаточно. Здесь я хочу поделиться решением одной проблемы, которую встретил на своем пути, работая с сущностями в Битриксе, а именно с неуниверсальностью кода.
Объясню в чем суть. В битриксе на каждую сущность (элемент инфоблока, раздел, заказ, свойство заказа и тп) есть свой класс (CIBlockElement, CIBlockSection, CSaleOrder, CSaleOrderProp и тп). Так вот эти классы не имеют общего предка, строго регламентированных методов с одинаковыми параметрами, хотя все они являются однотипными классами, которые имееют список общих методов (выборка, добавление, удаление и тп). Каждый из этих классов живет сам по себе и из-за этого возникают неудобства.
Например, самый распространенный метод GetList хоть и выполняет одну и ту же задачу, но его параметры могут отличаться у некоторых классов, иногда просто изменен их порядок. Еще интересен пример с методом GetByID где-то он возвращает массив, а где-то объект CDBResult, поэтому поведение методов неочевидное и часто тратится время на проверку.
Знаний и опыта не хватало (до этого 1.5 года был джуниором на Java), но было ясное понимание, что так нельзя, поэтому я потихоньку начал писать свои классы-обертки для каждой сущности с общей структурой, наследованием и абстракцией. Я их несколько раз переписывал и в итоге у меня получилось 4 подмодуля:
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();
}
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();
}
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 = []);
}
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);
}
}
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);
}
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. Этот дополнительный слой позволил добавить дополнительную логику работы с выборкой, доп. параметры, предобработки, постобобработки. Например, в качестве фильтра можно задавать просто 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);
Думаю, новичкам в битриксе этот опыт будет полезен. Код лежит здесь [1]. Я недавно это все переписывал, поэтому там пока только реализация для элементов и разделов инфоблока. В ближайшее время постараюсь перенести остальное.
Автор: рекрут
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/249839
Ссылки в тексте:
[1] здесь: https://github.com/eax0/od.entity
[2] Источник: http://habrahabr.ru/sandbox/108046/
Нажмите здесь для печати.