Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
entityobject.php
1<?php
10
11use ArrayAccess;
36
50abstract class EntityObject implements ArrayAccess
51{
56 static public $dataClass;
57
59 protected $_entity;
60
65 protected $_state = State::RAW;
66
71 protected $_actualValues = [];
72
77 protected $_currentValues = [];
78
83 protected $_runtimeValues = [];
84
88 protected $_customData = null;
89
92
94 protected $_authContext;
95
97 protected $_savingInProgress = false;
98
103 static protected $_camelToSnakeCache = [];
104
109 static protected $_snakeToCamelCache = [];
110
119 final public function __construct($setDefaultValues = true)
120 {
121 if (is_array($setDefaultValues))
122 {
123 // we have custom default values
124 foreach ($setDefaultValues as $fieldName => $defaultValue)
125 {
126 $field = $this->entity->getField($fieldName);
127
128 if ($field instanceof Reference)
129 {
130 if (is_array($defaultValue))
131 {
132 $defaultValue = $field->getRefEntity()->createObject($defaultValue);
133 }
134
135 $this->set($fieldName, $defaultValue);
136 }
137 elseif (($field instanceof OneToMany || $field instanceof ManyToMany)
138 && is_array($defaultValue))
139 {
140 foreach ($defaultValue as $subValue)
141 {
142 if (is_array($subValue))
143 {
144 $subValue = $field->getRefEntity()->createObject($subValue);
145 }
146
147 $this->addTo($fieldName, $subValue);
148 }
149 }
150 else
151 {
152 $this->set($fieldName, $defaultValue);
153 }
154 }
155 }
156
157 // set map default values
158 if ($setDefaultValues || is_array($setDefaultValues))
159 {
160 foreach ($this->entity->getScalarFields() as $fieldName => $field)
161 {
162 if ($this->sysHasValue($fieldName))
163 {
164 // already set custom default value
165 continue;
166 }
167
168 $defaultValue = $field->getDefaultValue($this);
169
170 if ($defaultValue !== null)
171 {
172 $this->set($fieldName, $defaultValue);
173 }
174 }
175 }
176 }
177
178 public function __clone()
179 {
180 $this->_actualValues = $this->cloneValues($this->_actualValues);
181 $this->_currentValues = $this->cloneValues($this->_currentValues);
182 }
183
184 protected function cloneValues(array $values): array
185 {
186 // Do not clone References to avoid infinite recursion
187 $valuesWithoutReferences = $this->filterValuesByMask($values, FieldTypeMask::REFERENCE, true);
188 $references = array_diff_key($values, $valuesWithoutReferences);
189
190 return array_merge($references, \Bitrix\Main\Type\Collection::clone($valuesWithoutReferences));
191 }
192
193 protected function filterValuesByMask(array $values, int $fieldsMask, bool $invertedFilter = false): array
194 {
195 if ($fieldsMask === FieldTypeMask::ALL)
196 {
197 return $invertedFilter ? [] : $values;
198 }
199
200 return array_filter($values, function($fieldName) use ($fieldsMask, $invertedFilter)
201 {
202 $maskOfSingleField = $this->entity->getField($fieldName)->getTypeMask();
203 $matchesMask = (bool)($fieldsMask & $maskOfSingleField);
204
205 return $invertedFilter ? !$matchesMask: $matchesMask;
206 }, ARRAY_FILTER_USE_KEY);
207 }
208
219 final public function collectValues($valuesType = Values::ALL, $fieldsMask = FieldTypeMask::ALL, $recursive = false)
220 {
221 switch ($valuesType)
222 {
223 case Values::ACTUAL:
224 $objectValues = $this->_actualValues;
225 break;
226 case Values::CURRENT:
227 $objectValues = $this->_currentValues;
228 break;
229 default:
230 $objectValues = array_merge($this->_actualValues, $this->_currentValues);
231 }
232
233 // filter with field mask
234 if ($fieldsMask !== FieldTypeMask::ALL)
235 {
236 foreach ($objectValues as $fieldName => $value)
237 {
238 $fieldMask = $this->entity->getField($fieldName)->getTypeMask();
239 if (!($fieldsMask & $fieldMask))
240 {
241 unset($objectValues[$fieldName]);
242 }
243 }
244 }
245
246 // recursive convert object to array
247 if ($recursive)
248 {
249 foreach ($objectValues as $fieldName => $value)
250 {
251 if ($value instanceof EntityObject)
252 {
253 $objectValues[$fieldName] = $value->collectValues($valuesType, $fieldsMask, $recursive);
254 }
255 elseif ($value instanceof Collection)
256 {
257 $arrayCollection = [];
258 foreach ($value as $relationObject)
259 {
260 $arrayCollection[] = $relationObject->collectValues($valuesType, $fieldsMask, $recursive);
261 }
262 $objectValues[$fieldName] = $arrayCollection;
263 }
264 }
265 }
266
267 // remap from uppercase to real field names
268 $values = [];
269
270 foreach ($objectValues as $k => $v)
271 {
272 $values[$this->entity->getField($k)->getName()] = $v;
273 }
274
275 return $values;
276 }
277
286 final public function save()
287 {
288 // default empty result
289 switch ($this->state)
290 {
291 case State::RAW:
292 $result = new AddResult;
293 break;
294 case State::CHANGED:
295 case State::ACTUAL:
296 $result = new UpdateResult;
297 break;
298 default:
299 $result = new Result;
300 }
301
302 if ($this->_savingInProgress)
303 {
304 return $result;
305 }
306
307 $this->_savingInProgress = true;
308
309 $dataClass = $this->entity->getDataClass();
310
311 // check for object fields, it could be changed without notification
312 foreach ($this->_currentValues as $fieldName => $currentValue)
313 {
314 $field = $this->entity->getField($fieldName);
315
316 if ($field instanceof ObjectField)
317 {
318 $actualValue = $this->_actualValues[$fieldName];
319
320 if ($field->encode($currentValue) !== $field->encode($actualValue))
321 {
322 if ($this->_state === State::ACTUAL)
323 {
324 // value has changed, set new state
325 $this->_state = State::CHANGED;
326 }
327 }
328 else
329 {
330 // value has not changed, hide it until postSave
331 unset($this->_currentValues[$fieldName]);
332 }
333 }
334 }
335
336 // save data
337 if ($this->_state == State::RAW)
338 {
339 $data = $this->_currentValues;
340 $data['__object'] = $this;
341
342 // put secret key __object to array
343 $result = $dataClass::add($data);
344
345 // check for error
346 if (!$result->isSuccess())
347 {
348 $this->_savingInProgress = false;
349
350 return $result;
351 }
352
353 // set primary
354 foreach ($result->getPrimary() as $primaryName => $primaryValue)
355 {
356 $this->sysSetActual($primaryName, $primaryValue);
357
358 // db value has priority in case of custom value for autocomplete
359 $this->sysSetValue($primaryName, $primaryValue);
360 }
361
362 // on primary gain event
363 $this->sysOnPrimarySet();
364 }
365 elseif ($this->_state == State::CHANGED)
366 {
367 // changed scalar and reference
368 if (!empty($this->_currentValues))
369 {
370 $data = $this->_currentValues;
371 $data['__object'] = $this;
372
373 // put secret key __object to array
374 $result = $dataClass::update($this->primary, $data);
375
376 // check for error
377 if (!$result->isSuccess())
378 {
379 $this->_savingInProgress = false;
380
381 return $result;
382 }
383 }
384 }
385
386 // changed collections
387 $this->sysSaveRelations($result);
388
389 // return if there were errors
390 if (!$result->isSuccess())
391 {
392 return $result;
393 }
394
395 $this->sysPostSave();
396
397 $this->_savingInProgress = false;
398
399 return $result;
400 }
401
409 final public function delete()
410 {
411 $result = new Result;
412
413 // delete relations
414 foreach ($this->entity->getFields() as $field)
415 {
416 if ($field instanceof Reference)
417 {
418 if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW)
419 {
421 $remoteObject = $this->sysGetValue($field->getName());
422 $remoteObject->delete();
423 }
424 }
425 elseif ($field instanceof OneToMany)
426 {
427 if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW)
428 {
429 // delete
430 $collection = $this->sysFillRelationCollection($field);
431
432 foreach ($collection as $object)
433 {
434 $object->delete();
435 }
436 }
437 elseif ($field->getCascadeDeletePolicy() === CascadePolicy::SET_NULL)
438 {
439 // set null
440 $this->sysRemoveAllFromCollection($field->getName());
441 }
442 }
443 elseif ($field instanceof ManyToMany)
444 {
445 if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW_ORPHANS)
446 {
447 // delete
448 }
449 elseif ($field->getCascadeDeletePolicy() === CascadePolicy::SET_NULL)
450 {
451 // set null
452 }
453
454 // always delete mediator records
455 $this->sysRemoveAllFromCollection($field->getName());
456 }
457 }
458
459 $this->sysSaveRelations($result);
460
461 // delete object itself
462 $dataClass = static::$dataClass;
463 $dataClass::setCurrentDeletingObject($this);
464 $deleteResult = $dataClass::delete($this->primary);
465
466 if (!$deleteResult->isSuccess())
467 {
468 $result->addErrors($deleteResult->getErrors());
469 }
470
471 // clear status
472 foreach ($this->entity->getPrimaryArray()as $primaryName)
473 {
474 unset($this->_actualValues[$primaryName]);
475 }
476
477 $this->sysChangeState(State::DELETED);
478
479 return $result;
480 }
481
491 final public static function wakeUp($row)
492 {
494 $objectClass = get_called_class();
495
497 $dataClass = static::$dataClass;
498
499 $entity = $dataClass::getEntity();
500 $entityPrimary = $entity->getPrimaryArray();
501
502 // normalize input data and primary
503 $primary = [];
504
505 if (!is_array($row))
506 {
507 // it could be single primary
508 if (count($entityPrimary) == 1)
509 {
510 $primary[$entityPrimary[0]] = $row;
511 $row = [];
512 }
513 else
514 {
515 throw new ArgumentException(sprintf(
516 'Multi-primary for %s was not found', $objectClass
517 ));
518 }
519 }
520 else
521 {
522 foreach ($entityPrimary as $primaryName)
523 {
524 if (!isset($row[$primaryName]))
525 {
526 throw new ArgumentException(sprintf(
527 'Primary %s for %s was not found', $primaryName, $objectClass
528 ));
529 }
530
531 $primary[$primaryName] = $row[$primaryName];
532 unset($row[$primaryName]);
533 }
534 }
535
536 // create object
538 $object = new $objectClass(false); // here go with false to not set default values
539 $object->sysChangeState(State::ACTUAL);
540
541 // set primary
542 foreach ($primary as $primaryName => $primaryValue)
543 {
545 $primaryField = $entity->getField($primaryName);
546 $object->sysSetActual($primaryName, $primaryField->cast($primaryValue));
547 }
548
549 // set other data
550 foreach ($row as $fieldName => $value)
551 {
553 $field = $entity->getField($fieldName);
554
555 if ($field instanceof IReadable)
556 {
557 $object->sysSetActual($fieldName, $field->cast($value));
558 }
559 else
560 {
561 // we have a relation
562 if ($value instanceof static || $value instanceof Collection)
563 {
564 // it is ready data
565 $object->sysSetActual($fieldName, $value);
566 }
567 else
568 {
569 // wake up relation
570 if ($field instanceof Reference)
571 {
572 // wake up an object
573 $remoteObjectClass = $field->getRefEntity()->getObjectClass();
574 $remoteObject = $remoteObjectClass::wakeUp($value);
575
576 $object->sysSetActual($fieldName, $remoteObject);
577 }
578 elseif ($field instanceof OneToMany || $field instanceof ManyToMany)
579 {
580 // wake up collection
581 $remoteCollectionClass = $field->getRefEntity()->getCollectionClass();
582 $remoteCollection = $remoteCollectionClass::wakeUp($value);
583
584 $object->sysSetActual($fieldName, $remoteCollection);
585 }
586 }
587 }
588 }
589
590 return $object;
591 }
592
602 final public function fill($fields = FieldTypeMask::ALL)
603 {
604 // object must have primary
605 $primaryFilter = Query::filter();
606
607 foreach ($this->sysRequirePrimary() as $primaryName => $primaryValue)
608 {
609 $primaryFilter->where($primaryName, $primaryValue);
610 }
611
612 // collect fields to be selected
613 if (is_array($fields))
614 {
615 // go through IDLE fields
616 $fieldsToSelect = $this->sysGetIdleFields($fields);
617 }
618 elseif (is_scalar($fields) && !is_numeric($fields))
619 {
620 // one custom field
621 $fields = [$fields];
622 $fieldsToSelect = $this->sysGetIdleFields($fields);
623 }
624 else
625 {
626 // get fields according to selector mask
627 $fieldsToSelect = $this->sysGetIdleFieldsByMask($fields);
628 }
629
630 if (!empty($fieldsToSelect))
631 {
632 $fieldsToSelect = array_merge($this->entity->getPrimaryArray(), $fieldsToSelect);
633
634 // build query
635 $dataClass = $this->entity->getDataClass();
636 $result = $dataClass::query()->setSelect($fieldsToSelect)->where($primaryFilter)->exec();
637
638 // set object to identityMap of result, and it will be partially completed by fetch
639 $im = new IdentityMap;
640 $im->put($this);
641
642 $result->setIdentityMap($im);
643 $result->fetchObject();
644
645 // set filled flag to collections
646 foreach ($fieldsToSelect as $fieldName)
647 {
648 // check field before continue, it could be remote REF.ID definition so we skip it here
649 if ($this->entity->hasField($fieldName))
650 {
651 $field = $this->entity->getField($fieldName);
652
653 if ($field instanceof OneToMany || $field instanceof ManyToMany)
654 {
656 $collection = $this->sysGetValue($fieldName);
657
658 if (empty($collection))
659 {
660 $collection = $field->getRefEntity()->createCollection();
661 $this->_actualValues[$fieldName] = $collection;
662 }
663
664 $collection->sysSetFilled();
665 }
666 }
667 }
668 }
669
670 // return field value it it was only one
671 if (is_array($fields) && count($fields) == 1 && $this->entity->hasField(current($fields)))
672 {
673 return $this->sysGetValue(current($fields));
674 }
675
676 return null;
677 }
678
685 public function getId()
686 {
687 if (array_key_exists('ID', $this->_currentValues))
688 {
689 return $this->_currentValues['ID'];
690 }
691 elseif (array_key_exists('ID', $this->_actualValues))
692 {
693 return $this->_actualValues['ID'];
694 }
695 elseif (!$this->entity->hasField('ID'))
696 {
697 throw new SystemException(sprintf(
698 'Unknown method `%s` for object `%s`', 'getId', get_called_class()
699 ));
700 }
701 else
702 {
703 return null;
704 }
705 }
706
714 final public function get($fieldName)
715 {
716 return $this->__call(__FUNCTION__, func_get_args());
717 }
718
726 final public function remindActual($fieldName)
727 {
728 return $this->__call(__FUNCTION__, func_get_args());
729 }
730
738 final public function require($fieldName)
739 {
740 return $this->__call(__FUNCTION__, func_get_args());
741 }
742
751 final public function set($fieldName, $value)
752 {
753 return $this->__call(__FUNCTION__, func_get_args());
754 }
755
763 final public function reset($fieldName)
764 {
765 return $this->__call(__FUNCTION__, func_get_args());
766 }
767
775 final public function unset($fieldName)
776 {
777 return $this->__call(__FUNCTION__, func_get_args());
778 }
779
787 final public function has($fieldName)
788 {
789 return $this->__call(__FUNCTION__, func_get_args());
790 }
791
799 final public function isFilled($fieldName)
800 {
801 return $this->__call(__FUNCTION__, func_get_args());
802 }
803
811 final public function isChanged($fieldName)
812 {
813 return $this->__call(__FUNCTION__, func_get_args());
814 }
815
824 final public function addTo($fieldName, $value)
825 {
826 return $this->__call(__FUNCTION__, func_get_args());
827 }
828
837 final public function removeFrom($fieldName, $value)
838 {
839 return $this->__call(__FUNCTION__, func_get_args());
840 }
841
849 final public function removeAll($fieldName)
850 {
851 return $this->__call(__FUNCTION__, func_get_args());
852 }
853
854 final public function defineAuthContext(Context $authContext)
855 {
856 $this->_authContext = $authContext;
857 }
858
868 public function __get($name)
869 {
870 switch ($name)
871 {
872 case 'entity':
873 return $this->sysGetEntity();
874 case 'primary':
875 return $this->sysGetPrimary();
876 case 'primaryAsString':
877 return $this->sysGetPrimaryAsString();
878 case 'state':
879 return $this->sysGetState();
880 case 'dataClass':
881 throw new SystemException('Property `dataClass` should be received as static.');
882 case 'customData':
883
884 if ($this->_customData === null)
885 {
886 $this->_customData = new Dictionary;
887 }
888
889 return $this->_customData;
890
891 case 'authContext':
892 return $this->_authContext;
893 }
894
895 throw new SystemException(sprintf(
896 'Unknown property `%s` for object `%s`', $name, get_called_class()
897 ));
898 }
899
908 public function __set($name, $value)
909 {
910 switch ($name)
911 {
912 case 'authContext':
913 return $this->defineAuthContext($value);
914 case 'entity':
915 case 'primary':
916 case 'dataClass':
917 case 'customData':
918 case 'state':
919 throw new SystemException(sprintf(
920 'Property `%s` for object `%s` is read-only', $name, get_called_class()
921 ));
922 }
923
924 throw new SystemException(sprintf(
925 'Unknown property `%s` for object `%s`', $name, get_called_class()
926 ));
927 }
928
939 public function __call($name, $arguments)
940 {
941 $first3 = substr($name, 0, 3);
942
943 // regular getter
944 if ($first3 == 'get')
945 {
946 $fieldName = self::sysMethodToFieldCase(substr($name, 3));
947
948 if ($fieldName == '')
949 {
950 $fieldName = StringHelper::strtoupper($arguments[0]);
951
952 // check runtime
953 if (array_key_exists($fieldName, $this->_runtimeValues))
954 {
955 return $this->sysGetRuntime($fieldName);
956 }
957
958 // check if custom method exists
959 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
960
961 if (method_exists($this, $personalMethodName))
962 {
963 return $this->$personalMethodName(...array_slice($arguments, 1));
964 }
965
966 // hard field check
967 $this->entity->getField($fieldName);
968 }
969
970 // check if field exists
971 if ($this->entity->hasField($fieldName))
972 {
973 return $this->sysGetValue($fieldName);
974 }
975 }
976
977 // regular setter
978 if ($first3 == 'set')
979 {
980 $fieldName = self::sysMethodToFieldCase(substr($name, 3));
981 $value = $arguments[0];
982
983 if ($fieldName == '')
984 {
985 $fieldName = StringHelper::strtoupper($arguments[0]);
986 $value = $arguments[1];
987
988 // check for runtime field
989 if (array_key_exists($fieldName, $this->_runtimeValues))
990 {
991 throw new SystemException(sprintf(
992 'Setting value for runtime field `%s` in `%s` is not allowed, it is read-only field',
993 $fieldName, get_called_class()
994 ));
995 }
996
997 // check if custom method exists
998 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
999
1000 if (method_exists($this, $personalMethodName))
1001 {
1002 return $this->$personalMethodName(...array_slice($arguments, 1));
1003 }
1004
1005 // hard field check
1006 $this->entity->getField($fieldName);
1007 }
1008
1009 // check if field exists
1010 if ($this->entity->hasField($fieldName))
1011 {
1012 $field = $this->entity->getField($fieldName);
1013
1014 if ($field instanceof IReadable && !($value instanceof SqlExpression))
1015 {
1016 $value = $field->cast($value);
1017 }
1018
1019 return $this->sysSetValue($fieldName, $value);
1020 }
1021 }
1022
1023 if ($first3 == 'has')
1024 {
1025 $fieldName = self::sysMethodToFieldCase(substr($name, 3));
1026
1027 if ($fieldName == '')
1028 {
1029 $fieldName = StringHelper::strtoupper($arguments[0]);
1030
1031 // check if custom method exists
1032 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1033
1034 if (method_exists($this, $personalMethodName))
1035 {
1036 return $this->$personalMethodName(...array_slice($arguments, 1));
1037 }
1038
1039 // runtime fields
1040 if (array_key_exists($fieldName, $this->_runtimeValues))
1041 {
1042 return true;
1043 }
1044
1045 // hard field check
1046 $this->entity->getField($fieldName);
1047 }
1048
1049 if ($this->entity->hasField($fieldName))
1050 {
1051 return $this->sysHasValue($fieldName);
1052 }
1053 }
1054
1055 $first4 = substr($name, 0, 4);
1056
1057 // filler
1058 if ($first4 == 'fill')
1059 {
1060 $fieldName = self::sysMethodToFieldCase(substr($name, 4));
1061
1062 // no custom/personal method for fill
1063
1064 // check if field exists
1065 if ($this->entity->hasField($fieldName))
1066 {
1067 return $this->fill([$fieldName]);
1068 }
1069 }
1070
1071 $first5 = substr($name, 0, 5);
1072
1073 // relation adder
1074 if ($first5 == 'addTo')
1075 {
1076 $fieldName = self::sysMethodToFieldCase(substr($name, 5));
1077 $value = $arguments[0];
1078
1079 if ($fieldName == '')
1080 {
1081 $fieldName = StringHelper::strtoupper($arguments[0]);
1082 $value = $arguments[1];
1083
1084 // check if custom method exists
1085 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1086
1087 if (method_exists($this, $personalMethodName))
1088 {
1089 return $this->$personalMethodName(...array_slice($arguments, 1));
1090 }
1091
1092 // hard field check
1093 $this->entity->getField($fieldName);
1094 }
1095
1096 if ($this->entity->hasField($fieldName))
1097 {
1098 return $this->sysAddToCollection($fieldName, $value);
1099 }
1100 }
1101
1102 // unsetter
1103 if ($first5 == 'unset')
1104 {
1105 $fieldName = self::sysMethodToFieldCase(substr($name, 5));
1106
1107 if ($fieldName == '')
1108 {
1109 $fieldName = StringHelper::strtoupper($arguments[0]);
1110
1111 // check if custom method exists
1112 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1113
1114 if (method_exists($this, $personalMethodName))
1115 {
1116 return $this->$personalMethodName(...array_slice($arguments, 1));
1117 }
1118
1119 // hard field check
1120 $this->entity->getField($fieldName);
1121 }
1122
1123 if ($this->entity->hasField($fieldName))
1124 {
1125 return $this->sysUnset($fieldName);
1126 }
1127 }
1128
1129 // resetter
1130 if ($first5 == 'reset')
1131 {
1132 $fieldName = self::sysMethodToFieldCase(substr($name, 5));
1133
1134 if ($fieldName == '')
1135 {
1136 $fieldName = StringHelper::strtoupper($arguments[0]);
1137
1138 // check if custom method exists
1139 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1140
1141 if (method_exists($this, $personalMethodName))
1142 {
1143 return $this->$personalMethodName(...array_slice($arguments, 1));
1144 }
1145
1146 // hard field check
1147 $this->entity->getField($fieldName);
1148 }
1149
1150 if ($this->entity->hasField($fieldName))
1151 {
1152 $field = $this->entity->getField($fieldName);
1153
1154 if ($field instanceof OneToMany || $field instanceof ManyToMany)
1155 {
1156 return $this->sysResetRelation($fieldName);
1157 }
1158 else
1159 {
1160 return $this->sysReset($fieldName);
1161 }
1162 }
1163 }
1164
1165 $first9 = substr($name, 0, 9);
1166
1167 // relation mass remover
1168 if ($first9 == 'removeAll')
1169 {
1170 $fieldName = self::sysMethodToFieldCase(substr($name, 9));
1171
1172 if ($fieldName == '')
1173 {
1174 $fieldName = StringHelper::strtoupper($arguments[0]);
1175
1176 // check if custom method exists
1177 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1178
1179 if (method_exists($this, $personalMethodName))
1180 {
1181 return $this->$personalMethodName(...array_slice($arguments, 1));
1182 }
1183
1184 // hard field check
1185 $this->entity->getField($fieldName);
1186 }
1187
1188 if ($this->entity->hasField($fieldName))
1189 {
1190 return $this->sysRemoveAllFromCollection($fieldName);
1191 }
1192 }
1193
1194 $first10 = substr($name, 0, 10);
1195
1196 // relation remover
1197 if ($first10 == 'removeFrom')
1198 {
1199 $fieldName = self::sysMethodToFieldCase(substr($name, 10));
1200 $value = $arguments[0];
1201
1202 if ($fieldName == '')
1203 {
1204 $fieldName = StringHelper::strtoupper($arguments[0]);
1205 $value = $arguments[1];
1206
1207 // check if custom method exists
1208 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1209
1210 if (method_exists($this, $personalMethodName))
1211 {
1212 return $this->$personalMethodName(...array_slice($arguments, 1));
1213 }
1214
1215 // hard field check
1216 $this->entity->getField($fieldName);
1217 }
1218
1219 if ($this->entity->hasField($fieldName))
1220 {
1221 return $this->sysRemoveFromCollection($fieldName, $value);
1222 }
1223 }
1224
1225 $first12 = substr($name, 0, 12);
1226
1227 // actual value getter
1228 if ($first12 == 'remindActual')
1229 {
1230 $fieldName = self::sysMethodToFieldCase(substr($name, 12));
1231
1232 if ($fieldName == '')
1233 {
1234 $fieldName = StringHelper::strtoupper($arguments[0]);
1235
1236 // check if custom method exists
1237 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1238
1239 if (method_exists($this, $personalMethodName))
1240 {
1241 return $this->$personalMethodName(...array_slice($arguments, 1));
1242 }
1243
1244 // hard field check
1245 $this->entity->getField($fieldName);
1246 }
1247
1248 // check if field exists
1249 if ($this->entity->hasField($fieldName))
1250 {
1251 return $this->_actualValues[$fieldName] ?? null;
1252 }
1253 }
1254
1255 $first7 = substr($name, 0, 7);
1256
1257 // strict getter
1258 if ($first7 == 'require')
1259 {
1260 $fieldName = self::sysMethodToFieldCase(substr($name, 7));
1261
1262 if ($fieldName == '')
1263 {
1264 $fieldName = StringHelper::strtoupper($arguments[0]);
1265
1266 // check if custom method exists
1267 $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1268
1269 if (method_exists($this, $personalMethodName))
1270 {
1271 return $this->$personalMethodName(...array_slice($arguments, 1));
1272 }
1273
1274 // hard field check
1275 $this->entity->getField($fieldName);
1276 }
1277
1278 // check if field exists
1279 if ($this->entity->hasField($fieldName))
1280 {
1281 return $this->sysGetValue($fieldName, true);
1282 }
1283 }
1284
1285 $first2 = substr($name, 0, 2);
1286 $last6 = substr($name, -6);
1287
1288 // actual value checker
1289 if ($first2 == 'is' && $last6 =='Filled')
1290 {
1291 $fieldName = self::sysMethodToFieldCase(substr($name, 2, -6));
1292
1293 if ($fieldName == '')
1294 {
1295 $fieldName = StringHelper::strtoupper($arguments[0]);
1296
1297 // check if custom method exists
1298 $personalMethodName = $first2.static::sysFieldToMethodCase($fieldName).$last6;
1299
1300 if (method_exists($this, $personalMethodName))
1301 {
1302 return $this->$personalMethodName(...array_slice($arguments, 1));
1303 }
1304
1305 // hard field check
1306 $this->entity->getField($fieldName);
1307 }
1308
1309 if ($this->entity->hasField($fieldName))
1310 {
1311 $field = $this->entity->getField($fieldName);
1312
1313 if ($field instanceof OneToMany || $field instanceof ManyToMany)
1314 {
1315 return array_key_exists($fieldName, $this->_actualValues) && $this->_actualValues[$fieldName]->sysIsFilled();
1316 }
1317 else
1318 {
1319 return $this->sysIsFilled($fieldName);
1320 }
1321 }
1322 }
1323
1324 $last7 = substr($name, -7);
1325
1326 // runtime value checker
1327 if ($first2 == 'is' && $last7 == 'Changed')
1328 {
1329 $fieldName = self::sysMethodToFieldCase(substr($name, 2, -7));
1330
1331 if ($fieldName == '')
1332 {
1333 $fieldName = StringHelper::strtoupper($arguments[0]);
1334
1335 // check if custom method exists
1336 $personalMethodName = $first2.static::sysFieldToMethodCase($fieldName).$last7;
1337
1338 if (method_exists($this, $personalMethodName))
1339 {
1340 return $this->$personalMethodName(...array_slice($arguments, 1));
1341 }
1342
1343 // hard field check
1344 $this->entity->getField($fieldName);
1345 }
1346
1347 if ($this->entity->hasField($fieldName))
1348 {
1349 $field = $this->entity->getField($fieldName);
1350
1351 if ($field instanceof OneToMany || $field instanceof ManyToMany)
1352 {
1353 return array_key_exists($fieldName, $this->_actualValues) && $this->_actualValues[$fieldName]->sysIsChanged();
1354 }
1355 else
1356 {
1357 return $this->sysIsChanged($fieldName);
1358 }
1359 }
1360 }
1361
1362 throw new SystemException(sprintf(
1363 'Unknown method `%s` for object `%s`', $name, get_called_class()
1364 ));
1365 }
1366
1372 public function sysGetEntity()
1373 {
1374 if ($this->_entity === null)
1375 {
1377 $dataClass = static::$dataClass;
1378 $this->_entity = $dataClass::getEntity();
1379 }
1380
1381 return $this->_entity;
1382 }
1383
1391 public function sysGetPrimary()
1392 {
1393 $primaryValues = [];
1394
1395 foreach ($this->entity->getPrimaryArray() as $primaryName)
1396 {
1397 $primaryValues[$primaryName] = $this->sysGetValue($primaryName);
1398 }
1399
1400 return $primaryValues;
1401 }
1402
1403 public function sysGetPrimaryAsString()
1404 {
1405 return static::sysSerializePrimary($this->sysGetPrimary(), $this->_entity);
1406 }
1407
1416 public function sysGetRuntime($name)
1417 {
1418 return $this->_runtimeValues[$name] ?? null;
1419 }
1420
1430 public function sysSetRuntime($name, $value)
1431 {
1432 $this->_runtimeValues[$name] = $value;
1433
1434 return $this;
1435 }
1436
1444 public function sysSetActual($fieldName, $value)
1445 {
1446 $fieldName = StringHelper::strtoupper($fieldName);
1447 $this->_actualValues[$fieldName] = $value;
1448
1449 // special condition for object values - it should be gotten and changed as current value
1450 // and actual value will be used for comparison
1451 if ($this->entity->getField($fieldName) instanceof ObjectField)
1452 {
1453 $this->_currentValues[$fieldName] = clone $value;
1454 }
1455 }
1456
1464 public function sysChangeState($state)
1465 {
1466 if ($this->_state !== $state)
1467 {
1468 /* not sure if we need check or changes here
1469 if ($state == State::RAW)
1470 {
1471 // actual should be empty
1472 }
1473 elseif ($state == State::ACTUAL)
1474 {
1475 // runtime values should be empty
1476 }
1477 elseif ($state == State::CHANGED)
1478 {
1479 // runtime values should not be empty
1480 }*/
1481
1482 $this->_state = $state;
1483 }
1484
1485 }
1486
1494 public function sysGetState()
1495 {
1496 return $this->_state;
1497 }
1498
1509 public function sysGetValue($fieldName, $require = false)
1510 {
1511 $fieldName = StringHelper::strtoupper($fieldName);
1512
1513 if (array_key_exists($fieldName, $this->_currentValues))
1514 {
1515 return $this->_currentValues[$fieldName];
1516 }
1517 else
1518 {
1519 if ($require && !array_key_exists($fieldName, $this->_actualValues))
1520 {
1521 throw new SystemException(sprintf(
1522 '%s value is required for further operations', $fieldName
1523 ));
1524 }
1525
1526 return $this->_actualValues[$fieldName] ?? null;
1527 }
1528 }
1529
1541 public function sysSetValue($fieldName, $value)
1542 {
1543 $fieldName = StringHelper::strtoupper($fieldName);
1544 $field = $this->entity->getField($fieldName);
1545
1546 // system validations
1547 if ($field instanceof ScalarField)
1548 {
1549 // restrict updating primary
1550 if ($this->_state !== State::RAW && in_array($field->getName(), $this->entity->getPrimaryArray()))
1551 {
1552 throw new SystemException(sprintf(
1553 'Setting value for Primary `%s` in `%s` is not allowed, it is read-only field',
1554 $field->getName(), get_called_class()
1555 ));
1556 }
1557 }
1558
1559 // no setter for expressions
1560 if ($field instanceof ExpressionField && !($field instanceof UserTypeField))
1561 {
1562 throw new SystemException(sprintf(
1563 'Setting value for ExpressionField `%s` in `%s` is not allowed, it is read-only field',
1564 $fieldName, get_called_class()
1565 ));
1566 }
1567
1568 if ($field instanceof Reference)
1569 {
1570 if (!empty($value))
1571 {
1572 // validate object class and skip null
1573 $remoteObjectClass = $field->getRefEntity()->getObjectClass();
1574
1575 if (!($value instanceof $remoteObjectClass))
1576 {
1577 throw new ArgumentException(sprintf(
1578 'Expected instance of `%s`, got `%s` instead',
1579 $remoteObjectClass,
1580 is_object($value) ? get_class($value) : gettype($value)
1581 ));
1582 }
1583 }
1584 }
1585
1586 // change only if value is different from actual
1587 // exclude UF fields for this check as long as UF file fields look into request to change value
1588 // (\Bitrix\Main\UserField\Types\FileType::onBeforeSave)
1589 // let UF manager handle all the values without optimization
1590 if (array_key_exists($fieldName, $this->_actualValues) && !($field instanceof UserTypeField))
1591 {
1592 if ($field instanceof IReadable)
1593 {
1594 if ($field->cast($value) === $this->_actualValues[$fieldName]
1595 // double check if value objects are different, but db values are the same
1596 || $field->convertValueToDb($field->modifyValueBeforeSave($value, []))
1597 === $field->convertValueToDb($field->modifyValueBeforeSave($this->_actualValues[$fieldName], []))
1598 )
1599 {
1600 // forget previous runtime change
1601 unset($this->_currentValues[$fieldName]);
1602 return $this;
1603 }
1604 }
1605 elseif ($field instanceof Reference)
1606 {
1608 if ($value->primary === $this->_actualValues[$fieldName]->primary)
1609 {
1610 // forget previous runtime change
1611 unset($this->_currentValues[$fieldName]);
1612 return $this;
1613 }
1614 }
1615 }
1616
1617 // set value
1618 if ($field instanceof ScalarField || $field instanceof UserTypeField)
1619 {
1620 $this->_currentValues[$fieldName] = $value;
1621 }
1622 elseif ($field instanceof Reference)
1623 {
1625 $this->_currentValues[$fieldName] = $value;
1626
1627 // set elemental fields if there are any
1628 $elementals = $field->getElementals();
1629
1630 if (!empty($elementals))
1631 {
1632 $elementalsChanged = false;
1633
1634 foreach ($elementals as $localFieldName => $remoteFieldName)
1635 {
1636 if ($this->entity->getField($localFieldName)->isPrimary())
1637 {
1638 // skip local primary in non-raw state
1639 if ($this->state !== State::RAW)
1640 {
1641 continue;
1642 }
1643
1644 // skip autocomplete
1645 if ($this->state === State::RAW && $this->entity->getField($localFieldName)->isAutocomplete())
1646 {
1647 continue;
1648 }
1649 }
1650
1651 $remoteField = $field->getRefEntity()->getField($remoteFieldName);
1652
1653 if (!empty($value) && !$value->sysHasValue($remoteField->getName())
1654 && $value->state === State::RAW && $remoteField->isPrimary() && $remoteField->isAutocomplete())
1655 {
1656 // get primary value after save
1657 $localObject = $this;
1658 $remoteObject = $value;
1659
1660 $remoteObject->sysAddOnPrimarySetListener(function () use (
1661 $localObject, $localFieldName, $remoteObject, $remoteFieldName
1662 ) {
1663 $localObject->sysSetValue($localFieldName, $remoteObject->get($remoteFieldName));
1664 });
1665 }
1666 else
1667 {
1668 $elementalValue = empty($value) ? null : $value->sysGetValue($remoteFieldName);
1669 $this->sysSetValue($localFieldName, $elementalValue);
1670 }
1671
1672 $elementalsChanged = true;
1673 }
1674
1675 if (!$elementalsChanged)
1676 {
1677 // object was not changed actually
1678 return $this;
1679 }
1680 }
1681 }
1682 else
1683 {
1684 throw new SystemException(sprintf(
1685 'Unknown field type `%s` in system setter of `%s`', get_class($field), get_called_class()
1686 ));
1687 }
1688
1689 if ($this->_state == State::ACTUAL)
1690 {
1691 $this->sysChangeState(State::CHANGED);
1692 }
1693
1694 // on primary gain event
1695 if ($field instanceof ScalarField && $field->isPrimary() && $this->sysHasPrimary())
1696 {
1697 $this->sysOnPrimarySet();
1698 }
1699
1700 return $this;
1701 }
1702
1710 public function sysHasValue($fieldName)
1711 {
1712 $fieldName = StringHelper::strtoupper($fieldName);
1713
1714 return $this->sysIsFilled($fieldName) || $this->sysIsChanged($fieldName);
1715 }
1716
1724 public function sysIsFilled($fieldName)
1725 {
1726 $fieldName = StringHelper::strtoupper($fieldName);
1727
1728 return array_key_exists($fieldName, $this->_actualValues);
1729 }
1730
1738 public function sysIsChanged($fieldName)
1739 {
1740 $fieldName = StringHelper::strtoupper($fieldName);
1741 $field = $this->entity->getField($fieldName);
1742
1743 if ($field instanceof ObjectField)
1744 {
1745 $currentValue = $this->_currentValues[$fieldName];
1746 $actualValue = $this->_actualValues[$fieldName];
1747
1748 return $field->encode($currentValue) !== $field->encode($actualValue);
1749 }
1750
1751 return array_key_exists($fieldName, $this->_currentValues);
1752 }
1753
1759 public function sysHasPrimary()
1760 {
1761 foreach ($this->primary as $primaryValue)
1762 {
1763 if ($primaryValue === null)
1764 {
1765 return false;
1766 }
1767 }
1768
1769 return true;
1770 }
1771
1775 public function sysOnPrimarySet()
1776 {
1777 // call subscribers
1778 if ($this->sysHasPrimary())
1779 {
1780 foreach ($this->_onPrimarySetListeners as $listener)
1781 {
1782 call_user_func($listener, $this);
1783 }
1784 }
1785 }
1786
1792 public function sysAddOnPrimarySetListener($callback)
1793 {
1794 // add to listeners
1795 $this->_onPrimarySetListeners[] = $callback;
1796 }
1797
1805 public function sysUnset($fieldName)
1806 {
1807 $fieldName = StringHelper::strtoupper($fieldName);
1808
1809 unset($this->_currentValues[$fieldName]);
1810 unset($this->_actualValues[$fieldName]);
1811
1812 return $this;
1813 }
1814
1822 public function sysReset($fieldName)
1823 {
1824 $fieldName = StringHelper::strtoupper($fieldName);
1825
1826 unset($this->_currentValues[$fieldName]);
1827
1828 return $this;
1829 }
1830
1838 public function sysResetRelation($fieldName)
1839 {
1840 $fieldName = StringHelper::strtoupper($fieldName);
1841
1842 if (isset($this->_actualValues[$fieldName]))
1843 {
1845 $collection = $this->_actualValues[$fieldName];
1846 $collection->sysResetChanges(true);
1847 }
1848
1849 return $this;
1850 }
1851
1859 public function sysRequirePrimary()
1860 {
1861 $primaryValues = [];
1862
1863 foreach ($this->entity->getPrimaryArray() as $primaryName)
1864 {
1865 try
1866 {
1867 $primaryValues[$primaryName] = $this->sysGetValue($primaryName, true);
1868 }
1869 catch (SystemException $e)
1870 {
1871 throw new SystemException(sprintf(
1872 'Primary `%s` value is required for further operations', $primaryName
1873 ));
1874 }
1875 }
1876
1877 return $primaryValues;
1878 }
1879
1889 public function sysGetIdleFields($fields = [])
1890 {
1891 $list = [];
1892
1893 if (empty($fields))
1894 {
1895 // all fields by default
1896 $fields = array_keys($this->entity->getFields());
1897 }
1898
1899 foreach ($fields as $fieldName)
1900 {
1901 $fieldName = StringHelper::strtoupper($fieldName);
1902
1903 if (!isset($this->_actualValues[$fieldName]))
1904 {
1905 // regular field
1906 $list[] = $fieldName;
1907 }
1908 elseif ($this->_actualValues[$fieldName] instanceof Collection && !$this->_actualValues[$fieldName]->sysIsFilled())
1909 {
1910 // non-filled collection
1911 $list[] = $fieldName;
1912 }
1913 }
1914
1915 return $list;
1916 }
1917
1927 public function sysGetIdleFieldsByMask($mask = FieldTypeMask::ALL)
1928 {
1929 $list = [];
1930
1931 foreach ($this->entity->getFields() as $field)
1932 {
1933 $fieldMask = $field->getTypeMask();
1934
1935 if (!isset($this->_actualValues[StringHelper::strtoupper($field->getName())])
1936 && ($mask & $fieldMask)
1937 )
1938 {
1939 $list[] = $field->getName();
1940 }
1941 }
1942
1943 return $list;
1944 }
1945
1954 public function sysSaveRelations(Result $result)
1955 {
1956 $saveCascade = true;
1957
1958 foreach ($this->_actualValues as $fieldName => $value)
1959 {
1960 $field = $this->entity->getField($fieldName);
1961
1962 if ($field instanceof Reference && !array_key_exists($fieldName, $this->_currentValues))
1963 {
1964 // if there is a new relation, then the old one is not into cascade anymore
1965 if ($saveCascade && !empty($value))
1966 {
1967 $value->save();
1968 }
1969 }
1970 elseif ($field instanceof OneToMany)
1971 {
1972 $collection = $value;
1973
1975 $objectsToSave = [];
1976
1978 $objectsToDelete = [];
1979
1980 if ($collection->sysIsChanged())
1981 {
1982 // save changed elements of collection
1983 foreach ($collection->sysGetChanges() as $change)
1984 {
1985 list($remoteObject, $changeType) = $change;
1986
1987 if ($changeType == Collection::OBJECT_ADDED)
1988 {
1989 $objectsToSave[] = $remoteObject;
1990 }
1991 elseif ($changeType == Collection::OBJECT_REMOVED)
1992 {
1993 if ($field->getCascadeDeletePolicy() == CascadePolicy::FOLLOW)
1994 {
1995 $objectsToDelete[] = $remoteObject;
1996 }
1997 else
1998 {
1999 // set null by default
2000 $objectsToSave[] = $remoteObject;
2001 }
2002 }
2003 }
2004 }
2005
2006 if ($saveCascade)
2007 {
2008 // everything should be saved, except deleted
2009 foreach ($collection->getAll() as $remoteObject)
2010 {
2011 if (!in_array($remoteObject, $objectsToDelete) && !in_array($remoteObject, $objectsToSave))
2012 {
2013 $objectsToSave[] = $remoteObject;
2014 }
2015 }
2016 }
2017
2018 // save remote objects
2019 foreach ($objectsToSave as $remoteObject)
2020 {
2021 $remoteResult = $remoteObject->save();
2022
2023 if (!$remoteResult->isSuccess())
2024 {
2025 $result->addErrors($remoteResult->getErrors());
2026 }
2027 }
2028
2029 // delete remote objects
2030 foreach ($objectsToDelete as $remoteObject)
2031 {
2032 $remoteResult = $remoteObject->delete();
2033
2034 if (!$remoteResult->isSuccess())
2035 {
2036 $result->addErrors($remoteResult->getErrors());
2037 }
2038 }
2039
2040 // forget collection changes
2041 if ($collection->sysIsChanged())
2042 {
2043 $collection->sysResetChanges();
2044 }
2045 }
2046 elseif ($field instanceof ManyToMany)
2047 {
2048 $collection = $value;
2049
2050 if ($value->sysIsChanged())
2051 {
2052 foreach ($collection->sysGetChanges() as $change)
2053 {
2054 list($remoteObject, $changeType) = $change;
2055
2056 // initialize mediator object
2057 $mediatorObjectClass = $field->getMediatorEntity()->getObjectClass();
2058 $localReferenceName = $field->getLocalReferenceName();
2059 $remoteReferenceName = $field->getRemoteReferenceName();
2060
2062 $mediatorObject = new $mediatorObjectClass;
2063 $mediatorObject->sysSetValue($localReferenceName, $this);
2064 $mediatorObject->sysSetValue($remoteReferenceName, $remoteObject);
2065
2066 // add or remove mediator depending on changeType
2067 if ($changeType == Collection::OBJECT_ADDED)
2068 {
2069 $mediatorObject->save();
2070 }
2071 elseif ($changeType == Collection::OBJECT_REMOVED)
2072 {
2073 // destroy directly through data class
2074 $mediatorDataClass = $field->getMediatorEntity()->getDataClass();
2075 $mediatorDataClass::delete($mediatorObject->primary);
2076 }
2077 }
2078
2079 // forget collection changes
2080 $collection->sysResetChanges();
2081 }
2082
2083 // should everything be saved?
2084 if ($saveCascade)
2085 {
2086 foreach ($collection->getAll() as $remoteObject)
2087 {
2088 $remoteResult = $remoteObject->save();
2089
2090 if (!$remoteResult->isSuccess())
2091 {
2092 $result->addErrors($remoteResult->getErrors());
2093 }
2094 }
2095 }
2096 }
2097
2098 // remove deleted objects from collections
2099 if ($value instanceof Collection)
2100 {
2101 $value->sysReviseDeletedObjects();
2102 }
2103 }
2104
2105 if ($saveCascade)
2106 {
2107 $this->sysSaveCurrentReferences();
2108 }
2109 }
2110
2111 public function sysSaveCurrentReferences()
2112 {
2113 foreach ($this->_currentValues as $fieldName => $value)
2114 {
2115 if ($this->entity->getField($fieldName) instanceof Reference && !empty($value))
2116 {
2117 $value->save();
2118 }
2119 }
2120 }
2121
2122 public function sysPostSave()
2123 {
2124 // clear current values
2125 foreach ($this->_currentValues as $k => $v)
2126 {
2127 $field = $this->entity->getField($k);
2128
2129 // handle references
2130 if ($v instanceof EntityObject)
2131 {
2132 // hold raw references
2133 if ($v->state === State::RAW)
2134 {
2135 continue;
2136 }
2137
2138 // move actual or changed
2139 if ($v->state === State::ACTUAL || $v->state === State::CHANGED)
2140 {
2141 $this->sysSetActual($k, $v);
2142 }
2143 }
2144 elseif ($field instanceof ScalarField || $field instanceof UserTypeField)
2145 {
2146 $v = $field->cast($v);
2147
2148 if ($v instanceof SqlExpression)
2149 {
2150 continue;
2151 }
2152
2153 $this->sysSetActual($k, $v);
2154 }
2155
2156 // clear values
2157 unset($this->_currentValues[$k]);
2158 }
2159
2160 // change state
2161 $this->sysChangeState(State::ACTUAL);
2162
2163 // return object field to current values
2164 foreach ($this->_actualValues as $fieldName => $actualValue)
2165 {
2166 if ($this->entity->getField($fieldName) instanceof ObjectField)
2167 {
2168 $this->_currentValues[$fieldName] = clone $actualValue;
2169 }
2170 }
2171 }
2172
2182 public function sysAddToCollection($fieldName, $remoteObject)
2183 {
2184 $fieldName = StringHelper::strtoupper($fieldName);
2185
2187 $field = $this->entity->getField($fieldName);
2188 $remoteObjectClass = $field->getRefEntity()->getObjectClass();
2189
2190 // validate object class
2191 if (!($remoteObject instanceof $remoteObjectClass))
2192 {
2193 throw new ArgumentException(sprintf(
2194 'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject)
2195 ));
2196 }
2197
2198 // initialize collection
2199 $collection = $this->sysGetValue($fieldName);
2200
2201 if (empty($collection))
2202 {
2203 $collection = $field->getRefEntity()->createCollection();
2204 $this->_actualValues[$fieldName] = $collection;
2205 }
2206
2207 if ($field instanceof OneToMany)
2208 {
2209 // set self to the object
2210 $remoteFieldName = $field->getRefField()->getName();
2211 $remoteObject->sysSetValue($remoteFieldName, $this);
2212
2213 // if we don't have primary right now, repeat setter later
2214 if ($this->state == State::RAW)
2215 {
2216 $localObject = $this;
2217
2218 $this->sysAddOnPrimarySetListener(function () use ($localObject, $remoteObject, $remoteFieldName) {
2219 $remoteObject->sysSetValue($remoteFieldName, $localObject);
2220 });
2221 }
2222 }
2223
2225 $collection->add($remoteObject);
2226
2227 // mark object as changed
2228 if ($this->_state == State::ACTUAL)
2229 {
2230 $this->sysChangeState(State::CHANGED);
2231 }
2232 }
2233
2243 public function sysRemoveFromCollection($fieldName, $remoteObject)
2244 {
2245 $fieldName = StringHelper::strtoupper($fieldName);
2246
2248 $field = $this->entity->getField($fieldName);
2249 $remoteObjectClass = $field->getRefEntity()->getObjectClass();
2250
2251 // validate object class
2252 if (!($remoteObject instanceof $remoteObjectClass))
2253 {
2254 throw new ArgumentException(sprintf(
2255 'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject)
2256 ));
2257 }
2258
2260 $collection = $this->sysGetValue($fieldName);
2261
2262 if (empty($collection))
2263 {
2264 $collection = $field->getRefEntity()->createCollection();
2265 $this->_actualValues[$fieldName] = $collection;
2266 }
2267
2268 // remove from collection
2269 $collection->remove($remoteObject);
2270
2271 if ($field instanceof OneToMany)
2272 {
2273 // remove self from the object
2274 if ($field->getCascadeDeletePolicy() == CascadePolicy::FOLLOW)
2275 {
2276 // nothing to do
2277 }
2278 else
2279 {
2280 // set null by default
2281 $remoteFieldName = $field->getRefField()->getName();
2282 $remoteObject->sysSetValue($remoteFieldName, null);
2283 }
2284
2285 }
2286
2287 // mark object as changed
2288 if ($this->_state == State::ACTUAL)
2289 {
2290 $this->sysChangeState(State::CHANGED);
2291 }
2292 }
2293
2302 public function sysRemoveAllFromCollection($fieldName)
2303 {
2304 $fieldName = StringHelper::strtoupper($fieldName);
2305 $collection = $this->sysFillRelationCollection($fieldName);
2306
2307 // remove one by one
2308 foreach ($collection as $remoteObject)
2309 {
2310 $this->sysRemoveFromCollection($fieldName, $remoteObject);
2311 }
2312 }
2313
2321 public function sysFillRelationCollection($field)
2322 {
2323 if ($field instanceof Relation)
2324 {
2325 $fieldName = $field->getName();
2326 }
2327 else
2328 {
2329 $fieldName = $field;
2330 $field = $this->entity->getField($fieldName);
2331 }
2332
2334 $collection = $this->sysGetValue($fieldName);
2335
2336 if (empty($collection))
2337 {
2338 $collection = $field->getRefEntity()->createCollection();
2339 $this->_actualValues[$fieldName] = $collection;
2340 }
2341
2342 if (!$collection->sysIsFilled())
2343 {
2344 // we need only primary here
2345 $remotePrimaryDefinitions = [];
2346
2347 foreach ($field->getRefEntity()->getPrimaryArray() as $primaryName)
2348 {
2349 $remotePrimaryDefinitions[] = $fieldName.'.'.$primaryName;
2350 }
2351
2352 $this->fill($remotePrimaryDefinitions);
2353
2354 // we can set fullness flag here
2355 $collection->sysSetFilled();
2356 }
2357
2358 return $collection;
2359 }
2360
2368 public static function sysMethodToFieldCase($methodName)
2369 {
2370 if (!isset(static::$_camelToSnakeCache[$methodName]))
2371 {
2372 static::$_camelToSnakeCache[$methodName] = StringHelper::strtoupper(
2373 StringHelper::camel2snake($methodName)
2374 );
2375 }
2376
2377 return static::$_camelToSnakeCache[$methodName];
2378 }
2379
2387 public static function sysFieldToMethodCase($fieldName)
2388 {
2389 if (!isset(static::$_snakeToCamelCache[$fieldName]))
2390 {
2391 static::$_snakeToCamelCache[$fieldName] = StringHelper::snake2camel($fieldName);
2392 }
2393
2394 return static::$_snakeToCamelCache[$fieldName];
2395 }
2396
2404 public static function sysSerializePrimary($primary, $entity)
2405 {
2406 if (count($entity->getPrimaryArray()) == 1)
2407 {
2408 return (string) current($primary);
2409 }
2410
2411 return (string) Json::encode(array_values($primary));
2412 }
2413
2423 public function offsetExists($offset): bool
2424 {
2425 return $this->sysHasValue($offset) && $this->sysGetValue($offset) !== null;
2426 }
2427
2437 #[\ReturnTypeWillChange]
2438 public function offsetGet($offset)
2439 {
2440 if ($this->offsetExists($offset))
2441 {
2442 // regular field
2443 return $this->get($offset);
2444 }
2445 elseif (array_key_exists($offset, $this->_runtimeValues))
2446 {
2447 // runtime field
2448 return $this->sysGetRuntime($offset);
2449 }
2450
2451 return $this->offsetExists($offset) ? $this->get($offset) : null;
2452 }
2453
2463 public function offsetSet($offset, $value): void
2464 {
2465 if (is_null($offset))
2466 {
2467 throw new ArgumentException('Field name should be set');
2468 }
2469 else
2470 {
2471 $this->set($offset, $value);
2472 }
2473 }
2474
2480 public function offsetUnset($offset): void
2481 {
2482 $this->unset($offset);
2483 }
2484}
filterValuesByMask(array $values, int $fieldsMask, bool $invertedFilter=false)
collectValues($valuesType=Values::ALL, $fieldsMask=FieldTypeMask::ALL, $recursive=false)