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/objectify/ |
Upload File : |
<?php /** * Bitrix Framework * @package bitrix * @subpackage main * @copyright 2001-2018 Bitrix */ namespace Bitrix\Main\ORM\Objectify; use ArrayAccess; use Bitrix\Main\Authentication\Context; use Bitrix\Main\DB\SqlExpression; use Bitrix\Main\ORM\Data\AddResult; use Bitrix\Main\ORM\Data\DataManager; use Bitrix\Main\ORM\Data\UpdateResult; use Bitrix\Main\ORM\Entity; use Bitrix\Main\ORM\EntityError; use Bitrix\Main\ORM\Fields\ExpressionField; use Bitrix\Main\ORM\Fields\IReadable; use Bitrix\Main\ORM\Fields\ObjectField; use Bitrix\Main\ORM\Fields\Relations\CascadePolicy; use Bitrix\Main\ORM\Fields\Relations\ManyToMany; use Bitrix\Main\ORM\Fields\Relations\OneToMany; use Bitrix\Main\ORM\Fields\Relations\Relation; use Bitrix\Main\ORM\Fields\UserTypeField; use Bitrix\Main\ORM\Query\Query; use Bitrix\Main\ORM\Fields\Relations\Reference; use Bitrix\Main\ORM\Data\Result; use Bitrix\Main\ORM\Fields\ScalarField; use Bitrix\Main\ORM\Fields\FieldTypeMask; use Bitrix\Main\SystemException; use Bitrix\Main\ArgumentException; use Bitrix\Main\Text\StringHelper; use Bitrix\Main\Type\Dictionary; use Bitrix\Main\Web\Json; /** * Entity object * * @property-read \Bitrix\Main\ORM\Entity $entity * @property-read array $primary * @property-read string $primaryAsString * @property-read int $state @see State * @property-read Dictionary $customData * @property Context $authContext For UF values validation * * @package bitrix * @subpackage main */ abstract class EntityObject implements ArrayAccess { /** * Entity Table class. Read-only property. * @var DataManager */ static public $dataClass; /** @var Entity */ protected $_entity; /** * @var int * @see State */ protected $_state = State::RAW; /** * Actual values fetched from DB and collections of relations * @var mixed[]|static[]|Collection[] */ protected $_actualValues = []; /** * Current values - new or rewritten by setter (except changed collections - they are still in actual values) * @var mixed[]|static[] */ protected $_currentValues = []; /** * Container for non-entity data * @var mixed[] */ protected $_runtimeValues = []; /** * @var Dictionary */ protected $_customData = null; /** @var callable[] */ protected $_onPrimarySetListeners = []; /** @var Context */ protected $_authContext; /** @var bool Save lock */ protected $_savingInProgress = false; /** * Cache for lastName => LAST_NAME transforming * @var string[] */ static protected $_camelToSnakeCache = []; /** * Cache for LAST_NAME => lastName transforming * @var string[] */ static protected $_snakeToCamelCache = []; /** * EntityObject constructor * * @param bool|array $setDefaultValues * * @throws ArgumentException * @throws SystemException */ final public function __construct($setDefaultValues = true) { if (is_array($setDefaultValues)) { // we have custom default values foreach ($setDefaultValues as $fieldName => $defaultValue) { $field = $this->entity->getField($fieldName); if ($field instanceof Reference) { if (is_array($defaultValue)) { $defaultValue = $field->getRefEntity()->createObject($defaultValue); } $this->set($fieldName, $defaultValue); } elseif (($field instanceof OneToMany || $field instanceof ManyToMany) && is_array($defaultValue)) { foreach ($defaultValue as $subValue) { if (is_array($subValue)) { $subValue = $field->getRefEntity()->createObject($subValue); } $this->addTo($fieldName, $subValue); } } else { $this->set($fieldName, $defaultValue); } } } // set map default values if ($setDefaultValues || is_array($setDefaultValues)) { foreach ($this->entity->getScalarFields() as $fieldName => $field) { if ($this->sysHasValue($fieldName)) { // already set custom default value continue; } $defaultValue = $field->getDefaultValue($this); if ($defaultValue !== null) { $this->set($fieldName, $defaultValue); } } } } public function __clone() { $this->_actualValues = $this->cloneValues($this->_actualValues); $this->_currentValues = $this->cloneValues($this->_currentValues); } protected function cloneValues(array $values): array { // Do not clone References to avoid infinite recursion $valuesWithoutReferences = $this->filterValuesByMask($values, FieldTypeMask::REFERENCE, true); $references = array_diff_key($values, $valuesWithoutReferences); return array_merge($references, \Bitrix\Main\Type\Collection::clone($valuesWithoutReferences)); } protected function filterValuesByMask(array $values, int $fieldsMask, bool $invertedFilter = false): array { if ($fieldsMask === FieldTypeMask::ALL) { return $invertedFilter ? [] : $values; } return array_filter($values, function($fieldName) use ($fieldsMask, $invertedFilter) { $maskOfSingleField = $this->entity->getField($fieldName)->getTypeMask(); $matchesMask = (bool)($fieldsMask & $maskOfSingleField); return $invertedFilter ? !$matchesMask: $matchesMask; }, ARRAY_FILTER_USE_KEY); } /** * Returns all objects values as an array * * @param int $valuesType * @param int $fieldsMask * @param bool $recursive * * @return array * @throws ArgumentException */ final public function collectValues($valuesType = Values::ALL, $fieldsMask = FieldTypeMask::ALL, $recursive = false) { switch ($valuesType) { case Values::ACTUAL: $objectValues = $this->_actualValues; break; case Values::CURRENT: $objectValues = $this->_currentValues; break; default: $objectValues = array_merge($this->_actualValues, $this->_currentValues); } // filter with field mask if ($fieldsMask !== FieldTypeMask::ALL) { foreach ($objectValues as $fieldName => $value) { $fieldMask = $this->entity->getField($fieldName)->getTypeMask(); if (!($fieldsMask & $fieldMask)) { unset($objectValues[$fieldName]); } } } // recursive convert object to array if ($recursive) { foreach ($objectValues as $fieldName => $value) { if ($value instanceof EntityObject) { $objectValues[$fieldName] = $value->collectValues($valuesType, $fieldsMask, $recursive); } elseif ($value instanceof Collection) { $arrayCollection = []; foreach ($value as $relationObject) { $arrayCollection[] = $relationObject->collectValues($valuesType, $fieldsMask, $recursive); } $objectValues[$fieldName] = $arrayCollection; } } } // remap from uppercase to real field names $values = []; foreach ($objectValues as $k => $v) { $values[$this->entity->getField($k)->getName()] = $v; } return $values; } /** * ActiveRecord save. * * @return Result * @throws ArgumentException * @throws SystemException * @throws \Exception */ final public function save() { // default empty result switch ($this->state) { case State::RAW: $result = new AddResult(); break; case State::CHANGED: case State::ACTUAL: $result = new UpdateResult(); break; default: $result = new Result(); } if ($this->_savingInProgress) { return $result; } $this->_savingInProgress = true; $dataClass = $this->entity->getDataClass(); // check for object fields, it could be changed without notification foreach ($this->_currentValues as $fieldName => $currentValue) { $field = $this->entity->getField($fieldName); if ($field instanceof ObjectField) { $actualValue = $this->_actualValues[$fieldName]; if ($field->encode($currentValue) !== $field->encode($actualValue)) { if ($this->_state === State::ACTUAL) { // value has changed, set new state $this->_state = State::CHANGED; } } else { // value has not changed, hide it until postSave unset($this->_currentValues[$fieldName]); } } } // save data if ($this->_state == State::RAW) { $data = $this->_currentValues; $data['__object'] = $this; // put secret key __object to array $result = $dataClass::add($data); // check for error if (!$result->isSuccess()) { $this->_savingInProgress = false; return $result; } // set primary foreach ($result->getPrimary() as $primaryName => $primaryValue) { $this->sysSetActual($primaryName, $primaryValue); // db value has priority in case of custom value for autocomplete $this->sysSetValue($primaryName, $primaryValue); } // on primary gain event $this->sysOnPrimarySet(); } elseif ($this->_state == State::CHANGED) { // changed scalar and reference if (!empty($this->_currentValues)) { $data = $this->_currentValues; $data['__object'] = $this; // put secret key __object to array $result = $dataClass::update($this->primary, $data); // check for error if (!$result->isSuccess()) { $this->_savingInProgress = false; return $result; } } } // changed collections $this->sysSaveRelations($result); // return if there were errors if (!$result->isSuccess()) { return $result; } $this->sysPostSave(); $this->_savingInProgress = false; return $result; } /** * ActiveRecord delete. * * @return Result * @throws ArgumentException * @throws SystemException */ final public function delete() { $result = new Result; // delete relations foreach ($this->entity->getFields() as $field) { if ($field instanceof Reference) { if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW) { /** @var EntityObject $remoteObject */ $remoteObject = $this->sysGetValue($field->getName()); $remoteObject->delete(); } } elseif ($field instanceof OneToMany) { if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW) { // delete $collection = $this->sysFillRelationCollection($field); foreach ($collection as $object) { $object->delete(); } } elseif ($field->getCascadeDeletePolicy() === CascadePolicy::SET_NULL) { // set null $this->sysRemoveAllFromCollection($field->getName()); } } elseif ($field instanceof ManyToMany) { if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW_ORPHANS) { // delete } elseif ($field->getCascadeDeletePolicy() === CascadePolicy::SET_NULL) { // set null } // always delete mediator records $this->sysRemoveAllFromCollection($field->getName()); } } $this->sysSaveRelations($result); // delete object itself $dataClass = static::$dataClass; $dataClass::setCurrentDeletingObject($this); $deleteResult = $dataClass::delete($this->primary); if (!$deleteResult->isSuccess()) { $result->addErrors($deleteResult->getErrors()); } // clear status foreach ($this->entity->getPrimaryArray()as $primaryName) { unset($this->_actualValues[$primaryName]); } $this->sysChangeState(State::DELETED); return $result; } /** * Constructs existing object from pre-selected data, including references and relations. * * @param mixed $row Array of [field => value] or single scalar primary value. * * @return static * @throws ArgumentException * @throws SystemException */ final public static function wakeUp($row) { /** @var static $objectClass */ $objectClass = get_called_class(); /** @var \Bitrix\Main\ORM\Data\DataManager $dataClass */ $dataClass = static::$dataClass; $entity = $dataClass::getEntity(); $entityPrimary = $entity->getPrimaryArray(); // normalize input data and primary $primary = []; if (!is_array($row)) { // it could be single primary if (count($entityPrimary) == 1) { $primary[$entityPrimary[0]] = $row; $row = []; } else { throw new ArgumentException(sprintf( 'Multi-primary for %s was not found', $objectClass )); } } else { foreach ($entityPrimary as $primaryName) { if (!isset($row[$primaryName])) { throw new ArgumentException(sprintf( 'Primary %s for %s was not found', $primaryName, $objectClass )); } $primary[$primaryName] = $row[$primaryName]; unset($row[$primaryName]); } } // create object /** @var static $object */ $object = new $objectClass(false); // here go with false to not set default values $object->sysChangeState(State::ACTUAL); // set primary foreach ($primary as $primaryName => $primaryValue) { /** @var ScalarField $primaryField */ $primaryField = $entity->getField($primaryName); $object->sysSetActual($primaryName, $primaryField->cast($primaryValue)); } // set other data foreach ($row as $fieldName => $value) { /** @var ScalarField $primaryField */ $field = $entity->getField($fieldName); if ($field instanceof IReadable) { $object->sysSetActual($fieldName, $field->cast($value)); } else { // we have a relation if ($value instanceof static || $value instanceof Collection) { // it is ready data $object->sysSetActual($fieldName, $value); } else { // wake up relation if ($field instanceof Reference) { // wake up an object $remoteObjectClass = $field->getRefEntity()->getObjectClass(); $remoteObject = $remoteObjectClass::wakeUp($value); $object->sysSetActual($fieldName, $remoteObject); } elseif ($field instanceof OneToMany || $field instanceof ManyToMany) { // wake up collection $remoteCollectionClass = $field->getRefEntity()->getCollectionClass(); $remoteCollection = $remoteCollectionClass::wakeUp($value); $object->sysSetActual($fieldName, $remoteCollection); } } } } return $object; } /** * Fills all the values and relations of object * * @param int|string[] $fields Names of fields to fill * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function fill($fields = FieldTypeMask::ALL) { // object must have primary $primaryFilter = Query::filter(); foreach ($this->sysRequirePrimary() as $primaryName => $primaryValue) { $primaryFilter->where($primaryName, $primaryValue); } // collect fields to be selected if (is_array($fields)) { // go through IDLE fields $fieldsToSelect = $this->sysGetIdleFields($fields); } elseif (is_scalar($fields) && !is_numeric($fields)) { // one custom field $fields = [$fields]; $fieldsToSelect = $this->sysGetIdleFields($fields); } else { // get fields according to selector mask $fieldsToSelect = $this->sysGetIdleFieldsByMask($fields); } if (!empty($fieldsToSelect)) { $fieldsToSelect = array_merge($this->entity->getPrimaryArray(), $fieldsToSelect); // build query $dataClass = $this->entity->getDataClass(); $result = $dataClass::query()->setSelect($fieldsToSelect)->where($primaryFilter)->exec(); // set object to identityMap of result, and it will be partially completed by fetch $im = new IdentityMap; $im->put($this); $result->setIdentityMap($im); $result->fetchObject(); // set filled flag to collections foreach ($fieldsToSelect as $fieldName) { // check field before continue, it could be remote REF.ID definition so we skip it here if ($this->entity->hasField($fieldName)) { $field = $this->entity->getField($fieldName); if ($field instanceof OneToMany || $field instanceof ManyToMany) { /** @var Collection $collection */ $collection = $this->sysGetValue($fieldName); if (empty($collection)) { $collection = $field->getRefEntity()->createCollection(); $this->_actualValues[$fieldName] = $collection; } $collection->sysSetFilled(); } } } } // return field value it it was only one if (is_array($fields) && count($fields) == 1 && $this->entity->hasField(current($fields))) { return $this->sysGetValue(current($fields)); } return null; } /** * Fast popular alternative to __call(). * * @return Collection|EntityObject|mixed * @throws SystemException */ public function getId() { if (array_key_exists('ID', $this->_currentValues)) { return $this->_currentValues['ID']; } elseif (array_key_exists('ID', $this->_actualValues)) { return $this->_actualValues['ID']; } elseif (!$this->entity->hasField('ID')) { throw new SystemException(sprintf( 'Unknown method `%s` for object `%s`', 'getId', get_called_class() )); } else { return null; } } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function get($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function remindActual($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function require($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * @param $value * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function set($fieldName, $value) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function reset($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function unset($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function has($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function isFilled($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function isChanged($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * @param $value * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function addTo($fieldName, $value) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * @param $value * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function removeFrom($fieldName, $value) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @param $fieldName * * @return mixed * @throws ArgumentException * @throws SystemException */ final public function removeAll($fieldName) { return $this->__call(__FUNCTION__, func_get_args()); } final public function defineAuthContext(Context $authContext) { $this->_authContext = $authContext; } /** * Magic read-only properties * * @param $name * * @return mixed * @throws ArgumentException * @throws SystemException */ public function __get($name) { switch ($name) { case 'entity': return $this->sysGetEntity(); case 'primary': return $this->sysGetPrimary(); case 'primaryAsString': return $this->sysGetPrimaryAsString(); case 'state': return $this->sysGetState(); case 'dataClass': throw new SystemException('Property `dataClass` should be received as static.'); case 'customData': if ($this->_customData === null) { $this->_customData = new Dictionary; } return $this->_customData; case 'authContext': return $this->_authContext; } throw new SystemException(sprintf( 'Unknown property `%s` for object `%s`', $name, get_called_class() )); } /** * Magic read-only properties * * @param $name * @param $value * * @throws SystemException */ public function __set($name, $value) { switch ($name) { case 'authContext': $this->defineAuthContext($value); return; case 'entity': case 'primary': case 'dataClass': case 'customData': case 'state': throw new SystemException(sprintf( 'Property `%s` for object `%s` is read-only', $name, get_called_class() )); } throw new SystemException(sprintf( 'Unknown property `%s` for object `%s`', $name, get_called_class() )); } /** * Magic to handle getters, setters etc. * * @param $name * @param $arguments * * @return mixed * @throws ArgumentException * @throws SystemException */ public function __call($name, $arguments) { $first3 = substr($name, 0, 3); // regular getter if ($first3 == 'get') { $fieldName = self::sysMethodToFieldCase(substr($name, 3)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check runtime if (array_key_exists($fieldName, $this->_runtimeValues)) { return $this->sysGetRuntime($fieldName); } // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } // check if field exists if ($this->entity->hasField($fieldName)) { return $this->sysGetValue($fieldName); } } // regular setter if ($first3 == 'set') { $fieldName = self::sysMethodToFieldCase(substr($name, 3)); $value = $arguments[0]; if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); $value = $arguments[1]; // check for runtime field if (array_key_exists($fieldName, $this->_runtimeValues)) { throw new SystemException(sprintf( 'Setting value for runtime field `%s` in `%s` is not allowed, it is read-only field', $fieldName, get_called_class() )); } // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } // check if field exists if ($this->entity->hasField($fieldName)) { $field = $this->entity->getField($fieldName); if ($field instanceof IReadable && !($value instanceof SqlExpression)) { $value = $field->cast($value); } return $this->sysSetValue($fieldName, $value); } } if ($first3 == 'has') { $fieldName = self::sysMethodToFieldCase(substr($name, 3)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // runtime fields if (array_key_exists($fieldName, $this->_runtimeValues)) { return true; } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { return $this->sysHasValue($fieldName); } } $first4 = substr($name, 0, 4); // filler if ($first4 == 'fill') { $fieldName = self::sysMethodToFieldCase(substr($name, 4)); // no custom/personal method for fill // check if field exists if ($this->entity->hasField($fieldName)) { return $this->fill([$fieldName]); } } $first5 = substr($name, 0, 5); // relation adder if ($first5 == 'addTo') { $fieldName = self::sysMethodToFieldCase(substr($name, 5)); $value = $arguments[0]; if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); $value = $arguments[1]; // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { $this->sysAddToCollection($fieldName, $value); return; } } // unsetter if ($first5 == 'unset') { $fieldName = self::sysMethodToFieldCase(substr($name, 5)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { return $this->sysUnset($fieldName); } } // resetter if ($first5 == 'reset') { $fieldName = self::sysMethodToFieldCase(substr($name, 5)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { $field = $this->entity->getField($fieldName); if ($field instanceof OneToMany || $field instanceof ManyToMany) { return $this->sysResetRelation($fieldName); } else { return $this->sysReset($fieldName); } } } $first9 = substr($name, 0, 9); // relation mass remover if ($first9 == 'removeAll') { $fieldName = self::sysMethodToFieldCase(substr($name, 9)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { $this->sysRemoveAllFromCollection($fieldName); return; } } $first10 = substr($name, 0, 10); // relation remover if ($first10 == 'removeFrom') { $fieldName = self::sysMethodToFieldCase(substr($name, 10)); $value = $arguments[0]; if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); $value = $arguments[1]; // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { $this->sysRemoveFromCollection($fieldName, $value); return; } } $first12 = substr($name, 0, 12); // actual value getter if ($first12 == 'remindActual') { $fieldName = self::sysMethodToFieldCase(substr($name, 12)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } // check if field exists if ($this->entity->hasField($fieldName)) { return $this->_actualValues[$fieldName] ?? null; } } $first7 = substr($name, 0, 7); // strict getter if ($first7 == 'require') { $fieldName = self::sysMethodToFieldCase(substr($name, 7)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $name.static::sysFieldToMethodCase($fieldName); if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } // check if field exists if ($this->entity->hasField($fieldName)) { return $this->sysGetValue($fieldName, true); } } $first2 = substr($name, 0, 2); $last6 = substr($name, -6); // actual value checker if ($first2 == 'is' && $last6 =='Filled') { $fieldName = self::sysMethodToFieldCase(substr($name, 2, -6)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $first2.static::sysFieldToMethodCase($fieldName).$last6; if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { $field = $this->entity->getField($fieldName); if ($field instanceof OneToMany || $field instanceof ManyToMany) { return array_key_exists($fieldName, $this->_actualValues) && $this->_actualValues[$fieldName]->sysIsFilled(); } else { return $this->sysIsFilled($fieldName); } } } $last7 = substr($name, -7); // runtime value checker if ($first2 == 'is' && $last7 == 'Changed') { $fieldName = self::sysMethodToFieldCase(substr($name, 2, -7)); if ($fieldName == '') { $fieldName = StringHelper::strtoupper($arguments[0]); // check if custom method exists $personalMethodName = $first2.static::sysFieldToMethodCase($fieldName).$last7; if (method_exists($this, $personalMethodName)) { return $this->$personalMethodName(...array_slice($arguments, 1)); } // hard field check $this->entity->getField($fieldName); } if ($this->entity->hasField($fieldName)) { $field = $this->entity->getField($fieldName); if ($field instanceof OneToMany || $field instanceof ManyToMany) { return array_key_exists($fieldName, $this->_actualValues) && $this->_actualValues[$fieldName]->sysIsChanged(); } else { return $this->sysIsChanged($fieldName); } } } throw new SystemException(sprintf( 'Unknown method `%s` for object `%s`', $name, get_called_class() )); } /** * @return Entity * @throws \Bitrix\Main\ArgumentException * @throws \Bitrix\Main\SystemException */ public function sysGetEntity() { if ($this->_entity === null) { /** @var \Bitrix\Main\ORM\Data\DataManager $dataClass */ $dataClass = static::$dataClass; $this->_entity = $dataClass::getEntity(); } return $this->_entity; } /** * Returns [primary => value] array. * * @return array * @throws ArgumentException * @throws SystemException */ public function sysGetPrimary() { $primaryValues = []; foreach ($this->entity->getPrimaryArray() as $primaryName) { $primaryValues[$primaryName] = $this->sysGetValue($primaryName); } return $primaryValues; } public function sysGetPrimaryAsString() { return static::sysSerializePrimary($this->sysGetPrimary(), $this->_entity); } /** * Query Runtime Field values or just any runtime value getter * @internal For internal system usage only. * * @param $name * * @return mixed */ public function sysGetRuntime($name) { return $this->_runtimeValues[$name] ?? null; } /** * Any runtime value setter * @internal For internal system usage only. * * @param $name * @param $value * * @return $this */ public function sysSetRuntime($name, $value) { $this->_runtimeValues[$name] = $value; return $this; } /** * Sets actual value. * @internal For internal system usage only. * * @param $fieldName * @param $value */ public function sysSetActual($fieldName, $value) { $fieldName = StringHelper::strtoupper($fieldName); $this->_actualValues[$fieldName] = $value; // special condition for object values - it should be gotten and changed as current value // and actual value will be used for comparison if ($this->entity->getField($fieldName) instanceof ObjectField) { $this->_currentValues[$fieldName] = clone $value; } } /** * Changes state. * @see State * @internal For internal system usage only. * * @param $state */ public function sysChangeState($state) { if ($this->_state !== $state) { /* not sure if we need check or changes here if ($state == State::RAW) { // actual should be empty } elseif ($state == State::ACTUAL) { // runtime values should be empty } elseif ($state == State::CHANGED) { // runtime values should not be empty }*/ $this->_state = $state; } } /** * Returns current state. * @see State * @internal For internal system usage only. * * @return int */ public function sysGetState() { return $this->_state; } /** * Regular getter, called by __call. * @internal For internal system usage only. * * @param string $fieldName * @param bool $require Throws an exception in the absence of value * * @return mixed * @throws SystemException */ public function sysGetValue($fieldName, $require = false) { $fieldName = StringHelper::strtoupper($fieldName); if (array_key_exists($fieldName, $this->_currentValues)) { return $this->_currentValues[$fieldName]; } else { if ($require && !array_key_exists($fieldName, $this->_actualValues)) { throw new SystemException(sprintf( '%s value is required for further operations', $fieldName )); } return $this->_actualValues[$fieldName] ?? null; } } /** * Regular setter, called by __call. Doesn't validate values. * @internal For internal system usage only. * * @param $fieldName * @param $value * * @return $this * @throws ArgumentException * @throws SystemException */ public function sysSetValue($fieldName, $value) { $fieldName = StringHelper::strtoupper($fieldName); $field = $this->entity->getField($fieldName); // system validations if ($field instanceof ScalarField) { // restrict updating primary if ($this->_state !== State::RAW && in_array($field->getName(), $this->entity->getPrimaryArray())) { throw new SystemException(sprintf( 'Setting value for Primary `%s` in `%s` is not allowed, it is read-only field', $field->getName(), get_called_class() )); } } // no setter for expressions if ($field instanceof ExpressionField && !($field instanceof UserTypeField)) { throw new SystemException(sprintf( 'Setting value for ExpressionField `%s` in `%s` is not allowed, it is read-only field', $fieldName, get_called_class() )); } if ($field instanceof Reference) { if (!empty($value)) { // validate object class and skip null $remoteObjectClass = $field->getRefEntity()->getObjectClass(); if (!($value instanceof $remoteObjectClass)) { throw new ArgumentException(sprintf( 'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, is_object($value) ? get_class($value) : gettype($value) )); } } } // change only if value is different from actual // exclude UF fields for this check as long as UF file fields look into request to change value // (\Bitrix\Main\UserField\Types\FileType::onBeforeSave) // let UF manager handle all the values without optimization if (array_key_exists($fieldName, $this->_actualValues) && !($field instanceof UserTypeField)) { if ($field instanceof IReadable) { if ($field->cast($value) === $this->_actualValues[$fieldName] // double check if value objects are different, but db values are the same || $field->convertValueToDb($field->modifyValueBeforeSave($value, [])) === $field->convertValueToDb($field->modifyValueBeforeSave($this->_actualValues[$fieldName], [])) ) { // forget previous runtime change unset($this->_currentValues[$fieldName]); return $this; } } elseif ($field instanceof Reference) { /** @var static $value */ if ($value->primary === $this->_actualValues[$fieldName]->primary) { // forget previous runtime change unset($this->_currentValues[$fieldName]); return $this; } } } // set value if ($field instanceof ScalarField || $field instanceof UserTypeField) { $this->_currentValues[$fieldName] = $value; } elseif ($field instanceof Reference) { /** @var static $value */ $this->_currentValues[$fieldName] = $value; // set elemental fields if there are any $elementals = $field->getElementals(); if (!empty($elementals)) { $elementalsChanged = false; foreach ($elementals as $localFieldName => $remoteFieldName) { if ($this->entity->getField($localFieldName)->isPrimary()) { // skip local primary in non-raw state if ($this->state !== State::RAW) { continue; } // skip autocomplete if ($this->state === State::RAW && $this->entity->getField($localFieldName)->isAutocomplete()) { continue; } } $remoteField = $field->getRefEntity()->getField($remoteFieldName); if (!empty($value) && !$value->sysHasValue($remoteField->getName()) && $value->state === State::RAW && $remoteField->isPrimary() && $remoteField->isAutocomplete()) { // get primary value after save $localObject = $this; $remoteObject = $value; $remoteObject->sysAddOnPrimarySetListener(function () use ( $localObject, $localFieldName, $remoteObject, $remoteFieldName ) { $localObject->sysSetValue($localFieldName, $remoteObject->get($remoteFieldName)); }); } else { $elementalValue = empty($value) ? null : $value->sysGetValue($remoteFieldName); $this->sysSetValue($localFieldName, $elementalValue); } $elementalsChanged = true; } if (!$elementalsChanged) { // object was not changed actually return $this; } } } else { throw new SystemException(sprintf( 'Unknown field type `%s` in system setter of `%s`', get_class($field), get_called_class() )); } if ($this->_state == State::ACTUAL) { $this->sysChangeState(State::CHANGED); } // on primary gain event if ($field instanceof ScalarField && $field->isPrimary() && $this->sysHasPrimary()) { $this->sysOnPrimarySet(); } return $this; } /** * @internal For internal system usage only. * * @param $fieldName * * @return bool */ public function sysHasValue($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); return $this->sysIsFilled($fieldName) || $this->sysIsChanged($fieldName); } /** * @internal For internal system usage only. * * @param $fieldName * * @return bool */ public function sysIsFilled($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); return array_key_exists($fieldName, $this->_actualValues); } /** * @internal For internal system usage only. * * @param $fieldName * * @return bool */ public function sysIsChanged($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); $field = $this->entity->getField($fieldName); if ($field instanceof ObjectField) { $currentValue = $this->_currentValues[$fieldName] ?? null; $actualValue = $this->_actualValues[$fieldName] ?? null; return $field->encode($currentValue) !== $field->encode($actualValue); } return array_key_exists($fieldName, $this->_currentValues); } /** * @internal For internal system usage only. * * @return bool */ public function sysHasPrimary() { foreach ($this->primary as $primaryValue) { if ($primaryValue === null) { return false; } } return true; } /** * @internal For internal system usage only. */ public function sysOnPrimarySet() { // call subscribers if ($this->sysHasPrimary()) { foreach ($this->_onPrimarySetListeners as $listener) { call_user_func($listener, $this); } } } /** * @internal For internal system usage only. * * @param callable $callback */ public function sysAddOnPrimarySetListener($callback) { // add to listeners $this->_onPrimarySetListeners[] = $callback; } /** * @internal For internal system usage only. * * @param $fieldName * * @return $this */ public function sysUnset($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); unset($this->_currentValues[$fieldName]); unset($this->_actualValues[$fieldName]); return $this; } /** * @internal For internal system usage only. * * @param $fieldName * * @return $this */ public function sysReset($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); unset($this->_currentValues[$fieldName]); return $this; } /** * @internal For internal system usage only. * * @param $fieldName * * @return $this */ public function sysResetRelation($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); if (isset($this->_actualValues[$fieldName])) { /** @var Collection $collection */ $collection = $this->_actualValues[$fieldName]; $collection->sysResetChanges(true); } return $this; } /** * @internal For internal system usage only. * * @return array * @throws ArgumentException * @throws SystemException */ public function sysRequirePrimary() { $primaryValues = []; foreach ($this->entity->getPrimaryArray() as $primaryName) { try { $primaryValues[$primaryName] = $this->sysGetValue($primaryName, true); } catch (SystemException $e) { throw new SystemException(sprintf( 'Primary `%s` value is required for further operations', $primaryName )); } } return $primaryValues; } /** * @internal For internal system usage only. * * Returns non-filled field names according to array of $fields * * @param array $fields * * @return array */ public function sysGetIdleFields($fields = []) { $list = []; if (empty($fields)) { // all fields by default $fields = array_keys($this->entity->getFields()); } foreach ($fields as $fieldName) { $fieldName = StringHelper::strtoupper($fieldName); if (!isset($this->_actualValues[$fieldName])) { // regular field $list[] = $fieldName; } elseif ($this->_actualValues[$fieldName] instanceof Collection && !$this->_actualValues[$fieldName]->sysIsFilled()) { // non-filled collection $list[] = $fieldName; } } return $list; } /** * @internal For internal system usage only. * * Returns non-filled field names according to $mask * * @param int $mask * * @return array */ public function sysGetIdleFieldsByMask($mask = FieldTypeMask::ALL) { $list = []; foreach ($this->entity->getFields() as $field) { $fieldMask = $field->getTypeMask(); if (!isset($this->_actualValues[StringHelper::strtoupper($field->getName())]) && ($mask & $fieldMask) ) { $list[] = $field->getName(); } } return $list; } /** * @internal For internal system usage only. * * @param Result $result * * @throws ArgumentException * @throws SystemException */ public function sysSaveRelations(Result $result) { $saveCascade = true; foreach ($this->_actualValues as $fieldName => $value) { $field = $this->entity->getField($fieldName); if ($field instanceof Reference && !array_key_exists($fieldName, $this->_currentValues)) { // if there is a new relation, then the old one is not into cascade anymore if ($saveCascade && !empty($value)) { /** @var static $value - reference is 1:1 relation, no collection here */ $value->save(); } } elseif ($field instanceof OneToMany) { $collection = $value; /** @var static[] $objectsToSave */ $objectsToSave = []; /** @var static[] $objectsToDelete */ $objectsToDelete = []; if ($collection->sysIsChanged()) { // save changed elements of collection foreach ($collection->sysGetChanges() as $change) { [$remoteObject, $changeType] = $change; if ($changeType == Collection::OBJECT_ADDED) { $objectsToSave[] = $remoteObject; } elseif ($changeType == Collection::OBJECT_REMOVED) { if ($field->getCascadeDeletePolicy() == CascadePolicy::FOLLOW) { $objectsToDelete[] = $remoteObject; } else { // set null by default $objectsToSave[] = $remoteObject; } } } } if ($saveCascade) { // everything should be saved, except deleted foreach ($collection->getAll() as $remoteObject) { if (!in_array($remoteObject, $objectsToDelete) && !in_array($remoteObject, $objectsToSave)) { $objectsToSave[] = $remoteObject; } } } // save remote objects foreach ($objectsToSave as $remoteObject) { $remoteResult = $remoteObject->save(); if (!$remoteResult->isSuccess()) { $result->addErrors($remoteResult->getErrors()); } } // delete remote objects foreach ($objectsToDelete as $remoteObject) { $remoteResult = $remoteObject->delete(); if (!$remoteResult->isSuccess()) { $result->addErrors($remoteResult->getErrors()); } } // forget collection changes if ($collection->sysIsChanged()) { $collection->sysResetChanges(); } } elseif ($field instanceof ManyToMany) { $collection = $value; if ($value->sysIsChanged()) { foreach ($collection->sysGetChanges() as $change) { [$remoteObject, $changeType] = $change; // initialize mediator object $mediatorObjectClass = $field->getMediatorEntity()->getObjectClass(); $localReferenceName = $field->getLocalReferenceName(); $remoteReferenceName = $field->getRemoteReferenceName(); /** @var static $mediatorObject */ $mediatorObject = new $mediatorObjectClass; $mediatorObject->sysSetValue($localReferenceName, $this); $mediatorObject->sysSetValue($remoteReferenceName, $remoteObject); // add or remove mediator depending on changeType if ($changeType == Collection::OBJECT_ADDED) { $mediatorObject->save(); } elseif ($changeType == Collection::OBJECT_REMOVED) { // destroy directly through data class $mediatorDataClass = $field->getMediatorEntity()->getDataClass(); $mediatorDataClass::delete($mediatorObject->primary); } } // forget collection changes $collection->sysResetChanges(); } // should everything be saved? if ($saveCascade) { foreach ($collection->getAll() as $remoteObject) { $remoteResult = $remoteObject->save(); if (!$remoteResult->isSuccess()) { $result->addErrors($remoteResult->getErrors()); } } } } // remove deleted objects from collections if ($value instanceof Collection) { $value->sysReviseDeletedObjects(); } } if ($saveCascade) { $this->sysSaveCurrentReferences(); } } public function sysSaveCurrentReferences() { foreach ($this->_currentValues as $fieldName => $value) { if ($this->entity->getField($fieldName) instanceof Reference && !empty($value)) { $value->save(); } } } public function sysPostSave() { // clear current values foreach ($this->_currentValues as $k => $v) { $field = $this->entity->getField($k); // handle references if ($v instanceof EntityObject) { // hold raw references if ($v->state === State::RAW) { continue; } // move actual or changed if ($v->state === State::ACTUAL || $v->state === State::CHANGED) { $this->sysSetActual($k, $v); } } elseif ($field instanceof ScalarField || $field instanceof UserTypeField) { $v = $field->cast($v); if ($v instanceof SqlExpression) { continue; } $this->sysSetActual($k, $v); } // clear values unset($this->_currentValues[$k]); } // change state $this->sysChangeState(State::ACTUAL); // return object field to current values foreach ($this->_actualValues as $fieldName => $actualValue) { if ($this->entity->getField($fieldName) instanceof ObjectField) { $this->_currentValues[$fieldName] = clone $actualValue; } } } /** * @internal For internal system usage only. * * @param $fieldName * @param $remoteObject * * @throws ArgumentException * @throws SystemException */ public function sysAddToCollection($fieldName, $remoteObject) { $fieldName = StringHelper::strtoupper($fieldName); /** @var OneToMany $field */ $field = $this->entity->getField($fieldName); $remoteObjectClass = $field->getRefEntity()->getObjectClass(); // validate object class if (!($remoteObject instanceof $remoteObjectClass)) { throw new ArgumentException(sprintf( 'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject) )); } // initialize collection $collection = $this->sysGetValue($fieldName); if (empty($collection)) { $collection = $field->getRefEntity()->createCollection(); $this->_actualValues[$fieldName] = $collection; } if ($field instanceof OneToMany) { // set self to the object $remoteFieldName = $field->getRefField()->getName(); $remoteObject->sysSetValue($remoteFieldName, $this); // if we don't have primary right now, repeat setter later if ($this->state == State::RAW) { $localObject = $this; $this->sysAddOnPrimarySetListener(function () use ($localObject, $remoteObject, $remoteFieldName) { $remoteObject->sysSetValue($remoteFieldName, $localObject); }); } } /** @var Collection $collection Add to collection */ $collection->add($remoteObject); // mark object as changed if ($this->_state == State::ACTUAL) { $this->sysChangeState(State::CHANGED); } } /** * @internal For internal system usage only. * * @param $fieldName * @param $remoteObject * * @throws ArgumentException * @throws SystemException */ public function sysRemoveFromCollection($fieldName, $remoteObject) { $fieldName = StringHelper::strtoupper($fieldName); /** @var OneToMany $field */ $field = $this->entity->getField($fieldName); $remoteObjectClass = $field->getRefEntity()->getObjectClass(); // validate object class if (!($remoteObject instanceof $remoteObjectClass)) { throw new ArgumentException(sprintf( 'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject) )); } /** @var Collection $collection Initialize collection */ $collection = $this->sysGetValue($fieldName); if (empty($collection)) { $collection = $field->getRefEntity()->createCollection(); $this->_actualValues[$fieldName] = $collection; } // remove from collection $collection->remove($remoteObject); if ($field instanceof OneToMany) { // remove self from the object if ($field->getCascadeDeletePolicy() == CascadePolicy::FOLLOW) { // nothing to do } else { // set null by default $remoteFieldName = $field->getRefField()->getName(); $remoteObject->sysSetValue($remoteFieldName, null); } } // mark object as changed if ($this->_state == State::ACTUAL) { $this->sysChangeState(State::CHANGED); } } /** * @internal For internal system usage only. * * @param $fieldName * * @throws ArgumentException * @throws SystemException */ public function sysRemoveAllFromCollection($fieldName) { $fieldName = StringHelper::strtoupper($fieldName); $collection = $this->sysFillRelationCollection($fieldName); // remove one by one foreach ($collection as $remoteObject) { $this->sysRemoveFromCollection($fieldName, $remoteObject); } } /** * @param OneToMany|ManyToMany|string $field * * @return Collection * @throws ArgumentException * @throws SystemException */ public function sysFillRelationCollection($field) { if ($field instanceof Relation) { $fieldName = $field->getName(); } else { $fieldName = $field; $field = $this->entity->getField($fieldName); } /** @var Collection $collection initialize collection */ $collection = $this->sysGetValue($fieldName); if (empty($collection)) { $collection = $field->getRefEntity()->createCollection(); $this->_actualValues[$fieldName] = $collection; } if (!$collection->sysIsFilled()) { // we need only primary here $remotePrimaryDefinitions = []; foreach ($field->getRefEntity()->getPrimaryArray() as $primaryName) { $remotePrimaryDefinitions[] = $fieldName.'.'.$primaryName; } $this->fill($remotePrimaryDefinitions); // we can set fullness flag here $collection->sysSetFilled(); } return $collection; } /** * @internal For internal system usage only. * * @param $methodName * * @return string */ public static function sysMethodToFieldCase($methodName) { if (!isset(static::$_camelToSnakeCache[$methodName])) { static::$_camelToSnakeCache[$methodName] = StringHelper::strtoupper( StringHelper::camel2snake($methodName) ); } return static::$_camelToSnakeCache[$methodName]; } /** * @internal For internal system usage only. * * @param $fieldName * * @return string */ public static function sysFieldToMethodCase($fieldName) { if (!isset(static::$_snakeToCamelCache[$fieldName])) { static::$_snakeToCamelCache[$fieldName] = StringHelper::snake2camel($fieldName); } return static::$_snakeToCamelCache[$fieldName]; } /** * @param array $primary * @param Entity $entity * * @return string * @throws ArgumentException */ public static function sysSerializePrimary($primary, $entity) { if (count($entity->getPrimaryArray()) == 1) { return (string) current($primary); } return (string) Json::encode(array_values($primary)); } /** * ArrayAccess interface implementation. * * @param mixed $offset * * @return bool * @throws ArgumentException * @throws SystemException */ public function offsetExists($offset): bool { return $this->sysHasValue($offset) && $this->sysGetValue($offset) !== null; } /** * ArrayAccess interface implementation. * * @param mixed $offset * * @return mixed|null * @throws ArgumentException * @throws SystemException */ #[\ReturnTypeWillChange] public function offsetGet($offset) { if ($this->offsetExists($offset)) { // regular field return $this->get($offset); } elseif (array_key_exists($offset, $this->_runtimeValues)) { // runtime field return $this->sysGetRuntime($offset); } return $this->offsetExists($offset) ? $this->get($offset) : null; } /** * ArrayAccess interface implementation. * * @param mixed $offset * @param mixed $value * * @throws ArgumentException * @throws SystemException */ public function offsetSet($offset, $value): void { if (is_null($offset)) { throw new ArgumentException('Field name should be set'); } else { $this->set($offset, $value); } } /** * ArrayAccess interface implementation. * * @param mixed $offset */ public function offsetUnset($offset): void { $this->unset($offset); } }