Bitrix-D7  20.0.0
entityobject.php
См. документацию.
1 <?php
2 /**
3  * Bitrix Framework
4  * @package bitrix
5  * @subpackage main
6  * @copyright 2001-2018 Bitrix
7  */
8 
10 
11 use ArrayAccess;
34 
35 /**
36  * Entity object
37  *
38  * @property-read \Bitrix\Main\ORM\Entity $entity
39  * @property-read array $primary
40  * @property-read int $state @see State
41  * @property-read Dictionary $customData
42  * @property Context $authContext For UF values validation
43  *
44  * @package bitrix
45  * @subpackage main
46  */
47 abstract class EntityObject implements ArrayAccess
48 {
49  /**
50  * Entity Table class. Read-only property.
51  * @var DataManager
52  */
53  static public $dataClass;
54 
55  /** @var Entity */
56  protected $_entity;
57 
58  /**
59  * @var int
60  * @see State
61  */
62  protected $_state = State::RAW;
63 
64  /**
65  * Actual values fetched from DB and collections of relations
66  * @var mixed[]|static[]|Collection[]
67  */
68  protected $_actualValues = [];
69 
70  /**
71  * Current values - new or rewritten by setter (except changed collections - they are still in actual values)
72  * @var mixed[]|static[]
73  */
74  protected $_currentValues = [];
75 
76  /**
77  * Container for non-entity data
78  * @var mixed[]
79  */
80  protected $_runtimeValues = [];
81 
82  /**
83  * @var Dictionary
84  */
85  protected $_customData = null;
86 
87  /** @var callable[] */
88  protected $_onPrimarySetListeners = [];
89 
90  /** @var Context */
91  protected $_authContext;
92 
93  /**
94  * Cache for lastName => LAST_NAME transforming
95  * @var string[]
96  */
97  static protected $_camelToSnakeCache = [];
98 
99  /**
100  * Cache for LAST_NAME => lastName transforming
101  * @var string[]
102  */
103  static protected $_snakeToCamelCache = [];
104 
105  /**
106  * EntityObject constructor
107  *
108  * @param bool|array $setDefaultValues
109  *
110  * @throws ArgumentException
111  * @throws SystemException
112  */
113  final public function __construct($setDefaultValues = true)
114  {
115  if (is_array($setDefaultValues))
116  {
117  // we have custom default values
118  foreach ($setDefaultValues as $fieldName => $defaultValue)
119  {
120  $this->set($fieldName, $defaultValue);
121  }
122  }
123 
124  if ($setDefaultValues || is_array($setDefaultValues))
125  {
126  foreach ($this->entity->getScalarFields() as $fieldName => $field)
127  {
128  if ($this->sysHasValue($fieldName))
129  {
130  // already set custom default value
131  continue;
132  }
133 
134  $defaultValue = $field->getDefaultValue($this);
135 
136  if ($defaultValue !== null)
137  {
138  $this->set($fieldName, $defaultValue);
139  }
140  }
141  }
142  }
143 
144  /**
145  * Returns all objects values as an array
146  *
147  * @param int $valuesType
148  * @param int $fieldsMask
149  *
150  * @return array
151  * @throws ArgumentException
152  * @throws SystemException
153  */
154  final public function collectValues($valuesType = Values::ALL, $fieldsMask = FieldTypeMask::ALL)
155  {
156  switch ($valuesType)
157  {
158  case Values::ACTUAL:
159  $objectValues = $this->_actualValues;
160  break;
161  case Values::CURRENT:
162  $objectValues = $this->_currentValues;
163  break;
164  default:
165  $objectValues = array_merge($this->_actualValues, $this->_currentValues);
166  }
167 
168  // filter with field mask
169  if ($fieldsMask !== FieldTypeMask::ALL)
170  {
171  foreach ($objectValues as $fieldName => $value)
172  {
173  $fieldMask = $this->entity->getField($fieldName)->getTypeMask();
174  if (!($fieldsMask & $fieldMask))
175  {
176  unset($objectValues[$fieldName]);
177  }
178  }
179  }
180 
181  // remap from uppercase to real field names
182  $values = [];
183 
184  foreach ($objectValues as $k => $v)
185  {
186  $values[$this->entity->getField($k)->getName()] = $v;
187  }
188 
189  return $values;
190  }
191 
192  /**
193  * ActiveRecord save.
194  *
195  * @return Result
196  * @throws ArgumentException
197  * @throws SystemException
198  * @throws \Exception
199  */
200  final public function save()
201  {
202  // default empty result
203  switch ($this->state)
204  {
205  case State::RAW:
206  $result = new AddResult;
207  break;
208  case State::CHANGED:
209  case State::ACTUAL:
210  $result = new UpdateResult;
211  break;
212  default:
213  $result = new Result;
214  }
215 
216  $dataClass = $this->entity->getDataClass();
217 
218  if ($this->_state == State::RAW)
219  {
220  $data = $this->_currentValues;
221  $data['__object'] = $this;
222 
223  // put secret key __object to array
224  $result = $dataClass::add($data);
225 
226  // check for error
227  if (!$result->isSuccess())
228  {
229  return $result;
230  }
231 
232  // set primary
233  foreach ($result->getPrimary() as $primaryName => $primaryValue)
234  {
235  $this->sysSetActual($primaryName, $primaryValue);
236  }
237 
238  // on primary gain event
239  $this->sysOnPrimarySet();
240  }
241  elseif ($this->_state == State::CHANGED)
242  {
243  // changed scalar and reference
244  if (!empty($this->_currentValues))
245  {
246  $data = $this->_currentValues;
247  $data['__object'] = $this;
248 
249  // put secret key __object to array
250  $result = $dataClass::update($this->primary, $data);
251 
252  // check for error
253  if (!$result->isSuccess())
254  {
255  return $result;
256  }
257  }
258  }
259 
260  // changed collections
261  $this->sysSaveRelations($result);
262 
263  // return if there were errors
264  if (!$result->isSuccess())
265  {
266  return $result;
267  }
268 
269  $this->sysPostSave();
270 
271  return $result;
272  }
273 
274  /**
275  * ActiveRecord delete.
276  *
277  * @return Result
278  * @throws ArgumentException
279  * @throws SystemException
280  */
281  final public function delete()
282  {
283  $result = new Result;
284 
285  // delete relations
286  foreach ($this->entity->getFields() as $field)
287  {
288  if ($field instanceof Reference)
289  {
290  if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW)
291  {
292  /** @var EntityObject $remoteObject */
293  $remoteObject = $this->sysGetValue($field->getName());
294  $remoteObject->delete();
295  }
296  }
297  elseif ($field instanceof OneToMany)
298  {
299  if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW)
300  {
301  // delete
302  $collection = $this->sysFillRelationCollection($field);
303 
304  foreach ($collection as $object)
305  {
306  $object->delete();
307  }
308  }
309  elseif ($field->getCascadeDeletePolicy() === CascadePolicy::SET_NULL)
310  {
311  // set null
312  $this->sysRemoveAllFromCollection($field->getName());
313  }
314  }
315  elseif ($field instanceof ManyToMany)
316  {
317  if ($field->getCascadeDeletePolicy() === CascadePolicy::FOLLOW_ORPHANS)
318  {
319  // delete
320  }
321  elseif ($field->getCascadeDeletePolicy() === CascadePolicy::SET_NULL)
322  {
323  // set null
324  }
325 
326  // always delete mediator records
327  $this->sysRemoveAllFromCollection($field->getName());
328  }
329  }
330 
331  $this->sysSaveRelations($result);
332 
333  // delete object itself
334  $dataClass = static::$dataClass;
335  $deleteResult = $dataClass::delete($this->primary);
336 
337  if (!$deleteResult->isSuccess())
338  {
339  $result->addErrors($deleteResult->getErrors());
340  }
341 
342  // clear status
343  foreach ($this->entity->getPrimaryArray()as $primaryName)
344  {
345  unset($this->_actualValues[$primaryName]);
346  }
347 
348  $this->sysChangeState(State::DELETED);
349 
350  return $result;
351  }
352 
353  /**
354  * Constructs existing object from pre-selected data, including references and relations.
355  *
356  * @param mixed $row Array of [field => value] or single scalar primary value.
357  *
358  * @return static
359  * @throws ArgumentException
360  * @throws SystemException
361  */
362  final public static function wakeUp($row)
363  {
364  /** @var static $objectClass */
365  $objectClass = get_called_class();
366 
367  /** @var \Bitrix\Main\ORM\Data\DataManager $dataClass */
368  $dataClass = static::$dataClass;
369 
370  $entity = $dataClass::getEntity();
371  $entityPrimary = $entity->getPrimaryArray();
372 
373  // normalize input data and primary
374  $primary = [];
375 
376  if (!is_array($row))
377  {
378  // it could be single primary
379  if (count($entityPrimary) == 1)
380  {
381  $primary[$entityPrimary[0]] = $row;
382  $row = [];
383  }
384  else
385  {
386  throw new ArgumentException(sprintf(
387  'Multi-primary for %s was not found', $objectClass
388  ));
389  }
390  }
391  else
392  {
393  foreach ($entityPrimary as $primaryName)
394  {
395  if (!isset($row[$primaryName]))
396  {
397  throw new ArgumentException(sprintf(
398  'Primary %s for %s was not found', $primaryName, $objectClass
399  ));
400  }
401 
402  $primary[$primaryName] = $row[$primaryName];
403  unset($row[$primaryName]);
404  }
405  }
406 
407  // create object
408  /** @var static $object */
409  $object = new $objectClass(false); // here go with false to not set default values
410  $object->sysChangeState(State::ACTUAL);
411 
412  // set primary
413  foreach ($primary as $primaryName => $primaryValue)
414  {
415  /** @var ScalarField $primaryField */
416  $primaryField = $entity->getField($primaryName);
417  $object->sysSetActual($primaryName, $primaryField->cast($primaryValue));
418  }
419 
420  // set other data
421  foreach ($row as $fieldName => $value)
422  {
423  /** @var ScalarField $primaryField */
424  $field = $entity->getField($fieldName);
425 
426  if ($field instanceof IReadable)
427  {
428  $object->sysSetActual($fieldName, $field->cast($value));
429  }
430  else
431  {
432  // we have a relation
433  if ($value instanceof static || $value instanceof Collection)
434  {
435  // it is ready data
436  $object->sysSetActual($fieldName, $value);
437  }
438  else
439  {
440  // wake up relation
441  if ($field instanceof Reference)
442  {
443  // wake up an object
444  $remoteObjectClass = $field->getRefEntity()->getObjectClass();
445  $remoteObject = $remoteObjectClass::wakeUp($value);
446 
447  $object->sysSetActual($fieldName, $remoteObject);
448  }
449  elseif ($field instanceof OneToMany || $field instanceof ManyToMany)
450  {
451  // wake up collection
452  $remoteCollectionClass = $field->getRefEntity()->getCollectionClass();
453  $remoteCollection = $remoteCollectionClass::wakeUp($value);
454 
455  $object->sysSetActual($fieldName, $remoteCollection);
456  }
457  }
458  }
459  }
460 
461  return $object;
462  }
463 
464  /**
465  * Fills all the values and relations of object
466  *
467  * @param int|string[] $fields Names of fields to fill
468  *
469  * @return mixed
470  * @throws ArgumentException
471  * @throws SystemException
472  */
473  final public function fill($fields = FieldTypeMask::ALL)
474  {
475  // object must have primary
476  $primaryFilter = Query::filter();
477 
478  foreach ($this->sysRequirePrimary() as $primaryName => $primaryValue)
479  {
480  $primaryFilter->where($primaryName, $primaryValue);
481  }
482 
483  // collect fields to be selected
484  if (is_array($fields))
485  {
486  // go through IDLE fields
487  $fieldsToSelect = $this->sysGetIdleFields($fields);
488  }
489  elseif (is_scalar($fields) && !is_numeric($fields))
490  {
491  // one custom field
492  $fields = [$fields];
493  $fieldsToSelect = $this->sysGetIdleFields($fields);
494  }
495  else
496  {
497  // get fields according to selector mask
498  $fieldsToSelect = $this->sysGetIdleFieldsByMask($fields);
499  }
500 
501  if (!empty($fieldsToSelect))
502  {
503  $fieldsToSelect = array_merge($this->entity->getPrimaryArray(), $fieldsToSelect);
504 
505  // build query
506  $dataClass = $this->entity->getDataClass();
507  $result = $dataClass::query()->setSelect($fieldsToSelect)->where($primaryFilter)->exec();
508 
509  // set object to identityMap of result, and it will be partially completed by fetch
510  $im = new IdentityMap;
511  $im->put($this);
512 
513  $result->setIdentityMap($im);
514  $result->fetchObject();
515 
516  // set filled flag to collections
517  foreach ($fieldsToSelect as $fieldName)
518  {
519  // check field before continue, it could be remote REF.ID definition so we skip it here
520  if ($this->entity->hasField($fieldName))
521  {
522  $field = $this->entity->getField($fieldName);
523 
524  if ($field instanceof OneToMany || $field instanceof ManyToMany)
525  {
526  /** @var Collection $collection */
527  $collection = $this->sysGetValue($fieldName);
528 
529  if (empty($collection))
530  {
531  $collection = $field->getRefEntity()->createCollection();
532  $this->_actualValues[$fieldName] = $collection;
533  }
534 
535  $collection->sysSetFilled();
536  }
537  }
538  }
539  }
540 
541  // return field value it it was only one
542  if (is_array($fields) && count($fields) == 1 && $this->entity->hasField(current($fields)))
543  {
544  return $this->sysGetValue(current($fields));
545  }
546 
547  return null;
548  }
549 
550  /**
551  * Fast popular alternative to __call().
552  *
553  * @return Collection|EntityObject|mixed
554  * @throws SystemException
555  */
556  public function getId()
557  {
558  if (array_key_exists('ID', $this->_currentValues))
559  {
560  return $this->_currentValues['ID'];
561  }
562  elseif (array_key_exists('ID', $this->_actualValues))
563  {
564  return $this->_actualValues['ID'];
565  }
566  elseif (!$this->entity->hasField('ID'))
567  {
568  throw new SystemException(sprintf(
569  'Unknown method `%s` for object `%s`', 'getId', get_called_class()
570  ));
571  }
572  else
573  {
574  return null;
575  }
576  }
577 
578  /**
579  * @param $fieldName
580  *
581  * @return mixed
582  * @throws ArgumentException
583  * @throws SystemException
584  */
585  final public function get($fieldName)
586  {
587  return $this->__call(__FUNCTION__, func_get_args());
588  }
589 
590  /**
591  * @param $fieldName
592  *
593  * @return mixed
594  * @throws ArgumentException
595  * @throws SystemException
596  */
597  final public function remindActual($fieldName)
598  {
599  return $this->__call(__FUNCTION__, func_get_args());
600  }
601 
602  /**
603  * @param $fieldName
604  *
605  * @return mixed
606  * @throws ArgumentException
607  * @throws SystemException
608  */
609  final public function require($fieldName)
610  {
611  return $this->__call(__FUNCTION__, func_get_args());
612  }
613 
614  /**
615  * @param $fieldName
616  * @param $value
617  *
618  * @return mixed
619  * @throws ArgumentException
620  * @throws SystemException
621  */
622  final public function set($fieldName, $value)
623  {
624  return $this->__call(__FUNCTION__, func_get_args());
625  }
626 
627  /**
628  * @param $fieldName
629  *
630  * @return mixed
631  * @throws ArgumentException
632  * @throws SystemException
633  */
634  final public function reset($fieldName)
635  {
636  return $this->__call(__FUNCTION__, func_get_args());
637  }
638 
639  /**
640  * @param $fieldName
641  *
642  * @return mixed
643  * @throws ArgumentException
644  * @throws SystemException
645  */
646  final public function unset($fieldName)
647  {
648  return $this->__call(__FUNCTION__, func_get_args());
649  }
650 
651  /**
652  * @param $fieldName
653  *
654  * @return mixed
655  * @throws ArgumentException
656  * @throws SystemException
657  */
658  final public function has($fieldName)
659  {
660  return $this->__call(__FUNCTION__, func_get_args());
661  }
662 
663  /**
664  * @param $fieldName
665  *
666  * @return mixed
667  * @throws ArgumentException
668  * @throws SystemException
669  */
670  final public function isFilled($fieldName)
671  {
672  return $this->__call(__FUNCTION__, func_get_args());
673  }
674 
675  /**
676  * @param $fieldName
677  *
678  * @return mixed
679  * @throws ArgumentException
680  * @throws SystemException
681  */
682  final public function isChanged($fieldName)
683  {
684  return $this->__call(__FUNCTION__, func_get_args());
685  }
686 
687  /**
688  * @param $fieldName
689  * @param $value
690  *
691  * @return mixed
692  * @throws ArgumentException
693  * @throws SystemException
694  */
695  final public function addTo($fieldName, $value)
696  {
697  return $this->__call(__FUNCTION__, func_get_args());
698  }
699 
700  /**
701  * @param $fieldName
702  * @param $value
703  *
704  * @return mixed
705  * @throws ArgumentException
706  * @throws SystemException
707  */
708  final public function removeFrom($fieldName, $value)
709  {
710  return $this->__call(__FUNCTION__, func_get_args());
711  }
712 
713  /**
714  * @param $fieldName
715  *
716  * @return mixed
717  * @throws ArgumentException
718  * @throws SystemException
719  */
720  final public function removeAll($fieldName)
721  {
722  return $this->__call(__FUNCTION__, func_get_args());
723  }
724 
725  final public function defineAuthContext(Context $authContext)
726  {
727  $this->_authContext = $authContext;
728  }
729 
730  /**
731  * Magic read-only properties
732  *
733  * @param $name
734  *
735  * @return mixed
736  * @throws ArgumentException
737  * @throws SystemException
738  */
739  public function __get($name)
740  {
741  switch ($name)
742  {
743  case 'entity':
744  return $this->sysGetEntity();
745  case 'primary':
746  return $this->sysGetPrimary();
747  case 'state':
748  return $this->sysGetState();
749  case 'dataClass':
750  throw new SystemException('Property `dataClass` should be received as static.');
751  case 'customData':
752 
753  if ($this->_customData === null)
754  {
755  $this->_customData = new Dictionary;
756  }
757 
758  return $this->_customData;
759 
760  case 'authContext':
761  return $this->_authContext;
762  }
763 
764  throw new SystemException(sprintf(
765  'Unknown property `%s` for object `%s`', $name, get_called_class()
766  ));
767  }
768 
769  /**
770  * Magic read-only properties
771  *
772  * @param $name
773  * @param $value
774  *
775  * @throws SystemException
776  */
777  public function __set($name, $value)
778  {
779  switch ($name)
780  {
781  case 'authContext':
782  return $this->defineAuthContext($value);
783  case 'entity':
784  case 'primary':
785  case 'dataClass':
786  case 'customData':
787  case 'state':
788  throw new SystemException(sprintf(
789  'Property `%s` for object `%s` is read-only', $name, get_called_class()
790  ));
791  }
792 
793  throw new SystemException(sprintf(
794  'Unknown property `%s` for object `%s`', $name, get_called_class()
795  ));
796  }
797 
798  /**
799  * Magic to handle getters, setters etc.
800  *
801  * @param $name
802  * @param $arguments
803  *
804  * @return mixed
805  * @throws ArgumentException
806  * @throws SystemException
807  */
808  public function __call($name, $arguments)
809  {
810  $first3 = substr($name, 0, 3);
811 
812  // regular getter
813  if ($first3 == 'get')
814  {
815  $fieldName = self::sysMethodToFieldCase(substr($name, 3));
816 
817  if (!strlen($fieldName))
818  {
819  $fieldName = StringHelper::strtoupper($arguments[0]);
820 
821  // check runtime
822  if (array_key_exists($fieldName, $this->_runtimeValues))
823  {
824  return $this->sysGetRuntime($fieldName);
825  }
826 
827  // check if custom method exists
828  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
829 
830  if (method_exists($this, $personalMethodName))
831  {
832  return $this->$personalMethodName(...array_slice($arguments, 1));
833  }
834 
835  // hard field check
836  $this->entity->getField($fieldName);
837  }
838 
839  // check if field exists
840  if ($this->entity->hasField($fieldName))
841  {
842  return $this->sysGetValue($fieldName);
843  }
844  }
845 
846  // regular setter
847  if ($first3 == 'set')
848  {
849  $fieldName = self::sysMethodToFieldCase(substr($name, 3));
850  $value = $arguments[0];
851 
852  if (!strlen($fieldName))
853  {
854  $fieldName = StringHelper::strtoupper($arguments[0]);
855  $value = $arguments[1];
856 
857  // check for runtime field
858  if (array_key_exists($fieldName, $this->_runtimeValues))
859  {
860  throw new SystemException(sprintf(
861  'Setting value for runtime field `%s` in `%s` is not allowed, it is read-only field',
862  $fieldName, get_called_class()
863  ));
864  }
865 
866  // check if custom method exists
867  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
868 
869  if (method_exists($this, $personalMethodName))
870  {
871  return $this->$personalMethodName(...array_slice($arguments, 1));
872  }
873 
874  // hard field check
875  $this->entity->getField($fieldName);
876  }
877 
878  // check if field exists
879  if ($this->entity->hasField($fieldName))
880  {
881  $field = $this->entity->getField($fieldName);
882 
883  if ($field instanceof IReadable && !($value instanceof SqlExpression))
884  {
885  $value = $field->cast($value);
886  }
887 
888  return $this->sysSetValue($fieldName, $value);
889  }
890  }
891 
892  if ($first3 == 'has')
893  {
894  $fieldName = self::sysMethodToFieldCase(substr($name, 3));
895 
896  if (!strlen($fieldName))
897  {
898  $fieldName = StringHelper::strtoupper($arguments[0]);
899 
900  // check if custom method exists
901  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
902 
903  if (method_exists($this, $personalMethodName))
904  {
905  return $this->$personalMethodName(...array_slice($arguments, 1));
906  }
907 
908  // hard field check
909  $this->entity->getField($fieldName);
910  }
911 
912  if ($this->entity->hasField($fieldName))
913  {
914  return $this->sysHasValue($fieldName);
915  }
916  }
917 
918  $first4 = substr($name, 0, 4);
919 
920  // filler
921  if ($first4 == 'fill')
922  {
923  $fieldName = self::sysMethodToFieldCase(substr($name, 4));
924 
925  // no custom/personal method for fill
926 
927  // check if field exists
928  if ($this->entity->hasField($fieldName))
929  {
930  return $this->fill([$fieldName]);
931  }
932  }
933 
934  $first5 = substr($name, 0, 5);
935 
936  // relation adder
937  if ($first5 == 'addTo')
938  {
939  $fieldName = self::sysMethodToFieldCase(substr($name, 5));
940  $value = $arguments[0];
941 
942  if (!strlen($fieldName))
943  {
944  $fieldName = StringHelper::strtoupper($arguments[0]);
945  $value = $arguments[1];
946 
947  // check if custom method exists
948  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
949 
950  if (method_exists($this, $personalMethodName))
951  {
952  return $this->$personalMethodName(...array_slice($arguments, 1));
953  }
954 
955  // hard field check
956  $this->entity->getField($fieldName);
957  }
958 
959  if ($this->entity->hasField($fieldName))
960  {
961  return $this->sysAddToCollection($fieldName, $value);
962  }
963  }
964 
965  // unsetter
966  if ($first5 == 'unset')
967  {
968  $fieldName = self::sysMethodToFieldCase(substr($name, 5));
969 
970  if (!strlen($fieldName))
971  {
972  $fieldName = StringHelper::strtoupper($arguments[0]);
973 
974  // check if custom method exists
975  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
976 
977  if (method_exists($this, $personalMethodName))
978  {
979  return $this->$personalMethodName(...array_slice($arguments, 1));
980  }
981 
982  // hard field check
983  $this->entity->getField($fieldName);
984  }
985 
986  if ($this->entity->hasField($fieldName))
987  {
988  return $this->sysUnset($fieldName);
989  }
990  }
991 
992  // resetter
993  if ($first5 == 'reset')
994  {
995  $fieldName = self::sysMethodToFieldCase(substr($name, 5));
996 
997  if (!strlen($fieldName))
998  {
999  $fieldName = StringHelper::strtoupper($arguments[0]);
1000 
1001  // check if custom method exists
1002  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1003 
1004  if (method_exists($this, $personalMethodName))
1005  {
1006  return $this->$personalMethodName(...array_slice($arguments, 1));
1007  }
1008 
1009  // hard field check
1010  $this->entity->getField($fieldName);
1011  }
1012 
1013  if ($this->entity->hasField($fieldName))
1014  {
1015  $field = $this->entity->getField($fieldName);
1016 
1017  if ($field instanceof OneToMany || $field instanceof ManyToMany)
1018  {
1019  return $this->sysResetRelation($fieldName);
1020  }
1021  else
1022  {
1023  return $this->sysReset($fieldName);
1024  }
1025  }
1026  }
1027 
1028  $first9 = substr($name, 0, 9);
1029 
1030  // relation mass remover
1031  if ($first9 == 'removeAll')
1032  {
1033  $fieldName = self::sysMethodToFieldCase(substr($name, 9));
1034 
1035  if (!strlen($fieldName))
1036  {
1037  $fieldName = StringHelper::strtoupper($arguments[0]);
1038 
1039  // check if custom method exists
1040  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1041 
1042  if (method_exists($this, $personalMethodName))
1043  {
1044  return $this->$personalMethodName(...array_slice($arguments, 1));
1045  }
1046 
1047  // hard field check
1048  $this->entity->getField($fieldName);
1049  }
1050 
1051  if ($this->entity->hasField($fieldName))
1052  {
1053  return $this->sysRemoveAllFromCollection($fieldName);
1054  }
1055  }
1056 
1057  $first10 = substr($name, 0, 10);
1058 
1059  // relation remover
1060  if ($first10 == 'removeFrom')
1061  {
1062  $fieldName = self::sysMethodToFieldCase(substr($name, 10));
1063  $value = $arguments[0];
1064 
1065  if (!strlen($fieldName))
1066  {
1067  $fieldName = StringHelper::strtoupper($arguments[0]);
1068  $value = $arguments[1];
1069 
1070  // check if custom method exists
1071  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1072 
1073  if (method_exists($this, $personalMethodName))
1074  {
1075  return $this->$personalMethodName(...array_slice($arguments, 1));
1076  }
1077 
1078  // hard field check
1079  $this->entity->getField($fieldName);
1080  }
1081 
1082  if ($this->entity->hasField($fieldName))
1083  {
1084  return $this->sysRemoveFromCollection($fieldName, $value);
1085  }
1086  }
1087 
1088  $first12 = substr($name, 0, 12);
1089 
1090  // actual value getter
1091  if ($first12 == 'remindActual')
1092  {
1093  $fieldName = self::sysMethodToFieldCase(substr($name, 12));
1094 
1095  if (!strlen($fieldName))
1096  {
1097  $fieldName = StringHelper::strtoupper($arguments[0]);
1098 
1099  // check if custom method exists
1100  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1101 
1102  if (method_exists($this, $personalMethodName))
1103  {
1104  return $this->$personalMethodName(...array_slice($arguments, 1));
1105  }
1106 
1107  // hard field check
1108  $this->entity->getField($fieldName);
1109  }
1110 
1111  // check if field exists
1112  if ($this->entity->hasField($fieldName))
1113  {
1114  return $this->_actualValues[$fieldName];
1115  }
1116  }
1117 
1118  $first7 = substr($name, 0, 7);
1119 
1120  // strict getter
1121  if ($first7 == 'require')
1122  {
1123  $fieldName = self::sysMethodToFieldCase(substr($name, 7));
1124 
1125  if (!strlen($fieldName))
1126  {
1127  $fieldName = StringHelper::strtoupper($arguments[0]);
1128 
1129  // check if custom method exists
1130  $personalMethodName = $name.static::sysFieldToMethodCase($fieldName);
1131 
1132  if (method_exists($this, $personalMethodName))
1133  {
1134  return $this->$personalMethodName(...array_slice($arguments, 1));
1135  }
1136 
1137  // hard field check
1138  $this->entity->getField($fieldName);
1139  }
1140 
1141  // check if field exists
1142  if ($this->entity->hasField($fieldName))
1143  {
1144  return $this->sysGetValue($fieldName, true);
1145  }
1146  }
1147 
1148  $first2 = substr($name, 0, 2);
1149  $last6 = substr($name, -6);
1150 
1151  // actual value checker
1152  if ($first2 == 'is' && $last6 =='Filled')
1153  {
1154  $fieldName = self::sysMethodToFieldCase(substr($name, 2, -6));
1155 
1156  if (!strlen($fieldName))
1157  {
1158  $fieldName = StringHelper::strtoupper($arguments[0]);
1159 
1160  // check if custom method exists
1161  $personalMethodName = $first2.static::sysFieldToMethodCase($fieldName).$last6;
1162 
1163  if (method_exists($this, $personalMethodName))
1164  {
1165  return $this->$personalMethodName(...array_slice($arguments, 1));
1166  }
1167 
1168  // hard field check
1169  $this->entity->getField($fieldName);
1170  }
1171 
1172  if ($this->entity->hasField($fieldName))
1173  {
1174  $field = $this->entity->getField($fieldName);
1175 
1176  if ($field instanceof OneToMany || $field instanceof ManyToMany)
1177  {
1178  return array_key_exists($fieldName, $this->_actualValues) && $this->_actualValues[$fieldName]->sysIsFilled();
1179  }
1180  else
1181  {
1182  return $this->sysIsFilled($fieldName);
1183  }
1184  }
1185  }
1186 
1187  $last7 = substr($name, -7);
1188 
1189  // runtime value checker
1190  if ($first2 == 'is' && $last7 == 'Changed')
1191  {
1192  $fieldName = self::sysMethodToFieldCase(substr($name, 2, -7));
1193 
1194  if (!strlen($fieldName))
1195  {
1196  $fieldName = StringHelper::strtoupper($arguments[0]);
1197 
1198  // check if custom method exists
1199  $personalMethodName = $first2.static::sysFieldToMethodCase($fieldName).$last7;
1200 
1201  if (method_exists($this, $personalMethodName))
1202  {
1203  return $this->$personalMethodName(...array_slice($arguments, 1));
1204  }
1205 
1206  // hard field check
1207  $this->entity->getField($fieldName);
1208  }
1209 
1210  if ($this->entity->hasField($fieldName))
1211  {
1212  $field = $this->entity->getField($fieldName);
1213 
1214  if ($field instanceof OneToMany || $field instanceof ManyToMany)
1215  {
1216  return array_key_exists($fieldName, $this->_actualValues) && $this->_actualValues[$fieldName]->sysIsChanged();
1217  }
1218  else
1219  {
1220  return $this->sysIsChanged($fieldName);
1221  }
1222  }
1223  }
1224 
1225  throw new SystemException(sprintf(
1226  'Unknown method `%s` for object `%s`', $name, get_called_class()
1227  ));
1228  }
1229 
1230  /**
1231  * @return Entity
1232  * @throws \Bitrix\Main\ArgumentException
1233  * @throws \Bitrix\Main\SystemException
1234  */
1235  public function sysGetEntity()
1236  {
1237  if ($this->_entity === null)
1238  {
1239  /** @var \Bitrix\Main\ORM\Data\DataManager $dataClass */
1240  $dataClass = static::$dataClass;
1241  $this->_entity = $dataClass::getEntity();
1242  }
1243 
1244  return $this->_entity;
1245  }
1246 
1247  /**
1248  * Returns [primary => value] array.
1249  *
1250  * @return array
1251  * @throws ArgumentException
1252  * @throws SystemException
1253  */
1254  public function sysGetPrimary()
1255  {
1256  $primaryValues = [];
1257 
1258  foreach ($this->entity->getPrimaryArray() as $primaryName)
1259  {
1260  $primaryValues[$primaryName] = $this->sysGetValue($primaryName);
1261  }
1262 
1263  return $primaryValues;
1264  }
1265 
1266  /**
1267  * Query Runtime Field values or just any runtime value getter
1268  * @internal For internal system usage only.
1269  *
1270  * @param $name
1271  *
1272  * @return mixed
1273  */
1274  public function sysGetRuntime($name)
1275  {
1276  return $this->_runtimeValues[$name];
1277  }
1278 
1279  /**
1280  * Any runtime value setter
1281  * @internal For internal system usage only.
1282  *
1283  * @param $name
1284  * @param $value
1285  *
1286  * @return $this
1287  */
1288  public function sysSetRuntime($name, $value)
1289  {
1290  $this->_runtimeValues[$name] = $value;
1291 
1292  return $this;
1293  }
1294 
1295  /**
1296  * Sets actual value.
1297  * @internal For internal system usage only.
1298  *
1299  * @param $fieldName
1300  * @param $value
1301  */
1302  public function sysSetActual($fieldName, $value)
1303  {
1304  $this->_actualValues[StringHelper::strtoupper($fieldName)] = $value;
1305  }
1306 
1307  /**
1308  * Changes state.
1309  * @see State
1310  * @internal For internal system usage only.
1311  *
1312  * @param $state
1313  */
1314  public function sysChangeState($state)
1315  {
1316  if ($this->_state !== $state)
1317  {
1318  /* not sure if we need check or changes here
1319  if ($state == State::RAW)
1320  {
1321  // actual should be empty
1322  }
1323  elseif ($state == State::ACTUAL)
1324  {
1325  // runtime values should be empty
1326  }
1327  elseif ($state == State::CHANGED)
1328  {
1329  // runtime values should not be empty
1330  }*/
1331 
1332  $this->_state = $state;
1333  }
1334 
1335  }
1336 
1337  /**
1338  * Returns current state.
1339  * @see State
1340  * @internal For internal system usage only.
1341  *
1342  * @return int
1343  */
1344  public function sysGetState()
1345  {
1346  return $this->_state;
1347  }
1348 
1349  /**
1350  * Regular getter, called by __call.
1351  * @internal For internal system usage only.
1352  *
1353  * @param string $fieldName
1354  * @param bool $require Throws an exception in the absence of value
1355  *
1356  * @return mixed
1357  * @throws SystemException
1358  */
1359  public function sysGetValue($fieldName, $require = false)
1360  {
1361  $fieldName = StringHelper::strtoupper($fieldName);
1362 
1363  if (array_key_exists($fieldName, $this->_currentValues))
1364  {
1365  return $this->_currentValues[$fieldName];
1366  }
1367  else
1368  {
1369  if ($require && !array_key_exists($fieldName, $this->_actualValues))
1370  {
1371  throw new SystemException(sprintf(
1372  '%s value is required for further operations', $fieldName
1373  ));
1374  }
1375 
1376  return isset($this->_actualValues[$fieldName])
1377  ? $this->_actualValues[$fieldName]
1378  : null;
1379  }
1380  }
1381 
1382  /**
1383  * Regular setter, called by __call. Doesn't validate values.
1384  * @internal For internal system usage only.
1385  *
1386  * @param $fieldName
1387  * @param $value
1388  *
1389  * @return $this
1390  * @throws ArgumentException
1391  * @throws SystemException
1392  */
1393  public function sysSetValue($fieldName, $value)
1394  {
1395  $fieldName = StringHelper::strtoupper($fieldName);
1396  $field = $this->entity->getField($fieldName);
1397 
1398  // system validations
1399  if ($field instanceof ScalarField)
1400  {
1401  // restrict updating primary
1402  if ($this->_state !== State::RAW && in_array($field->getName(), $this->entity->getPrimaryArray()))
1403  {
1404  throw new SystemException(sprintf(
1405  'Setting value for Primary `%s` in `%s` is not allowed, it is read-only field',
1406  $field->getName(), get_called_class()
1407  ));
1408  }
1409  }
1410 
1411  // no setter for expressions
1412  if ($field instanceof ExpressionField && !($field instanceof UserTypeField))
1413  {
1414  throw new SystemException(sprintf(
1415  'Setting value for ExpressionField `%s` in `%s` is not allowed, it is read-only field',
1416  $fieldName, get_called_class()
1417  ));
1418  }
1419 
1420  if ($field instanceof Reference)
1421  {
1422  if (!empty($value))
1423  {
1424  // validate object class and skip null
1425  $remoteObjectClass = $field->getRefEntity()->getObjectClass();
1426 
1427  if (!($value instanceof $remoteObjectClass))
1428  {
1429  throw new ArgumentException(sprintf(
1430  'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($value)
1431  ));
1432  }
1433  }
1434  }
1435 
1436  // change only if value is different from actual
1437  if (array_key_exists($fieldName, $this->_actualValues))
1438  {
1439  if ($field instanceof IReadable)
1440  {
1441  if ($field->cast($value) === $this->_actualValues[$fieldName]
1442  // double check if value objects are different, but db values are the same
1443  || $field->convertValueToDb($field->modifyValueBeforeSave($value, []))
1444  === $field->convertValueToDb($field->modifyValueBeforeSave($this->_actualValues[$fieldName], []))
1445  )
1446  {
1447  // forget previous runtime change
1448  unset($this->_currentValues[$fieldName]);
1449  return $this;
1450  }
1451  }
1452  elseif ($field instanceof Reference)
1453  {
1454  /** @var static $value */
1455  if ($value->primary === $this->_actualValues[$fieldName]->primary)
1456  {
1457  // forget previous runtime change
1458  unset($this->_currentValues[$fieldName]);
1459  return $this;
1460  }
1461  }
1462  }
1463 
1464  // set value
1465  if ($field instanceof ScalarField || $field instanceof UserTypeField)
1466  {
1467  $this->_currentValues[$fieldName] = $value;
1468  }
1469  elseif ($field instanceof Reference)
1470  {
1471  /** @var static $value */
1472  $this->_currentValues[$fieldName] = $value;
1473 
1474  // set elemental fields if there are any
1475  $elementals = $field->getElementals();
1476 
1477  if (!empty($elementals))
1478  {
1479  $elementalsChanged = false;
1480 
1481  foreach ($elementals as $localFieldName => $remoteFieldName)
1482  {
1483  // skip local primary in non-raw state
1484  if ($this->state !== State::RAW && $this->entity->getField($localFieldName)->isPrimary())
1485  {
1486  continue;
1487  }
1488 
1489  $elementalValue = empty($value) ? null : $value->sysGetValue($remoteFieldName);
1490  $this->sysSetValue($localFieldName, $elementalValue);
1491 
1492  $elementalsChanged = true;
1493  }
1494 
1495  if (!$elementalsChanged)
1496  {
1497  // object was not changed actually
1498  return $this;
1499  }
1500  }
1501  }
1502  else
1503  {
1504  throw new SystemException(sprintf(
1505  'Unknown field type `%s` in system setter of `%s`', get_class($field), get_called_class()
1506  ));
1507  }
1508 
1509  if ($this->_state == State::ACTUAL)
1510  {
1511  $this->sysChangeState(State::CHANGED);
1512  }
1513 
1514  // on primary gain event
1515  $this->sysOnPrimarySet();
1516 
1517  return $this;
1518  }
1519 
1520  /**
1521  * @internal For internal system usage only.
1522  *
1523  * @param $fieldName
1524  *
1525  * @return bool
1526  */
1527  public function sysHasValue($fieldName)
1528  {
1529  $fieldName = StringHelper::strtoupper($fieldName);
1530 
1531  return $this->sysIsFilled($fieldName) || $this->sysIsChanged($fieldName);
1532  }
1533 
1534  /**
1535  * @internal For internal system usage only.
1536  *
1537  * @param $fieldName
1538  *
1539  * @return bool
1540  */
1541  public function sysIsFilled($fieldName)
1542  {
1543  $fieldName = StringHelper::strtoupper($fieldName);
1544 
1545  return array_key_exists($fieldName, $this->_actualValues);
1546  }
1547 
1548  /**
1549  * @internal For internal system usage only.
1550  *
1551  * @param $fieldName
1552  *
1553  * @return bool
1554  */
1555  public function sysIsChanged($fieldName)
1556  {
1557  $fieldName = StringHelper::strtoupper($fieldName);
1558 
1559  return array_key_exists($fieldName, $this->_currentValues);
1560  }
1561 
1562  /**
1563  * @internal For internal system usage only.
1564  *
1565  * @return bool
1566  */
1567  public function sysHasPrimary()
1568  {
1569  foreach ($this->primary as $primaryValue)
1570  {
1571  if ($primaryValue === null)
1572  {
1573  return false;
1574  }
1575  }
1576 
1577  return true;
1578  }
1579 
1580  /**
1581  * @internal For internal system usage only.
1582  */
1583  public function sysOnPrimarySet()
1584  {
1585  // call subscribers
1586  if ($this->sysHasPrimary())
1587  {
1588  foreach ($this->_onPrimarySetListeners as $listener)
1589  {
1590  call_user_func($listener, $this);
1591  }
1592  }
1593  }
1594 
1595  /**
1596  * @internal For internal system usage only.
1597  *
1598  * @param callable $callback
1599  */
1600  public function sysAddOnPrimarySetListener($callback)
1601  {
1602  // add to listeners
1603  $this->_onPrimarySetListeners[] = $callback;
1604  }
1605 
1606  /**
1607  * @internal For internal system usage only.
1608  *
1609  * @param $fieldName
1610  *
1611  * @return $this
1612  */
1613  public function sysUnset($fieldName)
1614  {
1615  $fieldName = StringHelper::strtoupper($fieldName);
1616 
1617  unset($this->_currentValues[$fieldName]);
1618  unset($this->_actualValues[$fieldName]);
1619 
1620  return $this;
1621  }
1622 
1623  /**
1624  * @internal For internal system usage only.
1625  *
1626  * @param $fieldName
1627  *
1628  * @return $this
1629  */
1630  public function sysReset($fieldName)
1631  {
1632  $fieldName = StringHelper::strtoupper($fieldName);
1633 
1634  unset($this->_currentValues[$fieldName]);
1635 
1636  return $this;
1637  }
1638 
1639  /**
1640  * @internal For internal system usage only.
1641  *
1642  * @param $fieldName
1643  *
1644  * @return $this
1645  */
1646  public function sysResetRelation($fieldName)
1647  {
1648  $fieldName = StringHelper::strtoupper($fieldName);
1649 
1650  if (isset($this->_actualValues[$fieldName]))
1651  {
1652  /** @var Collection $collection */
1653  $collection = $this->_actualValues[$fieldName];
1654  $collection->sysResetChanges(true);
1655  }
1656 
1657  return $this;
1658  }
1659 
1660  /**
1661  * @internal For internal system usage only.
1662  *
1663  * @return array
1664  * @throws ArgumentException
1665  * @throws SystemException
1666  */
1667  public function sysRequirePrimary()
1668  {
1669  $primaryValues = [];
1670 
1671  foreach ($this->entity->getPrimaryArray() as $primaryName)
1672  {
1673  try
1674  {
1675  $primaryValues[$primaryName] = $this->sysGetValue($primaryName, true);
1676  }
1677  catch (SystemException $e)
1678  {
1679  throw new SystemException(sprintf(
1680  'Primary `%s` value is required for further operations', $primaryName
1681  ));
1682  }
1683  }
1684 
1685  return $primaryValues;
1686  }
1687 
1688  /**
1689  * @internal For internal system usage only.
1690  *
1691  * Returns non-filled field names according to array of $fields
1692  *
1693  * @param array $fields
1694  *
1695  * @return array
1696  */
1697  public function sysGetIdleFields($fields = [])
1698  {
1699  $list = [];
1700 
1701  if (empty($fields))
1702  {
1703  // all fields by default
1704  $fields = array_keys($this->entity->getFields());
1705  }
1706 
1707  foreach ($fields as $fieldName)
1708  {
1709  $fieldName = StringHelper::strtoupper($fieldName);
1710 
1711  if (!isset($this->_actualValues[$fieldName]))
1712  {
1713  // regular field
1714  $list[] = $fieldName;
1715  }
1716  elseif ($this->_actualValues[$fieldName] instanceof Collection && !$this->_actualValues[$fieldName]->sysIsFilled())
1717  {
1718  // non-filled collection
1719  $list[] = $fieldName;
1720  }
1721  }
1722 
1723  return $list;
1724  }
1725 
1726  /**
1727  * @internal For internal system usage only.
1728  *
1729  * Returns non-filled field names according to $mask
1730  *
1731  * @param int $mask
1732  *
1733  * @return array
1734  */
1735  public function sysGetIdleFieldsByMask($mask = FieldTypeMask::ALL)
1736  {
1737  $list = [];
1738 
1739  foreach ($this->entity->getFields() as $field)
1740  {
1741  $fieldMask = $field->getTypeMask();
1742 
1743  if (!isset($this->_actualValues[StringHelper::strtoupper($field->getName())])
1744  && ($mask & $fieldMask)
1745  )
1746  {
1747  $list[] = $field->getName();
1748  }
1749  }
1750 
1751  return $list;
1752  }
1753 
1754  /**
1755  * @internal For internal system usage only.
1756  *
1757  * @param Result $result
1758  *
1759  * @throws ArgumentException
1760  * @throws SystemException
1761  */
1762  public function sysSaveRelations(Result $result)
1763  {
1764  $saveCascade = true;
1765 
1766  foreach ($this->_actualValues as $fieldName => $value)
1767  {
1768  $field = $this->entity->getField($fieldName);
1769 
1770  if ($field instanceof Reference)
1771  {
1772  if ($saveCascade)
1773  {
1774  $value->save();
1775  }
1776  }
1777  elseif ($field instanceof OneToMany)
1778  {
1779  $collection = $value;
1780 
1781  /** @var static[] $objectsToSave */
1782  $objectsToSave = [];
1783 
1784  /** @var static[] $objectsToDelete */
1785  $objectsToDelete = [];
1786 
1787  if ($collection->sysIsChanged())
1788  {
1789  // save changed elements of collection
1790  foreach ($collection->sysGetChanges() as $change)
1791  {
1792  list($remoteObject, $changeType) = $change;
1793 
1794  if ($changeType == Collection::OBJECT_ADDED)
1795  {
1796  $objectsToSave[] = $remoteObject;
1797  }
1798  elseif ($changeType == Collection::OBJECT_REMOVED)
1799  {
1800  if ($field->getCascadeDeletePolicy() == CascadePolicy::FOLLOW)
1801  {
1802  $objectsToDelete[] = $remoteObject;
1803  }
1804  else
1805  {
1806  // set null by default
1807  $objectsToSave[] = $remoteObject;
1808  }
1809  }
1810  }
1811  }
1812 
1813  if ($saveCascade)
1814  {
1815  // everything should be saved, except deleted
1816  foreach ($collection->getAll() as $remoteObject)
1817  {
1818  if (!in_array($remoteObject, $objectsToDelete) && !in_array($remoteObject, $objectsToSave))
1819  {
1820  $objectsToSave[] = $remoteObject;
1821  }
1822  }
1823  }
1824 
1825  // save remote objects
1826  foreach ($objectsToSave as $remoteObject)
1827  {
1828  $remoteResult = $remoteObject->save();
1829 
1830  if (!$remoteResult->isSuccess())
1831  {
1832  $result->addErrors($remoteResult->getErrors());
1833  }
1834  }
1835 
1836  // delete remote objects
1837  foreach ($objectsToDelete as $remoteObject)
1838  {
1839  $remoteResult = $remoteObject->delete();
1840 
1841  if (!$remoteResult->isSuccess())
1842  {
1843  $result->addErrors($remoteResult->getErrors());
1844  }
1845  }
1846 
1847  // forget collection changes
1848  if ($collection->sysIsChanged())
1849  {
1850  $collection->sysResetChanges();
1851  }
1852  }
1853  elseif ($field instanceof ManyToMany)
1854  {
1855  $collection = $value;
1856 
1857  if ($value->sysIsChanged())
1858  {
1859  foreach ($collection->sysGetChanges() as $change)
1860  {
1861  list($remoteObject, $changeType) = $change;
1862 
1863  // initialize mediator object
1864  $mediatorObjectClass = $field->getMediatorEntity()->getObjectClass();
1865  $localReferenceName = $field->getLocalReferenceName();
1866  $remoteReferenceName = $field->getRemoteReferenceName();
1867 
1868  /** @var static $mediatorObject */
1869  $mediatorObject = new $mediatorObjectClass;
1870  $mediatorObject->sysSetValue($localReferenceName, $this);
1871  $mediatorObject->sysSetValue($remoteReferenceName, $remoteObject);
1872 
1873  // add or remove mediator depending on changeType
1874  if ($changeType == Collection::OBJECT_ADDED)
1875  {
1876  $mediatorObject->save();
1877  }
1878  elseif ($changeType == Collection::OBJECT_REMOVED)
1879  {
1880  // destroy directly through data class
1881  $mediatorDataClass = $field->getMediatorEntity()->getDataClass();
1882  $mediatorDataClass::delete($mediatorObject->primary);
1883  }
1884  }
1885 
1886  // forget collection changes
1887  $collection->sysResetChanges();
1888  }
1889 
1890  // should everything be saved?
1891  if ($saveCascade)
1892  {
1893  foreach ($collection->getAll() as $remoteObject)
1894  {
1895  $remoteResult = $remoteObject->save();
1896 
1897  if (!$remoteResult->isSuccess())
1898  {
1899  $result->addErrors($remoteResult->getErrors());
1900  }
1901  }
1902  }
1903  }
1904 
1905  // remove deleted objects from collections
1906  if ($value instanceof Collection)
1907  {
1908  $value->sysReviseDeletedObjects();
1909  }
1910  }
1911  }
1912 
1913  public function sysPostSave()
1914  {
1915  // clear current values
1916  foreach ($this->_currentValues as $k => $v)
1917  {
1918  $field = $this->entity->getField($k);
1919 
1920  // handle references
1921  if ($v instanceof EntityObject)
1922  {
1923  // hold raw references
1924  if ($v->state === State::RAW)
1925  {
1926  continue;
1927  }
1928 
1929  // move actual or changed
1930  if ($v->state === State::ACTUAL || $v->state === State::CHANGED)
1931  {
1932  $this->sysSetActual($k, $v);
1933  }
1934  }
1935  elseif ($field instanceof ScalarField)
1936  {
1937  $v = $field->cast($v);
1938  $this->sysSetActual($k, $v);
1939  }
1940 
1941  // clear values
1942  unset($this->_currentValues[$k]);
1943  }
1944 
1945  // change state
1946  $this->sysChangeState(State::ACTUAL);
1947  }
1948 
1949  /**
1950  * @internal For internal system usage only.
1951  *
1952  * @param $fieldName
1953  * @param $remoteObject
1954  *
1955  * @throws ArgumentException
1956  * @throws SystemException
1957  */
1958  public function sysAddToCollection($fieldName, $remoteObject)
1959  {
1960  $fieldName = StringHelper::strtoupper($fieldName);
1961 
1962  /** @var OneToMany $field */
1963  $field = $this->entity->getField($fieldName);
1964  $remoteObjectClass = $field->getRefEntity()->getObjectClass();
1965 
1966  // validate object class
1967  if (!($remoteObject instanceof $remoteObjectClass))
1968  {
1969  throw new ArgumentException(sprintf(
1970  'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject)
1971  ));
1972  }
1973 
1974  // initialize collection
1975  $collection = $this->sysGetValue($fieldName);
1976 
1977  if (empty($collection))
1978  {
1979  $collection = $field->getRefEntity()->createCollection();
1980  $this->_actualValues[$fieldName] = $collection;
1981  }
1982 
1983  /** @var Collection $collection Add to collection */
1984  $collection->add($remoteObject);
1985 
1986  if ($field instanceof OneToMany)
1987  {
1988  // set self to the object
1989  $remoteFieldName = $field->getRefField()->getName();
1990  $remoteObject->sysSetValue($remoteFieldName, $this);
1991 
1992  // if we don't have primary right now, repeat setter later
1993  if ($this->state == State::RAW)
1994  {
1995  $localObject = $this;
1996 
1997  $this->sysAddOnPrimarySetListener(function () use ($localObject, $remoteObject, $remoteFieldName) {
1998  $remoteObject->sysSetValue($remoteFieldName, $localObject);
1999  });
2000  }
2001  }
2002 
2003  // mark object as changed
2004  if ($this->_state == State::ACTUAL)
2005  {
2006  $this->sysChangeState(State::CHANGED);
2007  }
2008  }
2009 
2010  /**
2011  * @internal For internal system usage only.
2012  *
2013  * @param $fieldName
2014  * @param $remoteObject
2015  *
2016  * @throws ArgumentException
2017  * @throws SystemException
2018  */
2019  public function sysRemoveFromCollection($fieldName, $remoteObject)
2020  {
2021  $fieldName = StringHelper::strtoupper($fieldName);
2022 
2023  /** @var OneToMany $field */
2024  $field = $this->entity->getField($fieldName);
2025  $remoteObjectClass = $field->getRefEntity()->getObjectClass();
2026 
2027  // validate object class
2028  if (!($remoteObject instanceof $remoteObjectClass))
2029  {
2030  throw new ArgumentException(sprintf(
2031  'Expected instance of `%s`, got `%s` instead', $remoteObjectClass, get_class($remoteObject)
2032  ));
2033  }
2034 
2035  /** @var Collection $collection Initialize collection */
2036  $collection = $this->sysGetValue($fieldName);
2037 
2038  if (empty($collection))
2039  {
2040  $collection = $field->getRefEntity()->createCollection();
2041  $this->_actualValues[$fieldName] = $collection;
2042  }
2043 
2044  // remove from collection
2045  $collection->remove($remoteObject);
2046 
2047  if ($field instanceof OneToMany)
2048  {
2049  // remove self from the object
2050  if ($field->getCascadeDeletePolicy() == CascadePolicy::FOLLOW)
2051  {
2052  // nothing to do
2053  }
2054  else
2055  {
2056  // set null by default
2057  $remoteFieldName = $field->getRefField()->getName();
2058  $remoteObject->sysSetValue($remoteFieldName, null);
2059  }
2060 
2061  }
2062 
2063  // mark object as changed
2064  if ($this->_state == State::ACTUAL)
2065  {
2066  $this->sysChangeState(State::CHANGED);
2067  }
2068  }
2069 
2070  /**
2071  * @internal For internal system usage only.
2072  *
2073  * @param $fieldName
2074  *
2075  * @throws ArgumentException
2076  * @throws SystemException
2077  */
2078  public function sysRemoveAllFromCollection($fieldName)
2079  {
2080  $fieldName = StringHelper::strtoupper($fieldName);
2081  $collection = $this->sysFillRelationCollection($fieldName);
2082 
2083  // remove one by one
2084  foreach ($collection as $remoteObject)
2085  {
2086  $this->sysRemoveFromCollection($fieldName, $remoteObject);
2087  }
2088  }
2089 
2090  /**
2091  * @param OneToMany|ManyToMany|string $field
2092  *
2093  * @return Collection
2094  * @throws ArgumentException
2095  * @throws SystemException
2096  */
2097  public function sysFillRelationCollection($field)
2098  {
2099  if ($field instanceof Relation)
2100  {
2101  $fieldName = $field->getName();
2102  }
2103  else
2104  {
2105  $fieldName = $field;
2106  $field = $this->entity->getField($fieldName);
2107  }
2108 
2109  /** @var Collection $collection initialize collection */
2110  $collection = $this->sysGetValue($fieldName);
2111 
2112  if (empty($collection))
2113  {
2114  $collection = $field->getRefEntity()->createCollection();
2115  $this->_actualValues[$fieldName] = $collection;
2116  }
2117 
2118  if (!$collection->sysIsFilled())
2119  {
2120  // we need only primary here
2121  $remotePrimaryDefinitions = [];
2122 
2123  foreach ($field->getRefEntity()->getPrimaryArray() as $primaryName)
2124  {
2125  $remotePrimaryDefinitions[] = $fieldName.'.'.$primaryName;
2126  }
2127 
2128  $this->fill($remotePrimaryDefinitions);
2129 
2130  // we can set fullness flag here
2131  $collection->sysSetFilled();
2132  }
2133 
2134  return $collection;
2135  }
2136 
2137  /**
2138  * @internal For internal system usage only.
2139  *
2140  * @param $methodName
2141  *
2142  * @return string
2143  */
2144  public static function sysMethodToFieldCase($methodName)
2145  {
2146  if (!isset(static::$_camelToSnakeCache[$methodName]))
2147  {
2148  static::$_camelToSnakeCache[$methodName] = StringHelper::strtoupper(
2149  StringHelper::camel2snake($methodName)
2150  );
2151  }
2152 
2153  return static::$_camelToSnakeCache[$methodName];
2154  }
2155 
2156  /**
2157  * @internal For internal system usage only.
2158  *
2159  * @param $fieldName
2160  *
2161  * @return string
2162  */
2163  public static function sysFieldToMethodCase($fieldName)
2164  {
2165  if (!isset(static::$_snakeToCamelCache[$fieldName]))
2166  {
2167  static::$_snakeToCamelCache[$fieldName] = StringHelper::snake2camel($fieldName);
2168  }
2169 
2170  return static::$_snakeToCamelCache[$fieldName];
2171  }
2172 
2173  /**
2174  * ArrayAccess interface implementation.
2175  *
2176  * @param mixed $offset
2177  *
2178  * @return bool
2179  * @throws ArgumentException
2180  * @throws SystemException
2181  */
2182  public function offsetExists($offset)
2183  {
2184  return $this->sysHasValue($offset) && $this->sysGetValue($offset) !== null;
2185  }
2186 
2187  /**
2188  * ArrayAccess interface implementation.
2189  *
2190  * @param mixed $offset
2191  *
2192  * @return mixed|null
2193  * @throws ArgumentException
2194  * @throws SystemException
2195  */
2196  public function offsetGet($offset)
2197  {
2198  if ($this->offsetExists($offset))
2199  {
2200  // regular field
2201  return $this->get($offset);
2202  }
2203  elseif (array_key_exists($offset, $this->_runtimeValues))
2204  {
2205  // runtime field
2206  return $this->sysGetRuntime($offset);
2207  }
2208 
2209  return $this->offsetExists($offset) ? $this->get($offset) : null;
2210  }
2211 
2212  /**
2213  * ArrayAccess interface implementation.
2214  *
2215  * @param mixed $offset
2216  * @param mixed $value
2217  *
2218  * @throws ArgumentException
2219  * @throws SystemException
2220  */
2221  public function offsetSet($offset, $value)
2222  {
2223  if (is_null($offset))
2224  {
2225  throw new ArgumentException('Field name should be set');
2226  }
2227  else
2228  {
2229  $this->set($offset, $value);
2230  }
2231  }
2232 
2233  /**
2234  * ArrayAccess interface implementation.
2235  *
2236  * @param mixed $offset
2237  */
2238  public function offsetUnset($offset)
2239  {
2240  $this->unset($offset);
2241  }
2242 }
Bitrix\Main\ORM\Objectify\EntityObject\remindActual
remindActual($fieldName)
Definition: entityobject.php:597
Bitrix\Main\ORM\Objectify\EntityObject\$_snakeToCamelCache
static $_snakeToCamelCache
Definition: entityobject.php:103
Bitrix\Main\ORM\Data\DataManager
Base entity data manager.
Definition: main/lib/orm/data/datamanager.php:32
Bitrix\Main\Text\StringHelper
Definition: stringhelper.php:16
Bitrix\Main\ORM\Fields\Relations\CascadePolicy\SET_NULL
const SET_NULL
Definition: cascadepolicy.php:19
Bitrix\Main\ORM\Data\UpdateResult
Definition: updateresult.php:13
Bitrix\Main\ORM\Objectify\EntityObject\wakeUp
static wakeUp($row)
Constructs existing object from pre-selected data, including references and relations.
Definition: entityobject.php:362
Bitrix\Main\ORM\Objectify\EntityObject\$_authContext
$_authContext
Definition: entityobject.php:91
Bitrix\Main\ORM\Fields\Relations\Reference
Definition: reference.php:25
Bitrix\Main\ORM\Objectify\EntityObject\$_state
$_state
Definition: entityobject.php:62
Bitrix\Main\ORM\Fields\IReadable
Definition: ireadable.php:15
Bitrix\Main\ORM\Fields\FieldTypeMask\ALL
const ALL
Definition: fieldtypemask.php:27
Bitrix\Main\ORM\Objectify\EntityObject\$_onPrimarySetListeners
$_onPrimarySetListeners
Definition: entityobject.php:88
Bitrix\Main\ORM\Objectify\EntityObject\getId
getId()
Fast popular alternative to __call().
Definition: entityobject.php:556
Bitrix\Main\ORM\Objectify\EntityObject\$_currentValues
$_currentValues
Definition: entityobject.php:74
Bitrix\Main\ORM\Objectify\Collection\OBJECT_REMOVED
const OBJECT_REMOVED
Definition: main/lib/orm/objectify/collection.php:68
Bitrix\Main\Type\Dictionary
Definition: main/lib/type/dictionary.php:4
Bitrix\Main\ORM\Objectify\EntityObject\$_entity
$_entity
Definition: entityobject.php:56
Bitrix\Main\ORM\Objectify\State\RAW
const RAW
Definition: main/lib/orm/objectify/state.php:18
Bitrix\Main\ORM\Objectify\State\CHANGED
const CHANGED
Definition: main/lib/orm/objectify/state.php:20
Bitrix\Main\ORM\Fields\UserTypeField
Definition: usertypefield.php:17
Bitrix\Main\ORM\Objectify\EntityObject\collectValues
collectValues($valuesType=Values::ALL, $fieldsMask=FieldTypeMask::ALL)
Returns all objects values as an array.
Definition: entityobject.php:154
Bitrix\Main\ORM\Objectify\EntityObject
Definition: entityobject.php:47
Bitrix\Main\ORM\Objectify\EntityObject\fill
fill($fields=FieldTypeMask::ALL)
Fills all the values and relations of object.
Definition: entityobject.php:473
Bitrix\Main\ORM\Objectify\EntityObject\$_camelToSnakeCache
static $_camelToSnakeCache
Definition: entityobject.php:97
Bitrix\Main\ORM\Objectify\Collection\OBJECT_ADDED
const OBJECT_ADDED
Definition: main/lib/orm/objectify/collection.php:65
Bitrix\Main\ORM\Objectify\Collection
Definition: main/lib/orm/objectify/collection.php:32
Bitrix\Main\Text\StringHelper\camel2snake
static camel2snake($str)
Changes registry from CamelCase to snake_case.
Definition: stringhelper.php:44
Bitrix\Main\ORM\Fields\ScalarField
Definition: scalarfield.php:18
Bitrix\Main\DB\SqlExpression
Definition: sqlexpression.php:18
Bitrix\Main\ORM\Fields\ExpressionField
Definition: expressionfield.php:23
Bitrix\Main\ORM\Objectify\EntityObject\save
save()
ActiveRecord save.
Definition: entityobject.php:200
Bitrix\Main\ORM\Objectify\IdentityMap
Definition: identitymap.php:19
Bitrix\Main\ORM\Entity
Base entity.
Definition: main/lib/orm/entity.php:25
Bitrix\Main\ORM\Query\Query\filter
static filter()
Returns new instance of Filter.
Definition: main/lib/orm/query/query.php:756
Bitrix\Main\ORM\Objectify\EntityObject\$dataClass
static $dataClass
Definition: entityobject.php:53
Bitrix\Main\ORM\Objectify\EntityObject\__construct
__construct($setDefaultValues=true)
EntityObject constructor.
Definition: entityobject.php:113
Bitrix\Main\ArgumentException
Exception is thrown when function argument is not valid.
Definition: main/lib/exception.php:33
Bitrix\Main\ORM\Fields\Relations\ManyToMany
Definition: manytomany.php:25
Bitrix\Main\ORM\Objectify\EntityObject\$_actualValues
$_actualValues
Definition: entityobject.php:68
Bitrix\Main\ORM\Objectify\EntityObject\$_customData
$_customData
Definition: entityobject.php:85
Bitrix\Main\ORM\Query\Query
Definition: main/lib/orm/query/query.php:112
Bitrix\Main\SystemException
Base class for fatal exceptions.
Definition: main/lib/exception.php:7
Bitrix\Main\ORM\Objectify\State\ACTUAL
const ACTUAL
Definition: main/lib/orm/objectify/state.php:19
Bitrix\Main\ORM\Objectify\Values\CURRENT
const CURRENT
Definition: values.php:19
Bitrix\Main\ORM\Fields\Relations\CascadePolicy\FOLLOW
const FOLLOW
Definition: cascadepolicy.php:21
Bitrix\Main\Text\StringHelper\strtoupper
static strtoupper($str)
Regular uppercase with result cache.
Definition: stringhelper.php:25
Bitrix\Main\ORM\Objectify
Definition: main/lib/orm/objectify/collection.php:9
Bitrix\Main\ORM\Fields\FieldTypeMask
Definition: fieldtypemask.php:15
Bitrix\Main\ORM\Objectify\IdentityMap\put
put($object)
Definition: identitymap.php:45
Bitrix\Main\ORM\Fields\Relations\CascadePolicy\FOLLOW_ORPHANS
const FOLLOW_ORPHANS
Definition: cascadepolicy.php:23
Bitrix\Main\ORM\Objectify\EntityObject\$_runtimeValues
$_runtimeValues
Definition: entityobject.php:80
Bitrix\Main\ORM\Fields\Relations\Relation
Definition: main/lib/orm/fields/relations/relation.php:25
Bitrix\Main\ORM\Fields\Relations\OneToMany
Definition: onetomany.php:22
Bitrix\Main\ORM\Objectify\State\DELETED
const DELETED
Definition: main/lib/orm/objectify/state.php:21
Bitrix\Main\ORM\Objectify\Values\ALL
const ALL
Definition: values.php:20
Bitrix\Main\ORM\Data\Result
Definition: main/lib/orm/data/result.php:15
Bitrix\Main\ORM\Objectify\Values\ACTUAL
const ACTUAL
Definition: values.php:18
Bitrix\Main\ORM\Fields\Relations\CascadePolicy
Definition: cascadepolicy.php:15
Bitrix\Main\Authentication\Context
Definition: main/lib/authentication/context.php:10
Bitrix\Main\ORM\Data\AddResult
Definition: addresult.php:11
Bitrix\Main\Text\StringHelper\snake2camel
static snake2camel($str)
Changes registry from snake_case or SNAKE_CASE to CamelCase.
Definition: stringhelper.php:56