На каждую сущность программируется свой GetList, Update, Add, Delete. В основном копи-пастом. Недостатки: разный набор параметров; разный синтаксис полей фильтров; события могут быть или не быть; иногда разный код под разные БД (Add).
Какая цель поставлена в новом ядре
Сделать операции выборки и сохранения в БД однотипными, с одинаковыми параметрами и фильтрами. По возможности таблицы сущностей должны обслуживаться с минимумом нового кода. Стандартные события добавления/изменения/удаления должны быть доступны автоматически.[spoiler]
Реализация
Введены три понятия:
- сущности (Bitrix\Main\Entity\Base)
- поля сущностей (Bitrix\Main\Entity\Field и его наследники)
- датаменеджер (Bitrix\Main\Entity\DataManager)
Сущность описывает таблицу в БД, в т.ч. содержит поля сущностей. Датаменеджер производит операции выборки и изменения сущности. На практике работа в основном ведется на уровне датаменеджера.
Пример
Начнем с простого примера использования датаменеджера для своей сущности:
use Bitrix\Main\Entity; class CultureTable extends Entity\DataManager { const LEFT_TO_RIGHT = 'Y'; const RIGHT_TO_LEFT = 'N'; public static function getFilePath() { return __FILE__; } public static function getTableName() { return 'b_culture'; } public static function getMap() { return array( 'ID' => array( 'data_type' => 'integer', 'primary' => true, 'autocomplete' => true, ), 'CODE' => array( 'data_type' => 'string', ), 'NAME' => array( 'data_type' => 'string', 'required' => true, 'title' => Loc::getMessage("culture_entity_name"), ), 'FORMAT_DATE' => array( 'data_type' => 'string', 'required' => true, 'title' => Loc::getMessage("culture_entity_date_format"), ), 'FORMAT_DATETIME' => array( 'data_type' => 'string', 'required' => true, 'title' => Loc::getMessage("culture_entity_datetime_format"), ), 'FORMAT_NAME' => array( 'data_type' => 'string', 'required' => true, 'title' => Loc::getMessage("culture_entity_name_format"), ), 'WEEK_START' => array( 'data_type' => 'integer', ), 'CHARSET' => array( 'data_type' => 'string', 'required' => true, 'title' => Loc::getMessage("culture_entity_charset"), ), 'DIRECTION' => array( 'data_type' => 'boolean', 'values' => array(self::RIGHT_TO_LEFT, self::LEFT_TO_RIGHT), ), ); } } |
1. Название сущности CultureTable состоит из собственно названия сущности и обязательного суффикса Table. Почему суффикс? Название класса Culture резервируется на будущее для «настоящей» ORM.
2. Класс должен быть наследован от Bitrix\Main\Entity\DataManager.
3. Должны быть перекрыты два абстрактных метода:
- getFilePath() возвращает путь к файлу сущности (в TODO есть избавиться от этого)
- getMap() возвращает массив, описывающий поля сущности. О полях ниже.
4. Метод getTableName() может быть определен и должен вернуть название таблицы. Если метод не переопределен, то базовая сущность попытается получить имя таблицы автоматически, например из Bitrix\Sale\OrderTable получилось бы b_sale_order.
Поля сущностей
Рассмотрим подробнее, как описываются поля сущности. Метод getMap() должен вернуть массив полей сущности вида:
array( "FIELD_CODE" => array( 'data_type' => 'тип поля', другие атрибуты ), … ) |
Атрибут data_ type описывает тип поля. Сейчас доступны типы:
boolean (наследует ScalarField)
date (наследует ScalarField)
datetime (наследует DateField)
enum (наследует ScalarField)
float (наследует ScalarField)
integer (наследует ScalarField)
string (наследует ScalarField)
text (наследует StringField)
«Другие атрибуты» могут зависеть от типа поля.
Первичный ключ указывается с помощью атрибута 'primary'=> true. Если первичный ключ - целочисленный с авто увеличением, то нужно указать атрибут 'autocomplete' => true.
Атрибут 'required' => true указывает на обязательность поля.
Атрибут 'format' => 'паттерн' валидирует значение по шаблону регулярного выражения, например '/^[A-Z][A-Za- z0-9]+$/'.
Атрибут expression позволяет получить вычисляемое значение, например:
'VAT_RATE_PRC' => array( 'data_type' => 'float', 'expression' => array( '100 * %s', 'VAT_RATE' ) ), |
Сущности могут быть связаны через поле типа reference. При этом в качестве типа указывается название сущности, на которую идет ссылка, например:
'ORDER' => array( 'data_type' => 'Order', 'reference' => array( '=this.ORDER_ID' => 'ref.ID' ) ), |
Атрибут values задает словарь для поля типа bool , например:
'DIRECTION' => array( 'data_type' => 'boolean', 'values' => array(self::RIGHT_TO_LEFT, self::LEFT_TO_RIGHT), ), |
Атрибут title задает текстовое название поля.
Датаменеджер
Как видим, класс сущности практически не содержит кода. Базовый класс датаменеджера дает нам следующие методы:
- CultureTable::update($ID, $arFields)
- CultureTable::add($arFields)
- CultureTable::Delete ($ID)
- и наш любимый CultureTable::getList($arParams)
- есть также базовый checkFields(Result $result, array $data, $id = null)
Обратите внимание, все методы статические. Сущность может переопределить эти базовые методы, если это необходимо.
Выборка данных
Для выборки данных используется статический метод getList(). На вход принимается массив параметров:
'select' – массив выбираемых в SELECT полей;
'filter' – массив условий фильтра для WHERE;
'group' – массив полей для группировки;
'order' – массив сортировки;
'limit' – ограничение количества записей;
'offset' – смещение начала выборки;
'count_total' – нужно ли считать количество записей выборки.
Пример использования:
$cultureList = CultureTable::getList(array( 'select' =>array('ID', 'NAME'), 'order' => array('NAME' =>'ASC'), 'filter'=>array('=CHARSET'=>'Windows-1251'), )); |
Метод возвращает объект нового ядра Bitrix\Main\DB\Result, с помощью которого можно выбирать записи:
$cultureRes = CultureTable::getList(array('order'=>array('name'=>'asc'))); while($cult = $cultureRes->fetch()) { var_dump($cult); } |
Сортировка может быть задана по нескольким полям. Фильтр задается в расширенном синтаксисе, который применяется в инфоблоках. Использование параметров, неизвестных методу, приведет к выбросу исключения.
Для выборки одной записи по первичному ключу может применяться метод getById():
$cultureDb = CultureTable::getById($cultureId); |
Модификация данных
Статические методы add(), update(), delete() служат для модификации данных сущности и вызывают соответствующие события. Методы add() и update() производят встроенную проверку полей сущностей, вызывая метод checkFields().
Методы модификации данных возвращают объекты типов Bitrix\Main\Entity\AddResult, Bitrix\Main\Entity\UpdateResult и Bitrix\Main\Entity\DeleteResult. Эти классы наследованы от Bitrix\Main\Entity\Result. Назначение этих объектов – сообщить об успешности операции и дать доступ к ошибкам.
checkFields()
Метод checkFields() датаменеджера в настоящее время проверяет поля на обязательные значения (атрибут поля сущности required) и соответствие маске (атрибут format). Для дополнительных проверок класс сущности может переопределить этот метод:
public static function checkFields(Result $result, array $data, $id = null) |
Параметр $result – объект результата. В случае ошибки метод должен добавить ошибку в этот объект. Ошибка может быть либо общая для всей сущности, либо относиться к конкретному полю. В первом случае добавляется объект типа Bitrix\Main\Entity\EntityError, вот втором - Bitrix\Main\Entity\FieldError:
$result->addError(new Entity\EntityError("Нельзя изменить запись номер 1.")); $result->addError(new Entity\FieldError(static::getEntity()->getField('NAME'), 'Поле «имя» очень странное.')); |
Если ни одна ошибка не была добавлена, то проверка считается успешной.
Параметр $data содержит массив вида 'код поля'=>'значение поля'.
Параметр $id содержит значение первичного ключа записи, если производится update, либо не указан, если производится add.
Метод не возвращает результат. Фактический результат содержится в объекте $result.
add()
public static function add(array $data) |
Параметр $data содержит массив вида 'код поля'=>'значение поля'.
Метод возвращает объект типа AddResult. Кроме проверки на успешность, объект результата содержит также идентификатор добавленной записи (только для целочисленных первичных ключей с автоинкрементом). Пример:
$arFields = array( "NAME" => $request['NAME'], "FORMAT_DATE" => $request['FORMAT_DATE'], "FORMAT_DATETIME" => $request['FORMAT_DATETIME'], "WEEK_START" => intval($request["WEEK_START"]), "FORMAT_NAME" => CSite::GetNameFormatByValue($request["FORMAT_NAME"]), "CHARSET" => $request['CHARSET'], "DIRECTION" => $request['DIRECTION'], "CODE" => $request['CODE'], ); if($ID > 0) { $result = CultureTable::update($ID, $arFields); } else { $result = CultureTable::add($arFields); $ID = $result->getId(); } if($result->isSuccess()) { if($request["save"] <> '') LocalRedirect(BX_ROOT."/admin/culture_admin.php?lang=".LANGUAGE_ID); else LocalRedirect(BX_ROOT."/admin/culture_edit.php?lang=".LANGUAGE_ID."&ID=".$ID."&".$tabControl->ActiveTabParam()); } else { $errors = $result->getErrorMessages(); } |
update()
public static function update($primary, array $data) |
Параметр $primary содержит значение первичного ключа. Для составных ключей можно передать массив значений.
Параметр $data содержит массив вида 'код поля'=>'значение поля'.
Метод возвращает объект типа UpdateResult. Пример см. выше.
delete()
public static function delete($primary) |
Параметр $primary содержит значение первичного ключа. Для составных ключей можно передать массив значений.
Метод возвращает объект типа DeleteResult. Пример:
$result = CultureTable::delete($ID); if(!$result->isSuccess()) { $adminList->AddGroupError("(ID=".$ID.") ".implode("<br>", $result->getErrorMessages()), $ID); } |
Результат
Как видно из примеров, объект результата содержит в себе признак успешности (isSuccess() возвращает булевое значение) и список ошибок (getErrorMessages() возвращает массив строк с ошибками):
if(!$result->isSuccess()) $errors = $result->getErrorMessages(); |
Метод getErrors() возвращает массив объектов типов Entity\EntityError или Entity\FieldError.
Класс AddResult содержит метод getId(), который возвращает идентификатор добавленной записи.
События
Датаменеджер при модификации данных отправляет события. Названия событий строятся автоматически и имеют вид <КодСущности> On( Before|<пусто>| After)( Add| Update| Delete), например CultureOnBeforeDelete. Отменить операцию могут только обработчики событий OnBefore.
При добавлении записи порядок событий следующий (указаны параметры для обработчика):
OnBeforeAdd (array("fields"=>$data))
checkFields()
OnAdd(array("fields"=>$data))
add
OnAfterAdd(array("id"=>$id, "fields"=>$data))
Если обработчик OnBeforeAdd вернет ошибку, то добавление не будет произведено. О реализации обработчиков см. ниже.
При обновлении записи порядок событий следующий:
OnBeforeUpdate(array("id"=>$primary, "fields"=>$data))
checkFields()
OnUpdate(array("id"=>$primary, "fields"=>$data))
update
OnAfterUpdate(array("id"=>$primary, "fields"=>$data))
Если обработчик OnBeforeUpdate вернет ошибку, то обновление не будет произведено.
При удалении записи порядок событий следующий:
OnBeforeDelete(array("id"=>$primary))
OnDelete(array("id"=>$primary))
delete
OnAfterDelete(array("id"=>$primary))
Если обработчик OnBeforeDelete вернет ошибку, то удаление не будет произведено.
Обработчики событий
Разберем код обработчика на основе примера:
use Bitrix\Main; use Bitrix\Main\Entity; $eventManager = Main\EventManager::getInstance(); $eventManager->addEventHandler("main", "CultureOnBeforeUpdate", "CultureBeforeUpdate"); function CultureBeforeUpdate(Entity\Event $event) { $errors = array(); $primary = $event->getParameter("id"); if($primary["ID"] == 1) { $errors[] = new Entity\EntityError("Нельзя изменить запись номер 1."); } $fields = $event->getParameter("fields"); if($fields["CHARSET"] == "UTF-8") { $entity = $event->getEntity(); $errors[] = new Entity\FieldError($entity->getField("CHARSET"), "На этом проекте недопустима кодировка UTF-8."); } $changedFields = array(); //slightly change encoding if($fields["CHARSET"] == "Windows-1252") { $changedFields["CHARSET"] = "Windows-1251"; } //impossible to change day of week $unsetFields = array("WEEK_START"); $result = new Entity\EventResult(); if(!empty($errors)) { $result->setErrors($errors); } else { $result->modifyFields($changedFields); $result->unsetFields($unsetFields); } return $result; } |
Функция обработчика получает на вход экземпляр объекта события Entity\DataManagerEvent $event. Класс DataManagerEvent является наследником \Bitrix\Main\Event.
Событие содержит в себе входящие параметры, переданные при отправке события. Получить значение параметра мы можем так:
$primary = $event->getParameter("id"); |
Либо сразу получить все параметры в виде ассоциативного массива:
$params = $event->getParameters(); |
Обработчик может вернуть результат в виде объекта типа Entity\EventResult (наследник Main\EventResult). Результат проверяется только для событий OnBefore, для остальных событий он игнорируется.
Результат по умолчанию создается как успешный. Если нужно вернуть ошибки (и прервать тем самым обновление данных), то используется метод, в который передается массив ошибок:
$result->setErrors($errors); |
Массив может содержать объекты типа Entity\EntityError (для всей сущности) или Entity\FieldError (для поля сущности).
UPD. Теперь обработчик OnBefore может вернуть массив измененных полей (14.0.4). Этот массив мержится с исходными данными.
UPD2. Объект результата события Entity\EventResult позволяет указать поля, которые будут изменены или удалены из исходных данных:
$result->modifyFields($changedFields); $result->unsetFields($unsetFields); |
Будет ли мэйджик функции, чтобы делать вызов
Исключение составляет разве что, _НЕ_ пошаговый импорт каталога миллионника, но при таком расскладе вызов call_user_func_array в EventManager::sendToEventHandler будет на порядок медленнее.
Как дополнительная фича, короткие вызовы были бы удобны, а тем кто хочет "бороться" за производительность может и getParameter использовать
Это будет возможно?
ps/ не поленился, протестировал, да разница есть во времени ... но онная весьма не существенная, на одну интерацию, при моем достаточно не шустром железе, дельта составила 0.00000031714690 секунды ...
Прогресс ведь не стоит на месте, за ним приходится бежать.
осталось допилить при использовании в реальных боевых условиях...
Естественно, эти классы не решат все задачи, но 80-90% хотя бы - уже круто.
Вот highloadblock хотим, во-первых, благодаря наличию админки, во-вторых - это же решение из-коробки
Еще вот у меня важный вопрос: будет привязка iblock к highloadblock и наоборот? К примеру, справочники можно вынести в highloadblock, и прочие "сервисные" данные по каталогу товаров. А сам каталог товаров оставить в iblock, т.к. других вариантов думаю не будет для торгового каталога )
хотя по-сути всегда можно свой тип свойства инфоблока сделать, и уже завязаться в нем на highloadblock
В презентации было 7е и 14е, думал 7го уже увижу....
Действительно ряд вещей пока откладываем в надежде на highloadblock, есть ли смысл еще 7 дней подождать?
сделал по аналогии с User, получаю ошибку Fatal error: Call to undefined method Bitrix\Main\Entity\UField::getColumnName() in /home/.../bitrix/modules/main/lib/entity/querychainelement.php on line 197
upd: сейчас проверил и стандартную UserTable ошибка аналогична, метода getColumnName в ядре вообще нет.
у меня следующий вызов срабатывает без ошибок
Fatal error: Call to undefined method Bitrix\Main\Entity\UField::getColumnName() in/home/.../bitrix/modules/main/lib/entity/querychainelement.php on line 197
что я делаю не так?
а с другими типами нет
$sectionsResult = \Bitrix\Iblock\SectionTable::getList(array(
'select' => array('ID','NAME'),
'filter' => array('IBLOCK_ID'=>3)
));
while($s = $sectionsResult->fetch()) { ... }
Вываливается исключение вида:
Fatal error: Uncaught exception 'Exception' with message 'Unknown field definition `IBLOCK_ID` (IBLOCK_ID) for Section Entity.' in /home/bitrix/www/bitrix/modules/main/lib/entity/querychain.php:282 Stack trace: #0 /home/bitrix/www/bitrix/modules/main/lib/entity/query.php(1271):
Кстати выборка чего-либо кроме ID и названия секции тоже не работает - в getMap только они определены
Хотя отсюда попутно вопрос:
Планируется ли упрощение работы с полем типа datatime?
Чтобы, например, можно было передавать дату не объектом, а строкой в поддерживаемых базовым РНР классом DateTime форматах.
Я спрашиваю, потому что у меня появилось желание посмотреть пример модели и я несмог конечно же етого сделать, так как непонятно, где их хранить.
Не планируется ли пойти по проверенному пути и хранить модели в папке предусмотренной для етого, собственно как обычно делают mvc фреймворки?
Насчет модулей я неочнеь понял. Если я хочу описать сущность и сделать для нее несколько удобных методов выборки и базы + REST, мне ненужно делать для нее модуль, я просто хочу вместо инфоблока хранить сущность с помощью модели и работать с ней с помощью модели. В таком случае небудет стандарта дял разработчиков, описывающего правила хранения таких сущностей?
Если хотите, вы можете разместить свой код в любом месте вашего проекта. Единственно вам придется самостоятельно озаботится его подключением на тех страницах, где он нужен.
Здесь и в новой документации функция checkFields описана как
public static function getConnectionName()
{
return 'my';
}
но все равно берется по умолчанию имя соединения default
ф-ия: query
строка: $connection = \Bitrix\Main\Application::getConnection();
здесь не указано имя соединения и в других нескольких функциях класса тоже,
странно что \Bitrix\Main\Application::getConnection() не определили отдельной функцией,
тогда получение соединения можно было бы переопределить в дочернем классе
По созданному hiload иблоку и соответсвенно таблице
Что-то подобное хотелось бы видеть и в функционале параметров компонентов.
Fatal error: Class 'Bitrix\Main\Entity\UpdateResult' not found in /home/bitrix/www/bitrix/modules/main/lib/entity/datamanager.php on line 416
Стоит последний стабильный битрикс
Как лечить ?
$arFilter = array(
"TITLE" => $find_title,
"DESC" => $find_desc,
"CODE" => $find_code,
"PROGRAM_ID" => $find_program_id
);
WHERE (`TITLE` IS NULL OR `TITLE` = '') AND (`DESC` IS NOT NULL AND LENGTH(`DESC`) > 0) AND (`CODE` IS NULL OR `CODE` = '') AND (`PROGRAM_ID` IS NULL OR `PROGRAM_ID` = 0)
Перепробовал уже всё что возможно:| Или это не предусмотрено в новом API и нужно всё таки в "новом коде" использовать старые функции?
Например,
SELECT ...
FROM `c_client` LEFT JOIN `c_client_contracts` ON `c_client`.`ID` = `c_client_contracts`.`CLIENT_ID`
LEFT JOIN `c_support_license` ON `c_client_contracts`.`id` = `c_support_license`.`contract_id`
можно ли при создании сущности задать значение по умолчанию?
Создал свою таблицу, через GetList не могу фильтровать нормально по дате, как это делать , живые бы примеры.
getCount возвращает возвращает количество всех записей, а нужно вернуть количество записей согласно фильтру.
Делаю:
теперь есть 2 вида обработчиков, версия 1 и версия 2
Хотя там в коде компоненты, при выполнении обработчиков события, первый аргумент и заявляется как массив.
То есть в заново переписанном модуле магазина все равно имеем куски старого ядра?
Все, что я нашел касательно этого параметра - закомментированный кусок кода с комментарием "Vadim: this is for paging but currently is not used"
Не удается воспользоваться описанной системой событий. Попробую описать, что имею, и что хочу получить, и что получаю в итоге.
У меня есть некая таблица SubscribeMessage (сообщение о рассылке), для нее создан и подключен файлик ORM. Когда туда добавляется элемент (сообщение), мне необходимо отправлять уведомления, т.е. некоторый класс CNotice должен отследить добавление сообщение и сделать свои дела.
Добавляю следующие строки в init.php:
Добавив его я полагаю, что вызовется метод SubscribeMessageOnAfterAddHandler класса CNotices. Но он не вызывается.
Если что-то не работает, айда в исходники. В исходниках я не нашел кода, который вызывал бы события, как вы описывали с названием "<КодСущности> On( Before|<пусто>| After)( Add| Update| Delete)".
Я нашел только вызов методов а-ля SubscribeMessage :: OnAfterAdd. Т.е. мало того, что название сокращенное, так еще и это метод класса сообщений, а не уведомлений.
Может я не то ожидаю и/или не так что-то делаю?
Есть множество качественных ORM, неужели вы думаете, что сделаете лучше? Судя по тому что есть у вас сейчас и как вы отстали по технике это не так. Люди душу вкладывают в open source проекты, а вы ?
Автолоадер - самый очевидный пример того, как надо сопротивляться прогрессу. PSR-0, PSR-4 стандарты, которые разрабатывались годами, которые удовлетворяют практически всем требованиям сообщества PHP, нет надо сделать свой. Строго требовать называть файлы так: /lib/inheritedproperty/elementtemplates.php , это издевательство.
Вам нужно развиваться вместе с сообществом. Перенимать лучшее. Цены бы вам не было.
Как понимаю, это один из методов что бы не оставить своих партнеров без заработка ...
В 'group' передаю такой массив:
Array("UF_NAME")
Все равно возвращает десяток записей с одинаковыми именами, то есть не группирует в принципе
Bitrix\Highloadblock\HighloadBlockTable::compileEntity
По какому принципу в первом случае block с маленькой буквы, а во втором с большой?
Гугл по запросу "getSelectedRowsCount bitrix" в принципе документацию битрикса по ORM не показывает. Она есть?
Простые вопросы: как писать «expression» и «runtime»? Как писать подзапрос?
Очень понравился мне, вызовов метода класса Query. Пример:
Последний компонент писал, делая запросы к базе, так скоро никакого битрикса на сайте не останется.
$query->setOrder('RAND()'); возвращает Unknown field definition `RAND()`
$query->registerRuntimeField(
'RAND', array('data_type' => 'float', 'expression' => array('RAND()'))
);
$query->addOrder("RAND", "ASC");
а так бы везде под ошибки и успехи было
вот из-за этого без фильтра вместо одной строчки
Вот так бы сделать, по аналогии с update и getList, где есть ID и ФИЛЬТР, красота
то как выбрать произвольных пользователей?
Но в переписанном заново модуле интернет-магазине начала появляться такая сущность, как коллекции: $order->getPropertyCollection();
Нельзя ли как-то дополнить статью описанием коллекций?
Наверняка, они также создаются по определенной схеме, а не абы как в каждом случае.
Если столбец в базе в нижнем регистре, то сериализация массива не работает
configureSerializationPhp()
configureSerializationJson()
Ошибка
Пример
Вот это добавление с полем FIELDS работает, в базе оно в нижнем регистре
Вот этот fields в нижнем регистре + в базе в нижнем, но вернет ошибку: There is no data to add.