Server IP : 80.87.202.40 / Your IP : 216.73.216.169 Web Server : Apache System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64 User : bitrix ( 600) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/main/lib/orm/data/ |
Upload File : |
<?php /** * Bitrix Framework * @package bitrix * @subpackage main * @copyright 2001-2024 Bitrix */ namespace Bitrix\Main\ORM\Data; use Bitrix\Main; use Bitrix\Main\ORM\Entity; use Bitrix\Main\ORM\EntityError; use Bitrix\Main\ORM\Event; use Bitrix\Main\ORM\Fields\ExpressionField; use Bitrix\Main\ORM\Fields\FieldError; use Bitrix\Main\ORM\Fields\FieldTypeMask; use Bitrix\Main\ORM\Fields\ScalarField; use Bitrix\Main\ORM\Objectify\Values; use Bitrix\Main\ORM\Query\Query; use Bitrix\Main\ORM\Query\Result as QueryResult; use Bitrix\Main\Localization\Loc; use Bitrix\Main\ORM\Data\AddStrategy\Contract\AddStrategy; use Bitrix\Main\ORM\Query\Filter\ConditionTree as Filter; use Bitrix\Main\ORM\Objectify\EntityObject; use Bitrix\Main\ORM\Objectify\Collection; Loc::loadMessages(__FILE__); /** * Base entity data manager */ abstract class DataManager { const EVENT_ON_BEFORE_ADD = "OnBeforeAdd"; const EVENT_ON_ADD = "OnAdd"; const EVENT_ON_AFTER_ADD = "OnAfterAdd"; const EVENT_ON_BEFORE_UPDATE = "OnBeforeUpdate"; const EVENT_ON_UPDATE = "OnUpdate"; const EVENT_ON_AFTER_UPDATE = "OnAfterUpdate"; const EVENT_ON_BEFORE_DELETE = "OnBeforeDelete"; const EVENT_ON_DELETE = "OnDelete"; const EVENT_ON_AFTER_DELETE = "OnAfterDelete"; /** @var Entity[] */ protected static $entity; /** @var EntityObject[] Cache of class names */ protected static $objectClass; /** @var Collection[] Cache of class names */ protected static $collectionClass; /** @var EntityObject[][] Objects that called delete() method themselves */ protected static $currentDeletingObjects; /** * Returns entity object * * @return Entity * @throws Main\ArgumentException * @throws Main\SystemException */ public static function getEntity() { $class = static::getEntityClass()::normalizeEntityClass(get_called_class()); if (!isset(static::$entity[$class])) { static::$entity[$class] = static::getEntityClass()::getInstance($class); } return static::$entity[$class]; } public static function unsetEntity($class) { $class = static::getEntityClass()::normalizeEntityClass($class); if (isset(static::$entity[$class])) { unset(static::$entity[$class]); } } /** * Returns DB table name for entity * * @return string */ public static function getTableName() { return null; } /** * Returns connection name for entity * * @return string */ public static function getConnectionName() { return 'default'; } /** * @return string | null */ public static function getTitle() { return null; } /** * Returns class of Object for current entity. * * @return string|EntityObject */ public static function getObjectClass() { if (!isset(static::$objectClass[get_called_class()])) { static::$objectClass[get_called_class()] = static::getObjectClassByDataClass(get_called_class()); } return static::$objectClass[get_called_class()]; } /** * Returns class name (without namespace) of Object for current entity. * * @return string */ final public static function getObjectClassName() { $class = static::getObjectClass(); return substr($class, strrpos($class, '\\') + 1); } protected static function getObjectClassByDataClass($dataClass) { $objectClass = static::getEntityClass()::normalizeName($dataClass); // make class name more unique $namespace = substr($objectClass, 0, strrpos($objectClass, '\\') + 1); $className = substr($objectClass, strrpos($objectClass, '\\') + 1); $className = static::getEntityClass()::getDefaultObjectClassName($className); return $namespace.$className; } /** * Returns class of Object collection for current entity. * * @return string|Collection */ public static function getCollectionClass() { if (!isset(static::$collectionClass[get_called_class()])) { static::$collectionClass[get_called_class()] = static::getCollectionClassByDataClass(get_called_class()); } return static::$collectionClass[get_called_class()]; } /** * Returns class name (without namespace) of Object collection for current entity. * * @return string */ final public static function getCollectionClassName() { $class = static::getCollectionClass(); return substr($class, strrpos($class, '\\') + 1); } protected static function getCollectionClassByDataClass($dataClass) { $objectClass = static::getEntityClass()::normalizeName($dataClass); // make class name more unique $namespace = substr($objectClass, 0, strrpos($objectClass, '\\') + 1); $className = substr($objectClass, strrpos($objectClass, '\\') + 1); $className = static::getEntityClass()::getDefaultCollectionClassName($className); return $namespace.$className; } /** * @return EntityObject|string */ public static function getObjectParentClass() { return EntityObject::class; } /** * @return Collection|string */ public static function getCollectionParentClass() { return Collection::class; } /** * @return Query|string */ public static function getQueryClass() { return Query::class; } /** * @return Entity|string */ public static function getEntityClass() { return Entity::class; } /** * @param bool $setDefaultValues * * @return null Actual type should be annotated by orm:annotate * @throws Main\ArgumentException * @throws Main\SystemException */ final public static function createObject($setDefaultValues = true) { return static::getEntity()->createObject($setDefaultValues); } /** * @return null Actual type should be annotated by orm:annotate * @throws Main\ArgumentException * @throws Main\SystemException */ final public static function createCollection() { return static::getEntity()->createCollection(); } /** * @see EntityObject::wakeUp() * * @param $row * * @return null Actual type should be annotated by orm:annotate * @throws Main\ArgumentException * @throws Main\SystemException */ final public static function wakeUpObject($row) { return static::getEntity()->wakeUpObject($row); } /** * @see Collection::wakeUp() * * @param $rows * * @return null Actual type should be annotated by orm:annotate * @throws Main\ArgumentException * @throws Main\SystemException */ final public static function wakeUpCollection($rows) { return static::getEntity()->wakeUpCollection($rows); } /** * Returns entity map definition. * To get initialized fields @see \Bitrix\Main\ORM\Entity::getFields() and \Bitrix\Main\ORM\Entity::getField() */ public static function getMap() { return array(); } public static function getUfId() { return null; } public static function isUts() { return false; } public static function isUtm() { return false; } /** * @param Query $query * * @return Query */ public static function setDefaultScope($query) { return $query; } /** * @param Entity $entity * * @return null */ public static function postInitialize(Entity $entity) { return null; } /** * Returns selection by entity's primary key and optional parameters for getList() * * @param mixed $primary Primary key of the entity * @param array $parameters Additional parameters for getList() * * @return QueryResult * @throws Main\ArgumentException * @throws Main\ObjectPropertyException * @throws Main\SystemException */ public static function getByPrimary($primary, array $parameters = array()) { static::normalizePrimary($primary); static::validatePrimary($primary); $primaryFilter = array(); foreach ($primary as $k => $v) { $primaryFilter['='.$k] = $v; } if (isset($parameters['filter'])) { $parameters['filter'] = array($primaryFilter, $parameters['filter']); } else { $parameters['filter'] = $primaryFilter; } return static::getList($parameters); } /** * Returns selection by entity's primary key * * @param mixed $id Primary key of the entity * * @return QueryResult * @throws Main\ArgumentException * @throws Main\ObjectPropertyException * @throws Main\SystemException */ public static function getById($id) { return static::getByPrimary($id); } /** * Returns one row (or null) by entity's primary key * * @param mixed $id Primary key of the entity * @param array $parameters Additional parameters for getList() * * @return array|null * @throws Main\ArgumentException * @throws Main\ObjectPropertyException * @throws Main\SystemException */ public static function getRowById($id, array $parameters = []) { $result = static::getByPrimary($id, $parameters); $row = $result->fetch(); return (is_array($row)? $row : null); } /** * Returns one row (or null) by parameters for getList() * * @param array $parameters Primary key of the entity * * @return array|null * @throws Main\ArgumentException * @throws Main\ObjectPropertyException * @throws Main\SystemException */ public static function getRow(array $parameters) { $parameters['limit'] = 1; $result = static::getList($parameters); $row = $result->fetch(); return (is_array($row)? $row : null); } /** * Executes the query and returns selection by parameters of the query. This function is an alias to the Query * object functions * * @param array $parameters An array of query parameters, available keys are:<br> * "select" => array of fields in the SELECT part of the query, aliases are possible in the form of * "alias"=>"field";<br> * "filter" => array of filters in the WHERE/HAVING part of the query in the form of * "(condition)field"=>"value"; also could be an instance of Filter;<br> * "group" => array of fields in the GROUP BY part of the query;<br> * "order" => array of fields in the ORDER BY part of the query in the form of "field"=>"asc|desc";<br> * "limit" => integer indicating maximum number of rows in the selection (like LIMIT n in MySql);<br> * "offset" => integer indicating first row number in the selection (like LIMIT n, 100 in MySql);<br> * "runtime" => array of entity fields created dynamically;<br> * "cache => array of cache options:<br> * "ttl" => integer indicating cache TTL;<br> * "cache_joins" => boolean enabling to cache joins, false by default. * @see Query::filter() * * @return QueryResult * @throws Main\ArgumentException * @throws Main\ObjectPropertyException * @throws Main\SystemException */ public static function getList(array $parameters = array()) { $query = static::query(); if(!isset($parameters['select'])) { $query->setSelect(array('*')); } foreach($parameters as $param => $value) { switch($param) { case 'select': $query->setSelect($value); break; case 'filter': $value instanceof Filter ? $query->where($value) : $query->setFilter($value); break; case 'group': $query->setGroup($value); break; case 'order'; $query->setOrder($value); break; case 'limit': $query->setLimit($value); break; case 'offset': $query->setOffset($value); break; case 'count_total': $query->countTotal($value); break; case 'runtime': foreach ($value as $name => $fieldInfo) { $query->registerRuntimeField($name, $fieldInfo); } break; case 'data_doubling': if($value) { $query->enableDataDoubling(); } else { $query->disableDataDoubling(); } break; case 'private_fields': if($value) { $query->enablePrivateFields(); } else { $query->disablePrivateFields(); } break; case 'cache': $query->setCacheTtl($value["ttl"]); if(isset($value["cache_joins"])) { $query->cacheJoins($value["cache_joins"]); } break; default: throw new Main\ArgumentException("Unknown parameter: ".$param, $param); } } return $query->exec(); } /** * Performs COUNT query on entity and returns the result. * * @param array|Filter $filter * @param array $cache An array of cache options * "ttl" => integer indicating cache TTL * @return int * @throws Main\ObjectPropertyException * @throws Main\SystemException */ public static function getCount($filter = array(), array $cache = array()) { $query = static::query(); // new filter $query->addSelect(new ExpressionField('CNT', 'COUNT(1)')); if ($filter instanceof Filter) { $query->where($filter); } else { $query->setFilter($filter); } if(isset($cache["ttl"])) { $query->setCacheTtl($cache["ttl"]); } $result = $query->exec()->fetch(); return (int)$result['CNT']; } /** * Creates and returns the Query object for the entity * * @return Query * @throws Main\ArgumentException * @throws Main\SystemException */ public static function query() { $queryClass = static::getQueryClass(); return new $queryClass(static::getEntity()); } /** * @param array $data * * @return array * @throws Main\ArgumentException * @throws Main\SystemException */ protected static function replaceFieldName($data = array()) { $newData = []; $entity = static::getEntity(); foreach ($data as $fieldName => $value) { $newData[$entity->getField($fieldName)->getColumnName()] = $value; } return $newData; } /** * @param $primary * @param array $data * * @throws Main\ArgumentException * @throws Main\SystemException */ protected static function normalizePrimary(&$primary, $data = array()) { $entity = static::getEntity(); $entity_primary = $entity->getPrimaryArray(); if ($primary === null) { $primary = array(); // extract primary from data array foreach ($entity_primary as $key) { /** @var ScalarField $field */ $field = $entity->getField($key); if ($field->isAutocomplete()) { continue; } if (!isset($data[$key])) { throw new Main\ArgumentException(sprintf( 'Primary `%s` was not found when trying to query %s row.', $key, $entity->getName() )); } $primary[$key] = $data[$key]; } } elseif (is_scalar($primary)) { if (count($entity_primary) > 1) { throw new Main\ArgumentException(sprintf( 'Require multi primary {`%s`}, but one scalar value "%s" found when trying to query %s row.', join('`, `', $entity_primary), $primary, $entity->getName() )); } $primary = array($entity_primary[0] => $primary); } } /** * @param $primary * * @throws Main\ArgumentException * @throws Main\SystemException */ protected static function validatePrimary($primary) { $entity = static::getEntity(); if (is_array($primary)) { if(empty($primary)) { throw new Main\ArgumentException(sprintf( 'Empty primary found when trying to query %s row.', $entity->getName() )); } $entity_primary = $entity->getPrimaryArray(); foreach (array_keys($primary) as $key) { if (!in_array($key, $entity_primary, true)) { throw new Main\ArgumentException(sprintf( 'Unknown primary `%s` found when trying to query %s row.', $key, $entity->getName() )); } } } else { throw new Main\ArgumentException(sprintf( 'Unknown type of primary "%s" found when trying to query %s row.', gettype($primary), $entity->getName() )); } // primary values validation foreach ($primary as $key => $value) { if (!is_scalar($value) && !($value instanceof Main\Type\Date)) { throw new Main\ArgumentException(sprintf( 'Unknown value type "%s" for primary "%s" found when trying to query %s row.', gettype($value), $key, $entity->getName() )); } } } /** * Checks the data fields before saving to DB. Result stores in the $result object * * @param Result $result * @param mixed $primary * @param array $data * * @throws Main\ArgumentException * @throws Main\SystemException */ public static function checkFields(Result $result, $primary, array $data) { $entity = static::getEntity(); //checks required fields foreach ($entity->getFields() as $field) { if ($field instanceof ScalarField && $field->isRequired()) { $fieldName = $field->getName(); if ( (empty($primary) && (!isset($data[$fieldName]) || $field->isValueEmpty($data[$fieldName]))) || (!empty($primary) && array_key_exists($fieldName, $data) && $field->isValueEmpty($data[$fieldName])) ) { $result->addError(new FieldError( $field, Loc::getMessage("MAIN_ENTITY_FIELD_REQUIRED", array("#FIELD#"=>$field->getTitle())), FieldError::EMPTY_REQUIRED )); } } } // checks data - fieldname & type & strlen etc. foreach ($data as $k => $v) { if ($entity->hasField($k)) { $field = $entity->getField($k); } else { throw new Main\ArgumentException(sprintf( 'Field `%s` not found in entity when trying to query %s row.', $k, $entity->getName() )); } $field->validateValue($v, $primary, $data, $result); } } /** * @param array $fields * @param bool $setDefaultValues * @param array $primary * * @return EntityObject * @throws Main\ArgumentException * @throws Main\SystemException */ protected static function convertArrayToObject(&$fields, $setDefaultValues = false, $primary = null) { // extended data format $data = null; if (isset($fields["fields"]) && is_array($fields["fields"])) { $data = $fields; $fields = $data["fields"]; } // convert to object if (isset($fields['__object'])) { $object = $fields['__object']; unset($fields['__object']); } else { $entity = static::getEntity(); /** @var EntityObject $object */ if ($primary === null) { $object = $entity->createObject($setDefaultValues); foreach ($fields as $fieldName => $value) { // sometimes data array can be used for storing non-entity data if ($entity->hasField($fieldName)) { $object->sysSetValue($fieldName, $value); } } } else { $object = $entity->wakeUpObject($primary); foreach ($fields as $fieldName => $value) { // sometimes data array can be used for storing non-entity data if ($entity->hasField($fieldName)) { if ($entity->getField($fieldName) instanceof ScalarField && $entity->getField($fieldName)->isPrimary()) { // ignore old primary if (array_key_exists($fieldName, $primary) && $primary[$fieldName] == $value) { unset($fields[$fieldName]); continue; } // but prevent primary changing trigger_error(sprintf( 'Primary of %s %s can not be changed. You can delete this row and add a new one', static::getObjectClass(), Main\Web\Json::encode($object->primary) ), E_USER_WARNING); continue; } $object->sysSetValue($fieldName, $value); } } } } // auth context if (isset($data['auth_context'])) { $object->authContext = $data['auth_context']; } return $object; } /** * @param EntityObject $object * @param $ufdata * @param Result $result */ protected static function checkUfFields($object, $ufdata, $result) { global $USER_FIELD_MANAGER, $APPLICATION; $userId = ($object->authContext && $object->authContext->getUserId()) ? $object->authContext->getUserId() : false; if ($object->sysGetState() === Main\ORM\Objectify\State::RAW) { $ufPrimary = false; } else { $ufPrimary = $object->primary; $ufPrimary = end($ufPrimary); } if (!$USER_FIELD_MANAGER->CheckFields($object->entity->getUfId(), $ufPrimary, $ufdata, $userId)) { if (is_object($APPLICATION) && $APPLICATION->getException()) { $e = $APPLICATION->getException(); $result->addError(new EntityError($e->getString())); $APPLICATION->resetException(); } else { $result->addError(new EntityError("Unknown error while checking userfields")); } } } protected static function getAddStrategy(): AddStrategy { return new Main\ORM\Data\AddStrategy\Insert(static::getEntity()); } /** * Adds row to entity table * * @param array $data An array with fields like * array( * "fields" => array( * "FIELD1" => "value1", * "FIELD2" => "value2", * ), * "auth_context" => \Bitrix\Main\Authentication\Context object * ) * or just a plain array of fields. * * This method uses the default strategy defined in the class. * * @return AddResult Contains ID of inserted row * * @throws \Exception */ public static function add(array $data) { return self::sysAddInternal(static::getAddStrategy(), $data); } /** * @internal For internal system usage only. */ final protected static function sysAddInternal( AddStrategy $strategy, array $data, bool $ignoreEvents = false, ): AddResult { global $USER_FIELD_MANAGER; // compatibility $fields = $data; // prepare entity object for compatibility with new code $object = static::convertArrayToObject($fields, true); $entity = static::getEntity(); $result = new AddResult(); try { if (!$ignoreEvents) { static::callOnBeforeAddEvent($object, $fields, $result); } // actualize old-style fields array from object $fields = $object->collectValues(Values::CURRENT, FieldTypeMask::SCALAR); // uf values $ufdata = $object->collectValues(Values::CURRENT, FieldTypeMask::USERTYPE); // check data static::checkFields($result, null, $fields); // check uf data if (!empty($ufdata)) { static::checkUfFields($object, $ufdata, $result); } // check if there is still some data if (empty($fields) && empty($ufdata)) { $result->addError(new EntityError('There is no data to add.')); } // return if any error if (!$result->isSuccess(true)) { return $result; } if (!$ignoreEvents) { //event on adding self::callOnAddEvent($object, $fields, $ufdata); } // use save modifiers $fieldsToDb = $fields; foreach ($fieldsToDb as $fieldName => $value) { $field = $entity->getField($fieldName); if ($field->isPrimary() && $field->isAutocomplete() && is_null($value)) { unset($fieldsToDb[$fieldName]); // postgresql compatibility continue; } $fieldsToDb[$fieldName] = $field->modifyValueBeforeSave($value, $fields); } $addedData = $strategy->add(static::replaceFieldName($fieldsToDb)); $id = $addedData->id; // build standard primary $primary = null; $isGuessedPrimary = false; if (!empty($id)) { if (!empty($entity->getAutoIncrement())) { $primary = [$entity->getAutoIncrement() => $id]; static::normalizePrimary($primary); } else { // for those who did not set 'autocomplete' flag but wants to get id from result $primary = ['ID' => $id]; $isGuessedPrimary = true; } } else { static::normalizePrimary($primary, $fields); } // fill result $result->setPrimary($primary); $result->setData($fields + $ufdata); $result->setObject($object); if (!$isGuessedPrimary) { foreach ($primary as $primaryName => $primaryValue) { $object->sysSetActual($primaryName, $primaryValue); } } // save uf data if (!empty($ufdata)) { $ufUserId = false; if ($object->authContext) { $ufUserId = $object->authContext->getUserId(); } $USER_FIELD_MANAGER->update($entity->getUfId(), end($primary), $ufdata, $ufUserId); } if ($addedData->isDBChanged) { static::cleanCache(); } if (!$ignoreEvents) { static::callOnAfterAddEvent($object, $fields + $ufdata, $id); } } catch (\Exception $e) { // check result to avoid warning $result->isSuccess(); throw $e; } return $result; } /** * Adds several rows to entity table. * * This method uses the default strategy defined in the class. * * @param $rows * @param bool $ignoreEvents * * @return AddResult * @throws Main\ArgumentException * @throws Main\SystemException */ public static function addMulti($rows, $ignoreEvents = false) { return self::sysAddMultiInternal( static::getAddStrategy(), (array)$rows, (bool)$ignoreEvents, ); } /** * @internal For internal system usage only. */ final protected static function sysAddMultiInternal( AddStrategy $strategy, array $multiData, bool $ignoreEvents = false, ): AddResult { if (count($multiData) <= 0) { return (new AddResult())->addError(new EntityError('There is no data to add.')); } if (count($multiData) === 1) { return self::sysAddInternal($strategy, reset($multiData), $ignoreEvents); } global $USER_FIELD_MANAGER; $rows = array_values($multiData); // prepare objects $objects = []; foreach ($rows as $k => &$row) { $objects[$k] = static::convertArrayToObject($row, true); } $entity = static::getEntity(); $result = new AddResult(); try { // call onBeforeEvent if (!$ignoreEvents) { foreach ($objects as $k => $object) { static::callOnBeforeAddEvent($object, $rows[$k], $result); } } // collect array data $allFields = []; $allUfData = []; foreach ($objects as $k => $object) { // actualize old-style fields array from object $allFields[$k] = $object->collectValues(Values::CURRENT, FieldTypeMask::SCALAR); // uf values $allUfData[$k] = $object->collectValues(Values::CURRENT, FieldTypeMask::USERTYPE); } // check data and uf foreach ($objects as $k => $object) { $fields = $allFields[$k]; $ufdata = $allUfData[$k]; // check data static::checkFields($result, null, $fields); // check uf data if (!empty($ufdata)) { static::checkUfFields($object, $ufdata, $result); } // check if there is still some data if (empty($fields) && empty($ufdata)) { $result->addError(new EntityError('There is no data to add.')); } } // return if any error in any row if (!$result->isSuccess(true)) { return $result; } //event on adding if (!$ignoreEvents) { foreach ($objects as $k => $object) { $fields = $allFields[$k]; $ufdata = $allUfData[$k]; self::callOnAddEvent($object, $fields, $ufdata); } } $allFieldsToDb = []; foreach ($allFields as $k => $fields) { // use save modifiers $fieldsToDb = $fields; foreach ($fieldsToDb as $fieldName => $value) { $field = $entity->getField($fieldName); $fieldsToDb[$fieldName] = $field->modifyValueBeforeSave($value, $fields); } $dataReplacedColumn = static::replaceFieldName($fieldsToDb); $allFieldsToDb[$k] = $dataReplacedColumn; } if (!$ignoreEvents && !empty($entity->getAutoIncrement())) { // change to warning trigger_error( 'Multi-insert doesn\'t work with events as far as we can not get last inserted IDs that we need for the events. '. 'Insert query was forced to multiple separate queries.', E_USER_NOTICE ); $forcedSeparateQueries = true; $isDBChanged = false; $ids = []; foreach ($allFieldsToDb as $key => $dbFields) { $addedData = $strategy->add($dbFields); $ids[$key] = $addedData->id; $isDBChanged = $isDBChanged || $addedData->isDBChanged; } } else { $addedData = $strategy->addMulti($allFieldsToDb); $forcedSeparateQueries = false; $isDBChanged = $addedData->isDBChanged; $ids = null; } // save uf data foreach ($allUfData as $k => $ufdata) { if (!empty($ufdata)) { $ufUserId = false; if ($objects[$k]->authContext) { $ufUserId = $objects[$k]->authContext->getUserId(); } $USER_FIELD_MANAGER->update($entity->getUfId(), end($primary), $ufdata, $ufUserId); } } if ($isDBChanged) { static::cleanCache(); } // after event if (!$ignoreEvents) { foreach ($objects as $k => $object) { $fields = $allFields[$k] + $allUfData[$k]; $id = $forcedSeparateQueries ? $ids[$k] : null; static::callOnAfterAddEvent($object, $fields, $id); } } } catch (\Exception $e) { // check result to avoid warning $result->isSuccess(); throw $e; } return $result; } /** * Updates row in entity table by primary key * * @param mixed $primary * @param array $data An array with fields like * array( * "fields" => array( * "FIELD1" => "value1", * "FIELD2" => "value2", * ), * "auth_context" => \Bitrix\Main\Authentication\Context object * ) * or just a plain array of fields. * * @return UpdateResult * * @throws \Exception */ public static function update($primary, array $data) { global $USER_FIELD_MANAGER; // check primary static::normalizePrimary( $primary, isset($data["fields"]) && is_array($data["fields"]) ? $data["fields"] : $data ); static::validatePrimary($primary); // compatibility $fields = $data; // prepare entity object for compatibility with new code $object = static::convertArrayToObject($fields, false, $primary); $entity = static::getEntity(); $result = new UpdateResult(); try { static::callOnBeforeUpdateEvent($object, $fields, $result); // actualize old-style fields array from object $fields = $object->collectValues(Values::CURRENT, FieldTypeMask::SCALAR); // uf values $ufdata = $object->collectValues(Values::CURRENT, FieldTypeMask::USERTYPE); // check data static::checkFields($result, $primary, $fields); // check uf data if (!empty($ufdata)) { static::checkUfFields($object, $ufdata, $result); } // check if there is still some data if (empty($fields) && empty($ufdata)) { return $result; } // return if any error if (!$result->isSuccess(true)) { return $result; } static::callOnUpdateEvent($object, $fields, $ufdata); // use save modifiers $fieldsToDb = $fields; foreach ($fieldsToDb as $fieldName => $value) { $field = $entity->getField($fieldName); $fieldsToDb[$fieldName] = $field->modifyValueBeforeSave($value, $fields); } // save data if (!empty($fieldsToDb)) { $connection = $entity->getConnection(); $helper = $connection->getSqlHelper(); $tableName = $entity->getDBTableName(); $dataReplacedColumn = static::replaceFieldName($fieldsToDb); $update = $helper->prepareUpdate($tableName, $dataReplacedColumn); $replacedPrimary = static::replaceFieldName($primary); $id = array(); foreach ($replacedPrimary as $k => $v) { $id[] = $helper->prepareAssignment($tableName, $k, $v); } $where = implode(' AND ', $id); $sql = "UPDATE ".$helper->quote($tableName)." SET ".$update[0]." WHERE ".$where; $connection->queryExecute($sql, $update[1]); $result->setAffectedRowsCount($connection); } $result->setData($fields + $ufdata); $result->setPrimary($primary); $result->setObject($object); // save uf data if (!empty($ufdata)) { $ufUserId = false; if ($object->authContext) { $ufUserId = $object->authContext->getUserId(); } $USER_FIELD_MANAGER->update($entity->getUfId(), end($primary), $ufdata, $ufUserId); } static::cleanCache(); // event after update static::callOnAfterUpdateEvent($object, $fields + $ufdata); } catch (\Exception $e) { // check result to avoid warning $result->isSuccess(); throw $e; } return $result; } /** * @param array $primaries * @param array $data * @param bool $ignoreEvents * * @return UpdateResult * @throws Main\ArgumentException * @throws Main\SystemException */ public static function updateMulti($primaries, $data, $ignoreEvents = false) { global $USER_FIELD_MANAGER; $entity = static::getEntity(); $primaries = array_values($primaries); /** @var EntityObject[] $objects */ $objects = []; foreach ($primaries as &$primary) { static::normalizePrimary($primary, $data); static::validatePrimary($primary); /** @var EntityObject $object */ $object = $entity->wakeUpObject($primary); foreach ($data as $k => $v) { $object->set($k, $v); } $objects[] = $object; } $result = new UpdateResult; try { // before event if (!$ignoreEvents) { foreach ($objects as $object) { static::callOnBeforeUpdateEvent($object, $data, $result); } } // collect array data $allFields = []; $allUfData = []; foreach ($objects as $k => $object) { // actualize old-style fields array from object $allFields[$k] = $object->collectValues(Values::CURRENT, FieldTypeMask::SCALAR); // uf values $allUfData[$k] = $object->collectValues(Values::CURRENT, FieldTypeMask::USERTYPE); } // check data and uf foreach ($objects as $k => $object) { $fields = $allFields[$k]; $ufdata = $allUfData[$k]; // check data static::checkFields($result, $object->primary, $fields); // check uf data if (!empty($ufdata)) { static::checkUfFields($object, $ufdata, $result); } // check if there is still some data if (empty($fields) && empty($ufdata)) { $result->addError(new EntityError("There is no data to add.")); } } // return if any error in any row if (!$result->isSuccess(true)) { return $result; } //event on adding if (!$ignoreEvents) { foreach ($objects as $k => $object) { $fields = $allFields[$k]; $ufdata = $allUfData[$k]; static::callOnUpdateEvent($object, $fields, $ufdata); } } // prepare sql $allSqlData = []; foreach ($allFields as $k => $fields) { // use save modifiers $fieldsToDb = $fields; foreach ($fieldsToDb as $fieldName => $value) { $field = $entity->getField($fieldName); $fieldsToDb[$fieldName] = $field->modifyValueBeforeSave($value, $fields); } $dataReplacedColumn = static::replaceFieldName($fieldsToDb); $allSqlData[$k] = $dataReplacedColumn; } // check if rows data are equal $areEqual = true; $dataSample = $allSqlData[0]; asort($dataSample); if (!empty($allSqlData[0])) { foreach ($allSqlData as $data) { asort($data); if ($data !== $dataSample) { $areEqual = false; break; } } // save data $connection = $entity->getConnection(); $helper = $connection->getSqlHelper(); $tableName = $entity->getDBTableName(); // save data if ($areEqual) { // one query $update = $helper->prepareUpdate($tableName, $dataSample); $where = []; $isSinglePrimary = (count($entity->getPrimaryArray()) == 1); foreach ($allSqlData as $k => $data) { $replacedPrimary = static::replaceFieldName($objects[$k]->primary); if ($isSinglePrimary) { // for single primary IN is better $primaryName = key($replacedPrimary); $primaryValue = current($replacedPrimary); $tableField = $entity->getConnection()->getTableField($tableName, $primaryName); $where[] = $helper->convertToDb($primaryValue, $tableField); } else { $id = []; foreach ($replacedPrimary as $primaryName => $primaryValue) { $id[] = $helper->prepareAssignment($tableName, $primaryName, $primaryValue); } $where[] = implode(' AND ', $id); } } if ($isSinglePrimary) { $where = $helper->quote($entity->getPrimary()).' IN ('.join(', ', $where).')'; } else { $where = '('.join(') OR (', $where).')'; } $sql = "UPDATE ".$helper->quote($tableName)." SET ".$update[0]." WHERE ".$where; $connection->queryExecute($sql, $update[1]); $result->setAffectedRowsCount($connection); } else { // query for each row foreach ($allSqlData as $k => $dataReplacedColumn) { $update = $helper->prepareUpdate($tableName, $dataReplacedColumn); $replacedPrimary = static::replaceFieldName($objects[$k]->primary); $id = []; foreach ($replacedPrimary as $primaryName => $primaryValue) { $id[] = $helper->prepareAssignment($tableName, $primaryName, $primaryValue); } $where = implode(' AND ', $id); $sql = "UPDATE ".$helper->quote($tableName)." SET ".$update[0]." WHERE ".$where; $connection->queryExecute($sql, $update[1]); $result->setAffectedRowsCount($connection); } } } // doesn't make sense for multiple rows $result->setData($dataSample); if (count($allSqlData) == 1) { $result->setPrimary($objects[0]->primary); $result->setObject($objects[0]); } // save uf data foreach ($allUfData as $k => $ufdata) { if (!empty($ufdata)) { $ufPrimary = $objects[$k]->primary; $USER_FIELD_MANAGER->update($entity->getUfId(), end($ufPrimary), $ufdata); } } static::cleanCache(); // event after update if (!$ignoreEvents) { foreach ($objects as $k => $object) { $fields = $allFields[$k] + $allUfData[$k]; static::callOnAfterUpdateEvent($object, $fields); } } } catch (\Exception $e) { // check result to avoid warning $result->isSuccess(); throw $e; } return $result; } /** * Deletes row in entity table by primary key * * @param mixed $primary * * @return DeleteResult * * @throws \Exception */ public static function delete($primary) { global $USER_FIELD_MANAGER; // check primary static::normalizePrimary($primary); static::validatePrimary($primary); $entity = static::getEntity(); $result = new DeleteResult(); $entityClass = static::getEntity()->getDataClass(); $primaryAsString = EntityObject::sysSerializePrimary($primary, static::getEntity()); $object = !empty(static::$currentDeletingObjects[$entityClass][$primaryAsString]) ? static::$currentDeletingObjects[$entityClass][$primaryAsString] : static::wakeUpObject($primary); try { //event before delete static::callOnBeforeDeleteEvent($object, $entity, $result); // return if any error if (!$result->isSuccess(true)) { return $result; } //event on delete static::callOnDeleteEvent($object, $entity); // delete $connection = $entity->getConnection(); $helper = $connection->getSqlHelper(); $tableName = $entity->getDBTableName(); $replacedPrimary = static::replaceFieldName($primary); $id = array(); foreach ($replacedPrimary as $k => $v) { $id[] = $helper->prepareAssignment($tableName, $k, $v); } $where = implode(' AND ', $id); $sql = "DELETE FROM ".$helper->quote($tableName)." WHERE ".$where; $connection->queryExecute($sql); // delete uf data if ($entity->getUfId()) { $USER_FIELD_MANAGER->delete($entity->getUfId(), end($primary)); } static::cleanCache(); //event after delete static::callOnAfterDeleteEvent($object, $entity); } catch (\Exception $e) { // check result to avoid warning $result->isSuccess(); throw $e; } finally { // clean temporary objects if (!empty(static::$currentDeletingObjects[$entityClass][$primaryAsString])) { unset(static::$currentDeletingObjects[$entityClass][$primaryAsString]); } } return $result; } /** * @param EntityObject $object * @param $fields * @param $result */ protected static function callOnBeforeAddEvent($object, $fields, $result) { //event before adding $event = new Event($object->entity, self::EVENT_ON_BEFORE_ADD, [ 'fields' => $fields, 'object' => $object, ]); $event->send(); $event->getErrors($result); $event->mergeObjectFields($object); //event before adding (modern with namespace) $event = new Event($object->entity, self::EVENT_ON_BEFORE_ADD, [ 'fields' => $fields, 'object' => $object, ], true); $event->send(); $event->getErrors($result); $event->mergeObjectFields($object); } /** * @param $object * @param $fields * @param $ufdata */ protected static function callOnAddEvent($object, $fields, $ufdata) { $event = new Event($object->entity, self::EVENT_ON_ADD, [ 'fields' => $fields + $ufdata, 'object' => clone $object, ]); $event->send(); //event on adding (modern with namespace) $event = new Event($object->entity, self::EVENT_ON_ADD, [ 'fields' => $fields + $ufdata, 'object' => clone $object, ], true); $event->send(); } /** * @param EntityObject $object * @param array $fields * @param int $id */ protected static function callOnAfterAddEvent($object, $fields, $id) { //event after adding $event = new Event($object->entity, self::EVENT_ON_AFTER_ADD, [ 'id' => $id, 'fields' => $fields, 'object' => clone $object, ]); $event->send(); //event after adding (modern with namespace) $event = new Event($object->entity, self::EVENT_ON_AFTER_ADD, [ 'id' => $id, 'primary' => $object->primary, 'fields' => $fields, 'object' => clone $object, ], true); $event->send(); } /** * @param EntityObject $object * @param $fields * @param $result */ protected static function callOnBeforeUpdateEvent($object, $fields, $result) { $event = new Event($object->entity, self::EVENT_ON_BEFORE_UPDATE, [ 'id' => $object->primary, 'fields' => $fields, 'object' => $object, ]); $event->send(); $event->getErrors($result); $event->mergeObjectFields($object); //event before update (modern with namespace) $event = new Event($object->entity, self::EVENT_ON_BEFORE_UPDATE, [ 'id' => $object->primary, 'primary' => $object->primary, 'fields' => $fields, 'object' => $object, ], true); $event->send(); $event->getErrors($result); $event->mergeObjectFields($object); } /** * @param EntityObject $object * @param $fields * @param $ufdata */ protected static function callOnUpdateEvent($object, $fields, $ufdata) { $event = new Event($object->entity, self::EVENT_ON_UPDATE, [ 'id' => $object->primary, 'fields' => $fields + $ufdata, 'object' => clone $object, ]); $event->send(); //event on update (modern with namespace) $event = new Event($object->entity, self::EVENT_ON_UPDATE, [ 'id' => $object->primary, 'primary' => $object->primary, 'fields' => $fields + $ufdata, 'object' => clone $object, ], true); $event->send(); } /** * @param EntityObject $object * @param $fields */ protected static function callOnAfterUpdateEvent($object, $fields) { $event = new Event($object->entity, self::EVENT_ON_AFTER_UPDATE, [ 'id' => $object->primary, 'fields' => $fields, 'object' => clone $object, ]); $event->send(); //event after update (modern with namespace) $event = new Event($object->entity, self::EVENT_ON_AFTER_UPDATE, [ 'id' => $object->primary, 'primary' => $object->primary, 'fields' => $fields, 'object' => clone $object, ], true); $event->send(); } /** * @param $object * @param $entity * @param $result */ protected static function callOnBeforeDeleteEvent($object, $entity, $result) { $event = new Event($entity, self::EVENT_ON_BEFORE_DELETE, array("id" => $object->primary)); $event->send(); $event->getErrors($result); //event before delete (modern with namespace) $event = new Event($entity, self::EVENT_ON_BEFORE_DELETE, array("id" => $object->primary, "primary" => $object->primary, "object" => clone $object), true); $event->send(); $event->getErrors($result); } /** * @param $object * @param $entity */ protected static function callOnDeleteEvent($object, $entity) { $event = new Event($entity, self::EVENT_ON_DELETE, array("id" => $object->primary)); $event->send(); //event on delete (modern with namespace) $event = new Event($entity, self::EVENT_ON_DELETE, array("id" => $object->primary, "primary" => $object->primary, "object" => clone $object), true); $event->send(); } /** * @param $object * @param $entity */ protected static function callOnAfterDeleteEvent($object, $entity) { $event = new Event($entity, self::EVENT_ON_AFTER_DELETE, array("id" => $object->primary)); $event->send(); //event after delete (modern with namespace) $event = new Event($entity, self::EVENT_ON_AFTER_DELETE, array("id" => $object->primary, "primary" => $object->primary, "object" => clone $object), true); $event->send(); } /** * Sets a flag indicating crypto support for a field. * * @param string $field * @param string $table * @param bool $mode */ public static function enableCrypto($field, $table = null, $mode = true) { if($table === null) { $table = static::getTableName(); } $options = array(); $optionString = Main\Config\Option::get("main", "~crypto_".$table); if($optionString <> '') { $options = unserialize($optionString, ['allowed_classes' => false]); } $options[strtoupper($field)] = $mode; Main\Config\Option::set("main", "~crypto_".$table, serialize($options)); } /** * Returns true if crypto is enabled for a field. * * @param string $field * @param string $table * * @return bool */ public static function cryptoEnabled($field, $table = null) { if($table === null) { $table = static::getTableName(); } $optionString = Main\Config\Option::get("main", "~crypto_".$table); if($optionString <> '') { $field = strtoupper($field); $options = unserialize($optionString, ['allowed_classes' => false]); if(isset($options[$field]) && $options[$field] === true) { return true; } } return false; } /** * @param EntityObject $object */ public static function setCurrentDeletingObject($object): void { $entityClass = static::getEntity()->getDataClass(); self::$currentDeletingObjects[$entityClass][$object->primaryAsString] = $object; } /** * Cleans the tablet cache after data modifications. * * @return void */ public static function cleanCache(): void { if (static::isCacheable()) { $entity = static::getEntity(); $entity->cleanCache(); } } /** * You can disable cache for the tablet completely. * * @return bool */ public static function isCacheable(): bool { return true; } /* An inheritor class can define the event handlers for own events. Why? To prevent from rewriting the add/update/delete functions. These handlers are triggered in the Bitrix\Main\ORM\Event::send() function */ public static function onBeforeAdd(Event $event){} public static function onAdd(Event $event){} public static function onAfterAdd(Event $event){} public static function onBeforeUpdate(Event $event){} public static function onUpdate(Event $event){} public static function onAfterUpdate(Event $event){} public static function onBeforeDelete(Event $event){} public static function onDelete(Event $event){} public static function onAfterDelete(Event $event){} }