Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
collection.php
1<?php
10
23
32abstract class Collection implements \ArrayAccess, \Iterator, \Countable
33{
38 static public $dataClass;
39
41 protected $_entity;
42
44 protected $_objectClass;
45
47 protected $_objects = [];
48
50 protected $_isFilled = false;
51
54
57
60
63
65 const OBJECT_ADDED = 1;
66
68 const OBJECT_REMOVED = 2;
69
78 final public function __construct(Entity $entity = null)
79 {
80 if (empty($entity))
81 {
82 if (__CLASS__ !== get_called_class())
83 {
84 // custom collection class
85 $dataClass = static::$dataClass;
86 $this->_entity = $dataClass::getEntity();
87 }
88 else
89 {
90 throw new ArgumentException('Entity required when constructing collection');
91 }
92 }
93 else
94 {
95 $this->_entity = $entity;
96 }
97
98 $this->_objectClass = $this->_entity->getObjectClass();
99 $this->_isSinglePrimary = count($this->_entity->getPrimaryArray()) == 1;
100 }
101
102 public function __clone()
103 {
104 $this->_objects = \Bitrix\Main\Type\Collection::clone((array)$this->_objects);
105 $this->_objectsRemoved = \Bitrix\Main\Type\Collection::clone((array)$this->_objectsRemoved);
106 $this->_iterableObjects = null;
107 }
108
115 final public function add(EntityObject $object)
116 {
117 // check object class
118 if (!($object instanceof $this->_objectClass))
119 {
120 throw new ArgumentException(sprintf(
121 'Invalid object class %s for %s collection, expected "%s".',
122 get_class($object), get_class($this), $this->_objectClass
123 ));
124 }
125
126 $srPrimary = $this->sysGetPrimaryKey($object);
127
128 if (!$object->sysHasPrimary())
129 {
130 // object is new and there is no primary yet
131 $object->sysAddOnPrimarySetListener([$this, 'sysOnObjectPrimarySet']);
132 }
133
134 if (empty($this->_objects[$srPrimary])
135 && (!isset($this->_objectsChanges[$srPrimary]) || $this->_objectsChanges[$srPrimary] != static::OBJECT_REMOVED))
136 {
137 $this->_objects[$srPrimary] = $object;
138 $this->_objectsChanges[$srPrimary] = static::OBJECT_ADDED;
139 }
140 elseif (isset($this->_objectsChanges[$srPrimary]) && $this->_objectsChanges[$srPrimary] == static::OBJECT_REMOVED)
141 {
142 // silent add for removed runtime
143 $this->_objects[$srPrimary] = $object;
144
145 unset($this->_objectsChanges[$srPrimary]);
146 unset($this->_objectsRemoved[$srPrimary]);
147 }
148 }
149
157 final public function has(EntityObject $object)
158 {
159 // check object class
160 if (!($object instanceof $this->_objectClass))
161 {
162 throw new ArgumentException(sprintf(
163 'Invalid object class %s for %s collection, expected "%s".',
164 get_class($object), get_class($this), $this->_objectClass
165 ));
166 }
167
168 return array_key_exists($this->sysGetPrimaryKey($object), $this->_objects);
169 }
170
177 final public function hasByPrimary($primary)
178 {
179 $normalizedPrimary = $this->sysNormalizePrimary($primary);
180 return array_key_exists($this->sysSerializePrimaryKey($normalizedPrimary), $this->_objects);
181 }
182
189 final public function getByPrimary($primary)
190 {
191 $normalizedPrimary = $this->sysNormalizePrimary($primary);
192 $serializePrimaryKey = $this->sysSerializePrimaryKey($normalizedPrimary);
193
194 if (isset($this->_objects[$serializePrimaryKey]))
195 {
196 return $this->_objects[$serializePrimaryKey];
197 }
198
199 return null;
200 }
201
205 final public function getAll()
206 {
207 return array_values($this->_objects);
208 }
209
217 final public function remove(EntityObject $object)
218 {
219 // check object class
220 if (!($object instanceof $this->_objectClass))
221 {
222 throw new ArgumentException(sprintf(
223 'Invalid object class %s for %s collection, expected "%s".',
224 get_class($object), get_class($this), $this->_objectClass
225 ));
226 }
227
228 // ignore deleted objects
229 if ($object->state === State::DELETED)
230 {
231 return;
232 }
233
234 $srPrimary = $this->sysGetPrimaryKey($object);
235 $this->sysRemove($srPrimary);
236 }
237
243 final public function removeByPrimary($primary)
244 {
245 $normalizedPrimary = $this->sysNormalizePrimary($primary);
246 $srPrimary = $this->sysSerializePrimaryKey($normalizedPrimary);
247
248 $this->sysRemove($srPrimary);
249 }
250
251 public function sysRemove($srPrimary)
252 {
253 $object = $this->_objects[$srPrimary];
254
255 if (empty($object))
256 {
257 $object = $this->entity->wakeUpObject($srPrimary);
258 }
259
260 unset($this->_objects[$srPrimary]);
261
262 if (!isset($this->_objectsChanges[$srPrimary]) || $this->_objectsChanges[$srPrimary] != static::OBJECT_ADDED)
263 {
264 // regular remove
265 $this->_objectsChanges[$srPrimary] = static::OBJECT_REMOVED;
266 $this->_objectsRemoved[$srPrimary] = $object;
267 }
268 elseif (isset($this->_objectsChanges[$srPrimary]) && $this->_objectsChanges[$srPrimary] == static::OBJECT_ADDED)
269 {
270 // silent remove for added runtime
271 unset($this->_objectsChanges[$srPrimary]);
272 unset($this->_objectsRemoved[$srPrimary]);
273 }
274 }
275
285 final public function fill($fields = FieldTypeMask::ALL)
286 {
287 $entityPrimary = $this->_entity->getPrimaryArray();
288
289 $primaryValues = [];
290 $fieldsToSelect = [];
291
292 // if field is the only one
293 if (is_scalar($fields) && !is_numeric($fields))
294 {
295 $fields = [$fields];
296 }
297
298 // collect custom fields to select
299 foreach ($this->_objects as $object)
300 {
301 $idleFields = is_array($fields)
302 ? $object->sysGetIdleFields($fields)
303 : $object->sysGetIdleFieldsByMask($fields);
304
305 if (!empty($idleFields))
306 {
307 $fieldsToSelect = array_unique(array_merge($fieldsToSelect, $idleFields));
308
309 // add object to query
310 $objectPrimary = $object->sysRequirePrimary();
311
312 $primaryValues[] = count($objectPrimary) == 1
313 ? current($objectPrimary)
314 : $objectPrimary;
315 }
316 }
317
318 // add primary to select
319 if (!empty($fieldsToSelect))
320 {
321 $fieldsToSelect = array_unique(array_merge($entityPrimary, $fieldsToSelect));
322
323 // build primary filter
324 $primaryFilter = Query::filter();
325
326 if (count($entityPrimary) == 1)
327 {
328 // IN for single-primary objects
329 $primaryFilter->whereIn($entityPrimary[0], $primaryValues);
330 }
331 else
332 {
333 // OR for multi-primary objects
334 $primaryFilter->logic('or');
335
336 foreach ($primaryValues as $objectPrimary)
337 {
338 // add each object as a separate condition
339 $oneObjectFilter = Query::filter();
340
341 foreach ($objectPrimary as $primaryName => $primaryValue)
342 {
343 $oneObjectFilter->where($primaryName, $primaryValue);
344 }
345
346 $primaryFilter->where($oneObjectFilter);
347 }
348 }
349
350 // build query
351 $dataClass = $this->_entity->getDataClass();
352 $result = $dataClass::query()->setSelect($fieldsToSelect)->where($primaryFilter)->exec();
353
354 // set object to identityMap of result, and it will be partially completed by fetch
355 $im = new IdentityMap;
356
357 foreach ($this->_objects as $object)
358 {
359 $im->put($object);
360 }
361
362 $result->setIdentityMap($im);
363 $result->fetchCollection();
364 }
365
366 // return field value it it was only one
367 if (is_array($fields) && count($fields) == 1 && $this->entity->hasField(current($fields)))
368 {
369 $fieldName = current($fields);
370 $field = $this->entity->getField($fieldName);
371
372 return ($field instanceof Relation)
373 ? $this->sysGetCollection($fieldName)
374 : $this->sysGetList($fieldName);
375 }
376 }
377
378 final public function save($ignoreEvents = false)
379 {
380 $result = new Result;
381
383 $addObjects = [];
384
386 $updateObjects = [];
387
388 foreach ($this->_objects as $object)
389 {
390 if ($object->sysGetState() === State::RAW)
391 {
392 $addObjects[] = ['__object' => $object];
393 }
394 elseif ($object->sysGetState() === State::CHANGED)
395 {
396 $updateObjects[] = $object;
397 }
398 }
399
400 $dataClass = static::$dataClass;
401
402 // multi add
403 if (!empty($addObjects))
404 {
405 $result = $dataClass::addMulti($addObjects, $ignoreEvents);
406 }
407
408 // multi update
409 if (!empty($updateObjects))
410 {
411 $areEqual = true;
412 $primaries = [];
413
414 $dataSample = $updateObjects[0]->collectValues(Values::CURRENT, FieldTypeMask::SCALAR | FieldTypeMask::USERTYPE);
415 asort($dataSample);
416
417 // get only scalar & uf data and check its uniqueness
418 foreach ($updateObjects as $object)
419 {
420 $objectData = $object->collectValues(Values::CURRENT, FieldTypeMask::SCALAR | FieldTypeMask::USERTYPE);
421 asort($objectData);
422
423 if ($dataSample !== $objectData)
424 {
425 $areEqual = false;
426 break;
427 }
428
429 $primaries[] = $object->primary;
430 }
431
432 if ($areEqual)
433 {
434 // one query
435 $result = $dataClass::updateMulti($primaries, $dataSample, $ignoreEvents);
436
437 // post save
438 foreach ($updateObjects as $object)
439 {
440 $object->sysSaveRelations($result);
441 $object->sysPostSave();
442 }
443 }
444 else
445 {
446 // each object separately
447 foreach ($updateObjects as $object)
448 {
449 $objectResult = $object->save();
450
451 if (!$objectResult->isSuccess())
452 {
453 $result->addErrors($objectResult->getErrors());
454 }
455 }
456 }
457 }
458
459 return $result;
460 }
461
471 final public static function wakeUp($rows)
472 {
473 // define object class
474 $dataClass = static::$dataClass;
475 $objectClass = $dataClass::getObjectClass();
476
477 // complete collection
478 $collection = new static;
479
480 foreach ($rows as $row)
481 {
482 $collection->sysAddActual($objectClass::wakeUp($row));
483 }
484
485 return $collection;
486 }
487
496 public function __get($name)
497 {
498 switch ($name)
499 {
500 case 'entity':
501 return $this->_entity;
502 case 'dataClass':
503 throw new SystemException('Property `dataClass` should be received as static.');
504 }
505
506 throw new SystemException(sprintf(
507 'Unknown property `%s` for collection `%s`', $name, get_called_class()
508 ));
509 }
510
519 public function __set($name, $value)
520 {
521 switch ($name)
522 {
523 case 'entity':
524 case 'dataClass':
525 throw new SystemException(sprintf(
526 'Property `%s` for collection `%s` is read-only', $name, get_called_class()
527 ));
528 }
529
530 throw new SystemException(sprintf(
531 'Unknown property `%s` for collection `%s`', $name, get_called_class()
532 ));
533 }
534
545 public function __call($name, $arguments)
546 {
547 $first3 = substr($name, 0, 3);
548 $last4 = substr($name, -4);
549
550 // group getter
551 if ($first3 == 'get' && $last4 == 'List')
552 {
553 $fieldName = EntityObject::sysMethodToFieldCase(substr($name, 3, -4));
554
555 if ($fieldName == '')
556 {
557 $fieldName = StringHelper::strtoupper($arguments[0]);
558
559 // check if custom method exists
560 $personalMethodName = $first3.EntityObject::sysFieldToMethodCase($fieldName).$last4;
561
562 if (method_exists($this, $personalMethodName))
563 {
564 return $this->$personalMethodName(...array_slice($arguments, 1));
565 }
566
567 // hard field check
568 $this->entity->getField($fieldName);
569 }
570
571 // check if field exists
572 if ($this->_entity->hasField($fieldName))
573 {
574 return $this->sysGetList($fieldName);
575 }
576 }
577
578 $last10 = substr($name, -10);
579
580 if ($first3 == 'get' && $last10 == 'Collection')
581 {
582 $fieldName = EntityObject::sysMethodToFieldCase(substr($name, 3, -10));
583
584 if ($fieldName == '')
585 {
586 $fieldName = StringHelper::strtoupper($arguments[0]);
587
588 // check if custom method exists
589 $personalMethodName = $first3.EntityObject::sysFieldToMethodCase($fieldName).$last10;
590
591 if (method_exists($this, $personalMethodName))
592 {
593 return $this->$personalMethodName(...array_slice($arguments, 1));
594 }
595
596 // hard field check
597 $this->entity->getField($fieldName);
598 }
599
600 // check if field exists
601 if ($this->_entity->hasField($fieldName) && $this->_entity->getField($fieldName) instanceof Relation)
602 {
603 return $this->sysGetCollection($fieldName);
604 }
605 }
606
607 $first4 = substr($name, 0, 4);
608
609 // filler
610 if ($first4 == 'fill')
611 {
612 $fieldName = EntityObject::sysMethodToFieldCase(substr($name, 4));
613
614 // check if field exists
615 if ($this->_entity->hasField($fieldName))
616 {
617 return $this->fill([$fieldName]);
618 }
619 }
620
621 throw new SystemException(sprintf(
622 'Unknown method `%s` for object `%s`', $name, get_called_class()
623 ));
624 }
625
634 public function sysAddActual(EntityObject $object)
635 {
636 $this->_objects[$this->sysGetPrimaryKey($object)] = $object;
637 }
638
644 public function sysOnObjectPrimarySet($object)
645 {
646 $srHash = spl_object_hash($object);
647 $srPrimary = $this->sysSerializePrimaryKey($object->primary);
648
649 if (isset($this->_objects[$srHash]))
650 {
651 // rewrite object
652 unset($this->_objects[$srHash]);
653 $this->_objects[$srPrimary] = $object;
654
655 // rewrite changes
656 if (isset($this->_objectsChanges[$srHash]))
657 {
658 $this->_objectsChanges[$srPrimary] = $this->_objectsChanges[$srHash];
659 unset($this->_objectsChanges[$srHash]);
660 }
661
662 // rewrite removed registry
663 if (isset($this->_objectsRemoved[$srHash]))
664 {
665 $this->_objectsRemoved[$srPrimary] = $this->_objectsRemoved[$srHash];
666 unset($this->_objectsRemoved[$srHash]);
667 }
668 }
669 }
670
676 public function sysIsFilled()
677 {
678 return $this->_isFilled;
679 }
680
686 public function sysIsChanged()
687 {
688 return !empty($this->_objectsChanges);
689 }
690
697 public function sysGetChanges()
698 {
699 $changes = [];
700
701 foreach ($this->_objectsChanges as $srPrimary => $changeCode)
702 {
703 if (isset($this->_objects[$srPrimary]))
704 {
705 $changedObject = $this->_objects[$srPrimary];
706 }
707 elseif (isset($this->_objectsRemoved[$srPrimary]))
708 {
709 $changedObject = $this->_objectsRemoved[$srPrimary];
710 }
711 else
712 {
713 $changedObject = null;
714 }
715
716 if (empty($changedObject))
717 {
718 throw new SystemException(sprintf(
719 'Object with primary `%s` was not found in `%s` collection', $srPrimary, get_class($this)
720 ));
721 }
722
723 $changes[] = [$changedObject, $changeCode];
724 }
725
726 return $changes;
727 }
728
734 public function sysResetChanges($rollback = false)
735 {
736 if ($rollback)
737 {
738 foreach ($this->_objectsChanges as $srPrimary => $changeCode)
739 {
740 if ($changeCode === static::OBJECT_ADDED)
741 {
742 unset($this->_objects[$srPrimary]);
743 }
744 elseif ($changeCode === static::OBJECT_REMOVED)
745 {
746 $this->_objects[$srPrimary] = $this->_objectsRemoved[$srPrimary];
747 }
748 }
749 }
750
751 $this->_objectsChanges = [];
752 $this->_objectsRemoved = [];
753 }
754
761 protected function sysGetList($fieldName)
762 {
763 $values = [];
764
765 // collect field values
766 foreach ($this->_objects as $objectPrimary => $object)
767 {
768 $values[] = $object->sysGetValue($fieldName);
769 }
770
771 return $values;
772 }
773
781 protected function sysGetCollection($fieldName)
782 {
784 $field = $this->_entity->getField($fieldName);
785
787 $values = $field->getRefEntity()->createCollection();
788
789 // collect field values
790 foreach ($this->_objects as $objectPrimary => $object)
791 {
792 $value = $object->sysGetValue($fieldName);
793
794 if ($value instanceof EntityObject)
795 {
796 $values[] = $value;
797 }
798 elseif ($value instanceof Collection)
799 {
800 foreach ($value->getAll() as $remoteObject)
801 {
802 $values[] = $remoteObject;
803 }
804 }
805 }
806
807 return $values;
808 }
809
813 public function sysReviseDeletedObjects()
814 {
815 // clear from deleted objects
816 foreach ($this->_objects as $k => $object)
817 {
818 if ($object->state === State::DELETED)
819 {
820 unset($this->_objects[$k]);
821 }
822 }
823 }
824
830 public function sysSetFilled($value = true)
831 {
832 $this->_isFilled = $value;
833 }
834
843 protected function sysNormalizePrimary($primary)
844 {
845 // normalize primary
846 $primaryNames = $this->_entity->getPrimaryArray();
847
848 if (!is_array($primary))
849 {
850 if (count($primaryNames) > 1)
851 {
852 throw new ArgumentException(sprintf(
853 'Only one value of primary found, when entity %s has %s primary keys',
854 $this->_entity->getDataClass(), count($primaryNames)
855 ));
856 }
857
858 $primary = [$primaryNames[0] => $primary];
859 }
860
861 // check in $this->objects
862 $normalizedPrimary = [];
863
864 foreach ($primaryNames as $primaryName)
865 {
867 $field = $this->_entity->getField($primaryName);
868 $normalizedPrimary[$primaryName] = $field->cast($primary[$primaryName]);
869 }
870
871 return $normalizedPrimary;
872 }
873
883 protected function sysGetPrimaryKey(EntityObject $object)
884 {
885 if ($object->sysHasPrimary())
886 {
887 return $this->sysSerializePrimaryKey($object->primary);
888 }
889 else
890 {
891 return spl_object_hash($object);
892 }
893 }
894
903 protected function sysSerializePrimaryKey($primary)
904 {
905 if ($this->_isSinglePrimary)
906 {
907 return current($primary);
908 }
909
910 return Json::encode(array_values($primary));
911 }
912
922 public function offsetSet($offset, $value): void
923 {
924 $this->add($value);
925 }
926
935 public function offsetExists($offset): bool
936 {
937 throw new NotImplementedException;
938 }
939
947 public function offsetUnset($offset): void
948 {
949 throw new NotImplementedException;
950 }
951
960 #[\ReturnTypeWillChange]
961 public function offsetGet($offset)
962 {
963 throw new NotImplementedException;
964 }
965
969 public function rewind(): void
970 {
971 $this->_iterableObjects = $this->_objects;
972 reset($this->_iterableObjects);
973 }
974
980 #[\ReturnTypeWillChange]
981 public function current()
982 {
983 if ($this->_iterableObjects === null)
984 {
985 $this->_iterableObjects = $this->_objects;
986 }
987
988 return current($this->_iterableObjects);
989 }
990
996 #[\ReturnTypeWillChange]
997 public function key()
998 {
999 return key($this->_iterableObjects);
1000 }
1001
1005 public function next(): void
1006 {
1007 next($this->_iterableObjects);
1008 }
1009
1015 public function valid(): bool
1016 {
1017 return key($this->_iterableObjects) !== null;
1018 }
1019
1025 public function count(): int
1026 {
1027 return count($this->_objects);
1028 }
1029
1034 public function merge(?self $collection): self
1035 {
1036 if (is_null($collection))
1037 {
1038 return $this;
1039 }
1040
1041 if (get_class($this) !== get_class($collection))
1042 {
1043 throw new ArgumentException(
1044 'Invalid object class ' . get_class($collection) . ' for merge, ' . get_class($this) . ' expected .'
1045 );
1046 }
1047
1048 foreach ($collection as $item)
1049 {
1050 $this->add($item);
1051 }
1052
1053 return $this;
1054 }
1055
1056 public function isEmpty(): bool
1057 {
1058 return $this->count() === 0;
1059 }
1060}
sysAddActual(EntityObject $object)
sysGetPrimaryKey(EntityObject $object)
fill($fields=FieldTypeMask::ALL)